Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.CollectorRestoration.cs

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()
: [],
},
],
};
}
}