Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user