271 lines
9.4 KiB
C#
271 lines
9.4 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgeRouterIterative
|
|
{
|
|
private static bool CanRepairEdgeLocally(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)
|
|
&& HasClearOrthogonalShortcut(edge, nodes);
|
|
}
|
|
|
|
private static bool CanRouteSelectedRepairEdge(
|
|
ElkRoutedEdge edge,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
IReadOnlySet<string> routeRepairEdgeIds)
|
|
{
|
|
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!routeRepairEdgeIds.Contains(edge.Id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
|
|
&& !ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
|
|
}
|
|
|
|
private static bool HasClearOrthogonalShortcut(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes)
|
|
{
|
|
var firstSection = edge.Sections.FirstOrDefault();
|
|
var lastSection = edge.Sections.LastOrDefault();
|
|
if (firstSection is null || lastSection is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var start = firstSection.StartPoint;
|
|
var end = lastSection.EndPoint;
|
|
var obstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
|
|
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
|
|
!ElkEdgePostProcessor.SegmentCrossesObstacle(
|
|
from,
|
|
to,
|
|
obstacles,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
|
|
if (Math.Abs(start.X - end.X) < 2d || Math.Abs(start.Y - end.Y) < 2d)
|
|
{
|
|
return SegmentIsClear(start, end);
|
|
}
|
|
|
|
var horizontalThenVertical = new ElkPoint { X = end.X, Y = start.Y };
|
|
if (SegmentIsClear(start, horizontalThenVertical) && SegmentIsClear(horizontalThenVertical, end))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var verticalThenHorizontal = new ElkPoint { X = start.X, Y = end.Y };
|
|
return SegmentIsClear(start, verticalThenHorizontal)
|
|
&& SegmentIsClear(verticalThenHorizontal, end);
|
|
}
|
|
|
|
private static int CountRepeatCollectorNodeClearanceViolations(
|
|
IReadOnlyCollection<ElkRoutedEdge> edges,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
Dictionary<string, int>? severityByEdgeId,
|
|
int severityWeight = 1)
|
|
{
|
|
if (edges.Count == 0 || nodes.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
|
var minClearance = serviceNodes.Length > 0
|
|
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
|
: 50d;
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var count = 0;
|
|
|
|
foreach (var edge in edges)
|
|
{
|
|
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var edgeViolations = 0;
|
|
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
|
{
|
|
var horizontal = Math.Abs(segment.Start.Y - segment.End.Y) < 2d;
|
|
var vertical = Math.Abs(segment.Start.X - segment.End.X) < 2d;
|
|
if (!horizontal && !vertical)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var node in nodes)
|
|
{
|
|
if (node.Id == edge.SourceNodeId || node.Id == edge.TargetNodeId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (horizontal)
|
|
{
|
|
var overlapX = Math.Max(segment.Start.X, segment.End.X) > node.X
|
|
&& Math.Min(segment.Start.X, segment.End.X) < node.X + node.Width;
|
|
if (!overlapX)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = Math.Min(
|
|
Math.Abs(segment.Start.Y - node.Y),
|
|
Math.Abs(segment.Start.Y - (node.Y + node.Height)));
|
|
if (distance > 0.5d && distance < minClearance)
|
|
{
|
|
edgeViolations++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var overlapY = Math.Max(segment.Start.Y, segment.End.Y) > node.Y
|
|
&& Math.Min(segment.Start.Y, segment.End.Y) < node.Y + node.Height;
|
|
if (!overlapY)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = Math.Min(
|
|
Math.Abs(segment.Start.X - node.X),
|
|
Math.Abs(segment.Start.X - (node.X + node.Width)));
|
|
if (distance > 0.5d && distance < minClearance)
|
|
{
|
|
edgeViolations++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (edgeViolations <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
count += edgeViolations;
|
|
if (severityByEdgeId is not null)
|
|
{
|
|
severityByEdgeId[edge.Id] = severityByEdgeId.GetValueOrDefault(edge.Id) + (edgeViolations * severityWeight);
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static (ElkPoint StartPoint, ElkPoint EndPoint) ResolveRoutingEndpoints(
|
|
ElkPoint startPoint,
|
|
ElkPoint endPoint,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
|
|
{
|
|
var adjustedStart = startPoint;
|
|
if (nodesById.TryGetValue(sourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
adjustedStart = ResolveGatewayRoutingDeparturePoint(sourceNode, endPoint, startPoint);
|
|
}
|
|
|
|
var adjustedEnd = endPoint;
|
|
if (nodesById.TryGetValue(targetNodeId ?? string.Empty, out var targetNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
adjustedEnd = ResolveGatewayRoutingApproachPoint(targetNode, adjustedStart, endPoint);
|
|
}
|
|
|
|
return (adjustedStart, adjustedEnd);
|
|
}
|
|
|
|
private static ElkPoint ResolveGatewayRoutingBoundary(
|
|
ElkPositionedNode node,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint);
|
|
return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, boundary, referencePoint);
|
|
}
|
|
|
|
private static ElkPoint ResolveGatewayRoutingDeparturePoint(
|
|
ElkPositionedNode node,
|
|
ElkPoint referencePoint,
|
|
ElkPoint preferredBoundary)
|
|
{
|
|
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
|
|
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
|
|
: ResolveGatewayRoutingBoundary(node, referencePoint);
|
|
var exteriorDeparture = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
|
|
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorDeparture)
|
|
? boundary
|
|
: exteriorDeparture;
|
|
}
|
|
|
|
private static ElkPoint ResolveGatewayRoutingApproachPoint(
|
|
ElkPositionedNode node,
|
|
ElkPoint referencePoint,
|
|
ElkPoint preferredBoundary)
|
|
{
|
|
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
|
|
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
|
|
: ResolveGatewayRoutingBoundary(node, referencePoint);
|
|
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
|
|
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorApproach)
|
|
? boundary
|
|
: exteriorApproach;
|
|
}
|
|
|
|
}
|