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