279 lines
9.6 KiB
C#
279 lines
9.6 KiB
C#
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()
|
|
: [],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
}
|