namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { internal static ElkRoutedEdge[] SeparateMixedNodeFaceLaneConflicts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); var entries = new List<(int Index, ElkRoutedEdge Edge, IReadOnlyList Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)>(); for (var index = 0; index < result.Length; index++) { var edge = result[index]; var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) && (ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY) || ElkShapeBoundaries.IsGatewayShape(sourceNode))) { var side = ResolveSourceDepartureSide(path, sourceNode); var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex) ? side is "left" or "right" ? path[runEndIndex].X : path[runEndIndex].Y : ResolveDefaultSourceDepartureAxis(sourceNode, side); entries.Add(( index, edge, path, sourceNode, side, true, path[0], side is "left" or "right" ? path[0].Y : path[0].X, axisValue)); } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) && ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY)) { var side = ResolveTargetApproachSide(path, targetNode); var axisValue = ResolveTargetApproachAxisValue(path, side); if (double.IsNaN(axisValue)) { axisValue = side is "left" or "right" ? path[^1].Y : path[^1].X; } entries.Add(( index, edge, path, targetNode, side, false, path[^1], side is "left" or "right" ? path[^1].Y : path[^1].X, axisValue)); } } foreach (var group in entries.GroupBy( entry => $"{entry.Node.Id}|{entry.Side}", StringComparer.Ordinal)) { var groupEntries = group.ToArray(); var hasBoundarySlotIssue = groupEntries.Length >= 2 && HasBoundarySlotAlignmentIssue( groupEntries .Select(entry => (entry.Edge.Id, entry.BoundaryCoordinate, entry.IsOutgoing)) .ToArray(), groupEntries[0].Node, groupEntries[0].Side, minLineClearance); if (groupEntries.Length < 2 || !groupEntries.Any(entry => entry.IsOutgoing) || !groupEntries.Any(entry => !entry.IsOutgoing) || (!GroupHasMixedNodeFaceLaneConflict(groupEntries, minLineClearance) && !hasBoundarySlotIssue)) { continue; } if (restrictedSet is not null && !groupEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id))) { continue; } var node = groupEntries[0].Node; var side = groupEntries[0].Side; var orderedEntries = groupEntries .OrderBy(entry => entry.BoundaryCoordinate) .ThenBy(entry => entry.IsOutgoing ? 0 : 1) .ThenBy(entry => IsRepeatCollectorLabel(entry.Edge.Label) ? 1 : 0) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates( node, side, orderedEntries.Select(entry => entry.BoundaryCoordinate).ToArray()); var desiredCoordinateByEdgeId = new Dictionary(StringComparer.Ordinal); for (var i = 0; i < orderedEntries.Length; i++) { desiredCoordinateByEdgeId[orderedEntries[i].Edge.Id] = assignedSlotCoordinates[i]; } var hasAssignedSlotCollision = HasDuplicateBoundarySlotCoordinates(assignedSlotCoordinates); foreach (var entry in groupEntries) { var forceAlternateGatewayFaceCandidate = hasAssignedSlotCollision && ElkShapeBoundaries.IsGatewayShape(entry.Node); if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var desiredCoordinate) || (!forceAlternateGatewayFaceCandidate && Math.Abs(desiredCoordinate - entry.BoundaryCoordinate) <= 0.5d)) { continue; } var bestEdge = result[entry.Index]; var currentGroupEdges = groupEntries .Select(item => result[item.Index]) .ToArray(); var bestSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentGroupEdges, nodes); var bestTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentGroupEdges, nodes); var bestBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(currentGroupEdges, nodes); var bestBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(currentGroupEdges, nodes); var bestGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(currentGroupEdges, nodes); var bestUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(currentGroupEdges, nodes); var bestPathLength = ComputePathLength(entry.Path); var prefersAlternateRepeatFace = !entry.IsOutgoing && !ElkShapeBoundaries.IsGatewayShape(entry.Node) && IsRepeatCollectorLabel(entry.Edge.Label) && groupEntries.Any(other => other.IsOutgoing); var candidatePaths = new List>(); var directCandidate = entry.IsOutgoing ? BuildMixedSourceFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue) : BuildMixedTargetFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue); AddUniquePathCandidate(candidatePaths, directCandidate); var availableSpan = Math.Abs(desiredCoordinate - entry.BoundaryCoordinate); if ((forceAlternateGatewayFaceCandidate || prefersAlternateRepeatFace || availableSpan + 0.5d < minLineClearance) && TryBuildAlternateMixedFaceCandidate(entry, nodes, minLineClearance, out var alternateCandidate)) { AddUniquePathCandidate(candidatePaths, alternateCandidate); } foreach (var candidate in candidatePaths) { if (!PathChanged(entry.Path, candidate) || HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId)) { continue; } if (entry.IsOutgoing) { if (ElkShapeBoundaries.IsGatewayShape(entry.Node)) { if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: true)) { continue; } } else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2) || !HasValidBoundaryAngle(candidate[0], candidate[1], entry.Node)) { continue; } } else { if (ElkShapeBoundaries.IsGatewayShape(entry.Node)) { if (!CanAcceptGatewayTargetRepair(candidate, entry.Node) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: false)) { continue; } } else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, false, 4) || !HasValidBoundaryAngle(candidate[^1], candidate[^2], entry.Node) || HasTargetApproachBacktracking(candidate, entry.Node)) { continue; } } var candidateEdge = BuildSingleSectionEdge(entry.Edge, candidate); var candidateGroupEdges = groupEntries .Select(item => item.Index == entry.Index ? candidateEdge : result[item.Index]) .ToArray(); var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateGroupEdges, nodes); var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateGroupEdges, nodes); var candidateBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidateGroupEdges, nodes); var candidateBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidateGroupEdges, nodes); var candidateGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateGroupEdges, nodes); var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateGroupEdges, nodes); var candidatePathLength = ComputePathLength(candidate); var prefersForcedAlternateGatewayFace = forceAlternateGatewayFaceCandidate && entry.IsOutgoing && ResolveSourceDepartureSide(candidate, entry.Node) != entry.Side && ResolveSourceDepartureSide(ExtractFullPath(bestEdge), entry.Node) == entry.Side && candidateSharedLaneViolations <= bestSharedLaneViolations && candidateTargetJoinViolations <= bestTargetJoinViolations && candidateBoundarySlotViolations <= bestBoundarySlotViolations && candidateBoundaryAngleViolations <= bestBoundaryAngleViolations && candidateGatewaySourceExitViolations <= bestGatewaySourceExitViolations && candidateUnderNodeViolations <= bestUnderNodeViolations; if (prefersForcedAlternateGatewayFace) { bestEdge = candidateEdge; bestSharedLaneViolations = candidateSharedLaneViolations; bestTargetJoinViolations = candidateTargetJoinViolations; bestBoundarySlotViolations = candidateBoundarySlotViolations; bestBoundaryAngleViolations = candidateBoundaryAngleViolations; bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations; bestUnderNodeViolations = candidateUnderNodeViolations; bestPathLength = candidatePathLength; continue; } if (!IsBetterMixedNodeFaceCandidate( candidateSharedLaneViolations, candidateTargetJoinViolations, candidateBoundarySlotViolations, candidateBoundaryAngleViolations, candidateGatewaySourceExitViolations, candidateUnderNodeViolations, candidatePathLength, bestSharedLaneViolations, bestTargetJoinViolations, bestBoundarySlotViolations, bestBoundaryAngleViolations, bestGatewaySourceExitViolations, bestUnderNodeViolations, bestPathLength)) { continue; } bestEdge = candidateEdge; bestSharedLaneViolations = candidateSharedLaneViolations; bestTargetJoinViolations = candidateTargetJoinViolations; bestBoundarySlotViolations = candidateBoundarySlotViolations; bestBoundaryAngleViolations = candidateBoundaryAngleViolations; bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations; bestUnderNodeViolations = candidateUnderNodeViolations; bestPathLength = candidatePathLength; } result[entry.Index] = bestEdge; } } return result; } private static void AddUniquePathCandidate( ICollection> candidates, IReadOnlyList candidate) { if (candidates.Any(existing => existing.Count == candidate.Count && existing.Zip(candidate, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))) { return; } candidates.Add(candidate); } private static bool IsBetterMixedNodeFaceCandidate( int candidateSharedLaneViolations, int candidateTargetJoinViolations, int candidateBoundarySlotViolations, int candidateBoundaryAngleViolations, int candidateGatewaySourceExitViolations, int candidateUnderNodeViolations, double candidatePathLength, int currentSharedLaneViolations, int currentTargetJoinViolations, int currentBoundarySlotViolations, int currentBoundaryAngleViolations, int currentGatewaySourceExitViolations, int currentUnderNodeViolations, double currentPathLength) { if (candidateSharedLaneViolations != currentSharedLaneViolations) { return candidateSharedLaneViolations < currentSharedLaneViolations; } if (candidateTargetJoinViolations != currentTargetJoinViolations) { return candidateTargetJoinViolations < currentTargetJoinViolations; } if (candidateBoundarySlotViolations != currentBoundarySlotViolations) { return candidateBoundarySlotViolations < currentBoundarySlotViolations; } if (candidateBoundaryAngleViolations != currentBoundaryAngleViolations) { return candidateBoundaryAngleViolations < currentBoundaryAngleViolations; } if (candidateGatewaySourceExitViolations != currentGatewaySourceExitViolations) { return candidateGatewaySourceExitViolations < currentGatewaySourceExitViolations; } if (candidateUnderNodeViolations != currentUnderNodeViolations) { return candidateUnderNodeViolations < currentUnderNodeViolations; } return candidatePathLength + 0.5d < currentPathLength; } }