Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static ElkRoutedEdge[] RestoreProtectedRepeatCollectorCorridors(
|
||||
ElkRoutedEdge[] candidateEdges,
|
||||
ElkRoutedEdge[] referenceEdges,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (candidateEdges.Length == 0 || referenceEdges.Length == 0 || nodes.Length == 0)
|
||||
{
|
||||
return candidateEdges;
|
||||
}
|
||||
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
||||
var minLineClearance = ResolveMinLineClearance(nodes);
|
||||
var obstacles = BuildObstacles(nodes, 0d);
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var restoredIds = new List<string>();
|
||||
var result = candidateEdges.ToArray();
|
||||
|
||||
for (var i = 0; i < result.Length && i < referenceEdges.Length; i++)
|
||||
{
|
||||
var reference = referenceEdges[i];
|
||||
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(reference.Label)
|
||||
|| !ElkEdgePostProcessor.HasCorridorBendPoints(reference, graphMinY, graphMaxY)
|
||||
|| ElkEdgePostProcessor.HasCorridorBendPoints(result[i], graphMinY, graphMaxY)
|
||||
|| EdgeCrossesNode(reference, obstacles)
|
||||
|| EdgeViolatesNodeClearance(reference, nodes, minLineClearance))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var restored = reference;
|
||||
if (nodesById.TryGetValue(reference.SourceNodeId ?? string.Empty, out var sourceNode)
|
||||
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
||||
{
|
||||
restored = AlignProtectedCollectorGatewaySourceExit(restored, sourceNode, graphMinY, graphMaxY);
|
||||
}
|
||||
|
||||
result[i] = restored;
|
||||
restoredIds.Add(restored.Id);
|
||||
}
|
||||
|
||||
return restoredIds.Count > 0
|
||||
? ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes, restoredIds)
|
||||
: result;
|
||||
}
|
||||
|
||||
private static bool EdgeViolatesNodeClearance(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
double minLineClearance)
|
||||
{
|
||||
if (minLineClearance <= 0d)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
||||
{
|
||||
var horizontal = Math.Abs(segment.Start.Y - segment.End.Y) <= 0.5d;
|
||||
var vertical = Math.Abs(segment.Start.X - segment.End.X) <= 0.5d;
|
||||
if (!horizontal && !vertical)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Id == edge.SourceNodeId || node.Id == edge.TargetNodeId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (horizontal)
|
||||
{
|
||||
var minX = Math.Min(segment.Start.X, segment.End.X);
|
||||
var maxX = Math.Max(segment.Start.X, segment.End.X);
|
||||
if (maxX <= node.X || minX >= node.X + node.Width)
|
||||
{
|
||||
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 < minLineClearance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var minY = Math.Min(segment.Start.Y, segment.End.Y);
|
||||
var maxY = Math.Max(segment.Start.Y, segment.End.Y);
|
||||
if (maxY <= node.Y || minY >= node.Y + node.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var horizontalDistance = Math.Min(
|
||||
Math.Abs(segment.Start.X - node.X),
|
||||
Math.Abs(segment.Start.X - (node.X + node.Width)));
|
||||
if (horizontalDistance > 0.5d && horizontalDistance < minLineClearance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EdgeCrossesNode(
|
||||
ElkRoutedEdge edge,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles)
|
||||
{
|
||||
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
||||
{
|
||||
if (ElkEdgePostProcessor.SegmentCrossesObstacle(
|
||||
segment.Start,
|
||||
segment.End,
|
||||
obstacles,
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge AlignProtectedCollectorGatewaySourceExit(
|
||||
ElkRoutedEdge edge,
|
||||
ElkPositionedNode sourceNode,
|
||||
double graphMinY,
|
||||
double graphMaxY)
|
||||
{
|
||||
var path = new List<ElkPoint>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
{
|
||||
path.Add(section.StartPoint);
|
||||
}
|
||||
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
}
|
||||
|
||||
if (path.Count < 2)
|
||||
{
|
||||
return edge;
|
||||
}
|
||||
|
||||
var corridorIndex = 1;
|
||||
for (var i = 1; i < path.Count; i++)
|
||||
{
|
||||
if (path[i].Y < graphMinY - 8d || path[i].Y > graphMaxY + 8d)
|
||||
{
|
||||
corridorIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var corridorPoint = path[corridorIndex];
|
||||
var boundaryReferences = sourceNode.Kind == "Decision"
|
||||
? new[]
|
||||
{
|
||||
(BoundaryReference: path[^1], ExitReference: path[^1]),
|
||||
(BoundaryReference: corridorPoint, ExitReference: corridorPoint),
|
||||
(BoundaryReference: corridorPoint, ExitReference: path[^1]),
|
||||
}
|
||||
: new[]
|
||||
{
|
||||
(BoundaryReference: corridorPoint, ExitReference: corridorPoint),
|
||||
};
|
||||
|
||||
List<ElkPoint>? rebuilt = null;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
foreach (var (boundaryReference, exitReference) in boundaryReferences)
|
||||
{
|
||||
var boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, boundaryReference);
|
||||
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, boundaryReference);
|
||||
var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, exitReference);
|
||||
var desiredExitDx = exitReference.X - boundary.X;
|
||||
var desiredExitDy = exitReference.Y - boundary.Y;
|
||||
if (Math.Abs(desiredExitDx) >= Math.Abs(desiredExitDy) * 1.15d && Math.Sign(desiredExitDx) != 0)
|
||||
{
|
||||
exteriorApproach = new ElkPoint
|
||||
{
|
||||
X = desiredExitDx > 0d
|
||||
? sourceNode.X + sourceNode.Width + 8d
|
||||
: sourceNode.X - 8d,
|
||||
Y = boundary.Y,
|
||||
};
|
||||
}
|
||||
else if (Math.Abs(desiredExitDy) >= Math.Abs(desiredExitDx) * 1.15d && Math.Sign(desiredExitDy) != 0)
|
||||
{
|
||||
exteriorApproach = new ElkPoint
|
||||
{
|
||||
X = boundary.X,
|
||||
Y = desiredExitDy > 0d
|
||||
? sourceNode.Y + sourceNode.Height + 8d
|
||||
: sourceNode.Y - 8d,
|
||||
};
|
||||
}
|
||||
|
||||
var candidate = new List<ElkPoint> { boundary };
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
|
||||
{
|
||||
candidate.Add(exteriorApproach);
|
||||
}
|
||||
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corridorPoint))
|
||||
{
|
||||
var corner = BuildOrthogonalCollectorCorner(candidate[^1], corridorPoint);
|
||||
if (corner is not null && !ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corner))
|
||||
{
|
||||
candidate.Add(corner);
|
||||
}
|
||||
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corridorPoint))
|
||||
{
|
||||
candidate.Add(corridorPoint);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = corridorIndex + 1; i < path.Count; i++)
|
||||
{
|
||||
candidate.Add(path[i]);
|
||||
}
|
||||
|
||||
candidate = NormalizeProtectedCollectorTail(candidate, graphMinY, graphMaxY);
|
||||
var score = ScoreProtectedCollectorGatewaySourceExitCandidate(candidate, sourceNode, exitReference);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
rebuilt = candidate;
|
||||
}
|
||||
|
||||
rebuilt ??= NormalizeProtectedCollectorTail(path, graphMinY, graphMaxY);
|
||||
|
||||
return new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
SourcePortId = edge.SourcePortId,
|
||||
TargetPortId = edge.TargetPortId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = rebuilt[0],
|
||||
EndPoint = rebuilt[^1],
|
||||
BendPoints = rebuilt.Count > 2
|
||||
? rebuilt.Skip(1).Take(rebuilt.Count - 2).ToArray()
|
||||
: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user