using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static CandidateSolution RefineWinningSolution( CandidateSolution best, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance) { static string DescribeSolution(CandidateSolution solution) { return $"score={solution.Score.Value:F0} retry={DescribeRetryState(solution.RetryState)}"; } var current = best; ElkLayoutDiagnostics.LogProgress($"Winner refinement start: {DescribeSolution(current)}"); for (var round = 0; round < 3; round++) { var severityByEdgeId = new Dictionary(StringComparer.Ordinal); var pressure = ElkEdgeRoutingScoring.CountBadBoundaryAngles(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountBelowGraphViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, severityByEdgeId, 10); if (pressure == 0) { ElkLayoutDiagnostics.LogProgress($"Winner refinement pressure clear after round {round + 1}: {DescribeSolution(current)}"); break; } var improved = false; foreach (var focusRootEdgeId in severityByEdgeId .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [focusRootEdgeId]).ToArray(); if (focusEdgeIds.Length == 0) { continue; } var candidateEdges = CloseRemainingTerminalViolations( current.Edges, nodes, direction, minLineClearance, focusEdgeIds); var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); if (!IsBetterBoundarySlotRepairCandidate( candidateScore, candidateRetryState, current.Score, current.RetryState)) { continue; } current = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; improved = true; break; } if (!improved) { ElkLayoutDiagnostics.LogProgress($"Winner refinement pressure round {round + 1} made no improvement: {DescribeSolution(current)}"); break; } ElkLayoutDiagnostics.LogProgress($"Winner refinement pressure round {round + 1} improved: {DescribeSolution(current)}"); } ElkLayoutDiagnostics.LogProgress($"Winner refinement before under-node polish: {DescribeSolution(current)}"); current = ApplyFinalDirectUnderNodePolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after under-node polish: {DescribeSolution(current)}"); current = ApplyFinalProtectedLocalBundlePolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after local-bundle polish: {DescribeSolution(current)}"); current = ApplyFinalSharedLanePolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after shared-lane polish: {DescribeSolution(current)}"); current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after boundary-slot polish: {DescribeSolution(current)}"); current = ApplyWinnerDetourPolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after detour polish: {DescribeSolution(current)}"); current = ApplyFinalPostSlotHardRulePolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after post-slot hard-rule polish: {DescribeSolution(current)}"); if (current.RetryState.SharedLaneViolations == 0 && (current.RetryState.BoundarySlotViolations > 0 || current.RetryState.ExcessiveDetourViolations > 0)) { current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after second boundary-slot polish: {DescribeSolution(current)}"); current = ApplyWinnerDetourPolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after second detour polish: {DescribeSolution(current)}"); current = ApplyFinalPostSlotHardRulePolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Winner refinement after second post-slot hard-rule polish: {DescribeSolution(current)}"); } return current; } private static IEnumerable ExpandWinningSolutionFocus( IReadOnlyCollection edges, IEnumerable focusEdgeIds) { var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal); var expanded = new HashSet(StringComparer.Ordinal); foreach (var edgeId in focusEdgeIds) { if (!expanded.Add(edgeId) || !edgesById.TryGetValue(edgeId, out var edge)) { continue; } foreach (var peer in edges) { if (string.Equals(peer.Id, edge.Id, StringComparison.Ordinal)) { continue; } if (string.Equals(peer.SourceNodeId, edge.SourceNodeId, StringComparison.Ordinal) || string.Equals(peer.TargetNodeId, edge.TargetNodeId, StringComparison.Ordinal) || string.Equals(peer.SourceNodeId, edge.TargetNodeId, StringComparison.Ordinal) || string.Equals(peer.TargetNodeId, edge.SourceNodeId, StringComparison.Ordinal)) { expanded.Add(peer.Id); } } } return expanded.OrderBy(edgeId => edgeId, StringComparer.Ordinal); } private static IEnumerable ExpandSharedLanePolishFocus( IReadOnlyCollection edges, IReadOnlyCollection nodes, string focusEdgeId) { var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal); if (!edgesById.TryGetValue(focusEdgeId, out var focusEdge)) { return []; } var focusedEdgeIds = new HashSet(StringComparer.Ordinal) { focusEdgeId, }; var sharedNodeIds = new HashSet(StringComparer.Ordinal); foreach (var (leftEdgeId, rightEdgeId) in ElkEdgeRoutingScoring.DetectSharedLaneConflicts(edges, nodes)) { string partnerEdgeId; if (string.Equals(leftEdgeId, focusEdgeId, StringComparison.Ordinal)) { partnerEdgeId = rightEdgeId; } else if (string.Equals(rightEdgeId, focusEdgeId, StringComparison.Ordinal)) { partnerEdgeId = leftEdgeId; } else { continue; } if (!edgesById.TryGetValue(partnerEdgeId, out var partnerEdge)) { continue; } focusedEdgeIds.Add(partnerEdgeId); CollectSharedConflictNodeIds(focusEdge, partnerEdge, sharedNodeIds); } if (sharedNodeIds.Count == 0) { return ExpandWinningSolutionFocus(edges, [focusEdgeId]); } foreach (var edge in edges) { if (focusedEdgeIds.Contains(edge.Id)) { continue; } if ((edge.SourceNodeId is not null && sharedNodeIds.Contains(edge.SourceNodeId)) || (edge.TargetNodeId is not null && sharedNodeIds.Contains(edge.TargetNodeId))) { focusedEdgeIds.Add(edge.Id); } } return focusedEdgeIds.OrderBy(edgeId => edgeId, StringComparer.Ordinal); } private static void CollectSharedConflictNodeIds( ElkRoutedEdge edge, ElkRoutedEdge partner, ISet sharedNodeIds) { if (edge.SourceNodeId is not null && (string.Equals(edge.SourceNodeId, partner.SourceNodeId, StringComparison.Ordinal) || string.Equals(edge.SourceNodeId, partner.TargetNodeId, StringComparison.Ordinal))) { sharedNodeIds.Add(edge.SourceNodeId); } if (edge.TargetNodeId is not null && (string.Equals(edge.TargetNodeId, partner.SourceNodeId, StringComparison.Ordinal) || string.Equals(edge.TargetNodeId, partner.TargetNodeId, StringComparison.Ordinal))) { sharedNodeIds.Add(edge.TargetNodeId); } } private static CandidateSolution ApplyFinalDirectUnderNodePolish( CandidateSolution solution, ElkPositionedNode[] nodes, double minLineClearance) { var current = solution; var underNodeSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10); if (underNodeSeverity.Count == 0) { return current; } foreach (var edgeId in underNodeSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations( current.Edges, nodes, minLineClearance, [edgeId]); var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); if (!IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState)) { continue; } current = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; } return current; } private static CandidateSolution ApplyFinalProtectedLocalBundlePolish( CandidateSolution solution, ElkPositionedNode[] nodes, double minLineClearance) { var current = solution; var underNodeSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10); var focusSeverity = new Dictionary(underNodeSeverity, StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, focusSeverity, 10); if (focusSeverity.Count == 0) { return current; } foreach (var edgeId in focusSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray(); if (focusEdgeIds.Length == 0) { continue; } var focusedRepairSeeds = focusEdgeIds .Where(underNodeSeverity.ContainsKey) .OrderBy(id => id, StringComparer.Ordinal) .ToArray(); if (focusedRepairSeeds.Length == 0) { focusedRepairSeeds = [edgeId]; } var candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations( current.Edges, nodes, minLineClearance, focusedRepairSeeds); candidateEdges = ElkEdgePostProcessor.SpreadTargetApproachJoins( candidateEdges, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); candidateEdges = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( candidateEdges, nodes, minLineClearance, focusEdgeIds); candidateEdges = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidateEdges, nodes, minLineClearance, focusEdgeIds); candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations( candidateEdges, nodes, minLineClearance, focusEdgeIds); candidateEdges = ElkEdgePostProcessor.SpreadTargetApproachJoins( candidateEdges, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); candidateEdges = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( candidateEdges, nodes, minLineClearance, focusEdgeIds); candidateEdges = ChoosePreferredHardRuleLayout(current.Edges, candidateEdges, nodes); var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); if (!IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState)) { continue; } current = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; underNodeSeverity.Clear(); ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10); } return current; } private static CandidateSolution ApplyFinalSharedLanePolish( CandidateSolution solution, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance) { var current = solution; if (current.RetryState.SharedLaneViolations <= 0) { return current; } for (var round = 0; round < 3; round++) { var sharedLaneSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, sharedLaneSeverity, 10); if (sharedLaneSeverity.Count == 0) { break; } var improved = false; foreach (var edgeId in sharedLaneSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var focusEdgeIds = ExpandSharedLanePolishFocus(current.Edges, nodes, edgeId).ToArray(); if (focusEdgeIds.Length == 0) { continue; } var directCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( current.Edges, nodes, minLineClearance, focusEdgeIds); var directClosureCandidate = CloseRemainingTerminalViolations( directCandidate, nodes, direction, minLineClearance, focusEdgeIds); var closureCandidate = CloseRemainingTerminalViolations( current.Edges, nodes, direction, minLineClearance, focusEdgeIds); var aggressiveCandidate = ApplyAggressiveSharedLaneClosure( current.Edges, nodes, direction, minLineClearance, focusEdgeIds); var directRetryState = BuildRetryState( ElkEdgeRoutingScoring.ComputeScore(directCandidate, nodes), HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(directCandidate, nodes).Count : 0); var directClosureRetryState = BuildRetryState( ElkEdgeRoutingScoring.ComputeScore(directClosureCandidate, nodes), HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(directClosureCandidate, nodes).Count : 0); var closureRetryState = BuildRetryState( ElkEdgeRoutingScoring.ComputeScore(closureCandidate, nodes), HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(closureCandidate, nodes).Count : 0); var aggressiveRetryState = BuildRetryState( ElkEdgeRoutingScoring.ComputeScore(aggressiveCandidate, nodes), HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(aggressiveCandidate, nodes).Count : 0); ElkLayoutDiagnostics.LogProgress( $"Winner shared-lane focus edge={edgeId} focus=[{string.Join(", ", focusEdgeIds)}] " + $"direct={DescribeRetryState(directRetryState)} " + $"direct-closure={DescribeRetryState(directClosureRetryState)} " + $"closure={DescribeRetryState(closureRetryState)} " + $"aggressive={DescribeRetryState(aggressiveRetryState)}"); var candidateEdges = ChoosePreferredSharedLanePolishLayout(directCandidate, directClosureCandidate, nodes); candidateEdges = ChoosePreferredSharedLanePolishLayout(candidateEdges, closureCandidate, nodes); candidateEdges = ChoosePreferredSharedLanePolishLayout(candidateEdges, aggressiveCandidate, nodes); candidateEdges = ChoosePreferredSharedLanePolishLayout(current.Edges, candidateEdges, nodes); var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); var improvedSharedLanes = candidateRetryState.SharedLaneViolations < current.RetryState.SharedLaneViolations; if (HasBlockingSharedLanePromotionRegression(candidateRetryState, current.RetryState) || (!improvedSharedLanes && !IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState))) { continue; } current = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; improved = true; break; } if (!improved) { break; } } return current; } private static CandidateSolution ApplyFinalBoundarySlotPolish( CandidateSolution solution, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance) { var current = solution; for (var round = 0; round < 3; round++) { var boundarySlotSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, boundarySlotSeverity, 10); if (boundarySlotSeverity.Count == 0) { break; } var batchedRootEdgeIds = boundarySlotSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Take(MaxWinnerPolishBatchedRootEdges) .Select(pair => pair.Key) .ToArray(); var batchedFocusEdgeIds = ExpandWinningSolutionFocus(current.Edges, batchedRootEdgeIds).ToArray(); if (batchedFocusEdgeIds.Length > 0) { var batchedCandidateEdges = BuildFinalBoundarySlotCandidate( current.Edges, nodes, direction, minLineClearance, batchedFocusEdgeIds, allowLateRestabilizedClosure: false); if (TryPromoteFinalBoundarySlotCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted)) { current = batchedPromoted; continue; } } var improved = false; foreach (var edgeId in boundarySlotSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray(); if (focusEdgeIds.Length == 0) { continue; } var candidateEdges = BuildFinalBoundarySlotCandidate( current.Edges, nodes, direction, minLineClearance, focusEdgeIds, allowLateRestabilizedClosure: false); if (!TryPromoteFinalBoundarySlotCandidate(current, candidateEdges, nodes, out var promoted)) { continue; } current = promoted; improved = true; break; } if (!improved) { break; } } return current; } private static CandidateSolution ApplyFinalPostSlotHardRulePolish( CandidateSolution solution, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance) { var current = solution; for (var round = 0; round < 3; round++) { var severityByEdgeId = new Dictionary(StringComparer.Ordinal); var pressure = ElkEdgeRoutingScoring.CountBadBoundaryAngles(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountExcessiveDetourViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountBelowGraphViolations(current.Edges, nodes, severityByEdgeId, 10) + ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, severityByEdgeId, 10); ElkLayoutDiagnostics.LogProgress( $"Winner post-slot hard-rule round {round + 1} start: pressure={pressure} retry={DescribeRetryState(current.RetryState)} focus={severityByEdgeId.Count}"); if (pressure == 0) { break; } var preferFastTerminalOnly = ShouldPreferFastTerminalOnlyHardRuleClosure(current.RetryState); var batchedRootEdgeIds = severityByEdgeId .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Take(MaxWinnerPolishBatchedRootEdges) .Select(pair => pair.Key) .ToArray(); var batchedFocusEdgeIds = preferFastTerminalOnly ? batchedRootEdgeIds : ExpandWinningSolutionFocus(current.Edges, batchedRootEdgeIds).ToArray(); if (batchedFocusEdgeIds.Length > 0) { if (preferFastTerminalOnly) { ElkLayoutDiagnostics.LogProgress( $"Winner post-slot hard-rule round {round + 1} fast-terminal focus=[{string.Join(", ", batchedFocusEdgeIds)}]"); var quickBatchedCandidateEdges = BuildFastTerminalOnlyHardRuleCandidate( current.Edges, nodes, direction, minLineClearance, batchedFocusEdgeIds); if (TryPromoteFinalHardRuleCandidate(current, quickBatchedCandidateEdges, nodes, out var quickBatchedPromoted)) { current = quickBatchedPromoted; continue; } var focusedTerminalClosureEdges = CloseRemainingTerminalViolations( current.Edges, nodes, direction, minLineClearance, batchedFocusEdgeIds); if (TryPromoteFinalHardRuleCandidate(current, focusedTerminalClosureEdges, nodes, out var focusedTerminalPromoted)) { current = focusedTerminalPromoted; continue; } var exactRestabilizedEdges = BuildFinalRestabilizedCandidate( current.Edges, nodes, direction, minLineClearance, batchedFocusEdgeIds); if (TryPromoteFinalHardRuleCandidate(current, exactRestabilizedEdges, nodes, out var exactRestabilizedPromoted)) { current = exactRestabilizedPromoted; continue; } var quickBatchedScore = ElkEdgeRoutingScoring.ComputeScore(quickBatchedCandidateEdges, nodes); var quickBatchedRetryState = BuildRetryState( quickBatchedScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(quickBatchedCandidateEdges, nodes).Count : 0); var changedFocusEdgeIds = batchedFocusEdgeIds .Where(edgeId => HasEdgeGeometryChanged(current.Edges, quickBatchedCandidateEdges, edgeId)) .ToArray(); ElkLayoutDiagnostics.LogProgress( $"Winner post-slot hard-rule round {round + 1} fast-terminal made no promotion: candidate={DescribeRetryState(quickBatchedRetryState)} changed=[{string.Join(", ", changedFocusEdgeIds)}]"); } else { ElkLayoutDiagnostics.LogProgress( $"Winner post-slot hard-rule round {round + 1} full-restabilize focus=[{string.Join(", ", batchedFocusEdgeIds)}]"); var batchedCandidateEdges = BuildFinalRestabilizedCandidate( current.Edges, nodes, direction, minLineClearance, batchedFocusEdgeIds); if (TryPromoteFinalHardRuleCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted)) { current = batchedPromoted; continue; } } } var improved = false; foreach (var edgeId in severityByEdgeId .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key)) { var focusEdgeIds = preferFastTerminalOnly ? [edgeId] : ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray(); if (focusEdgeIds.Length == 0) { continue; } if (preferFastTerminalOnly) { var quickCandidateEdges = BuildFastTerminalOnlyHardRuleCandidate( current.Edges, nodes, direction, minLineClearance, focusEdgeIds); if (TryPromoteFinalHardRuleCandidate(current, quickCandidateEdges, nodes, out var quickPromoted)) { current = quickPromoted; improved = true; break; } continue; } var candidateEdges = BuildFinalRestabilizedCandidate( current.Edges, nodes, direction, minLineClearance, focusEdgeIds); if (!TryPromoteFinalHardRuleCandidate(current, candidateEdges, nodes, out var promoted)) { continue; } current = promoted; improved = true; break; } if (!improved) { break; } } return current; } private static bool ShouldPreferFastTerminalOnlyHardRuleClosure(RoutingRetryState retryState) { return retryState.RemainingShortHighways == 0 && retryState.RepeatCollectorCorridorViolations == 0 && retryState.RepeatCollectorNodeClearanceViolations == 0 && retryState.TargetApproachBacktrackingViolations == 0 && retryState.ExcessiveDetourViolations == 0 && retryState.BoundarySlotViolations == 0 && retryState.BelowGraphViolations == 0 && retryState.UnderNodeViolations == 0 && retryState.LongDiagonalViolations == 0 && retryState.SharedLaneViolations <= 2 && (retryState.TargetApproachJoinViolations > 0 || retryState.EntryAngleViolations > 0 || retryState.GatewaySourceExitViolations > 0 || retryState.SharedLaneViolations > 0); } private static ElkRoutedEdge[] BuildFastTerminalOnlyHardRuleCandidate( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection restrictedEdgeIds) { var candidate = edges; candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, restrictedEdgeIds), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.SpreadSourceDepartureJoins( candidate, nodes, minLineClearance, restrictedEdgeIds), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( candidate, nodes, minLineClearance, restrictedEdgeIds), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.SpreadTargetApproachJoins( candidate, nodes, minLineClearance, restrictedEdgeIds, forceOutwardAxisSpacing: true), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes), nodes); candidate = ChoosePreferredHardRuleLayout( candidate, ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, restrictedEdgeIds), nodes); return candidate; } private static bool ShouldPreferCompactFocusedTerminalClosure( RoutingRetryState retryState, int focusEdgeCount) { return focusEdgeCount <= 4 && retryState.BoundarySlotViolations == 0 && retryState.BelowGraphViolations == 0 && retryState.UnderNodeViolations == 0 && retryState.GatewaySourceExitViolations == 0 && retryState.EntryAngleViolations == 0 && retryState.TargetApproachBacktrackingViolations == 0 && retryState.ExcessiveDetourViolations == 0 && retryState.TargetApproachJoinViolations <= 1 && retryState.SharedLaneViolations <= 1 && (retryState.TargetApproachJoinViolations > 0 || retryState.SharedLaneViolations > 0); } private static ElkRoutedEdge[] ApplyCompactFocusedTerminalClosure( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection focusedEdgeIds) { var candidate = edges; candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focusedEdgeIds)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.SpreadSourceDepartureJoins(current, nodes, minLineClearance, focusedEdgeIds)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focusedEdgeIds)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.SpreadTargetApproachJoins( current, nodes, minLineClearance, focusedEdgeIds, forceOutwardAxisSpacing: true)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focusedEdgeIds)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes)); candidate = ApplyGuardedFocusedHardRulePass( candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes)); return candidate; } private static CandidateSolution ApplyWinnerDetourPolish( CandidateSolution solution, ElkPositionedNode[] nodes, double minLineClearance) { var focusSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountExcessiveDetourViolations(solution.Edges, nodes, focusSeverity, 10); ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(solution.Edges, nodes, focusSeverity, 10); if (focusSeverity.Count > 0) { var batchedRootEdgeIds = focusSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Take(Math.Min(focusSeverity.Count, MaxWinnerPolishBatchedRootEdges + 2)) .Select(pair => pair.Key) .ToArray(); var batchedFocusEdgeIds = ExpandWinningSolutionFocus(solution.Edges, batchedRootEdgeIds).ToArray(); if (batchedFocusEdgeIds.Length > 0) { var batchedCandidateEdges = ComposeTransactionalFinalDetourCandidate( solution.Edges, nodes, minLineClearance, batchedFocusEdgeIds); batchedCandidateEdges = ChoosePreferredHardRuleLayout(solution.Edges, batchedCandidateEdges, nodes); if (TryPromoteFinalDetourCandidate( solution.Edges, batchedCandidateEdges, nodes, solution.Score, solution.RetryState, out var batchedPromotedEdges)) { var batchedPromotedScore = ElkEdgeRoutingScoring.ComputeScore(batchedPromotedEdges, nodes); var batchedPromotedRetryState = BuildRetryState( batchedPromotedScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(batchedPromotedEdges, nodes).Count : 0); solution = new CandidateSolution( batchedPromotedScore, batchedPromotedRetryState, batchedPromotedEdges, solution.StrategyIndex); } } } var candidateEdges = ApplyFinalDetourPolish(solution.Edges, nodes, minLineClearance, restrictedEdgeIds: null); if (ReferenceEquals(candidateEdges, solution.Edges)) { return solution; } var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); return new CandidateSolution(candidateScore, candidateRetryState, candidateEdges, solution.StrategyIndex); } internal static ElkRoutedEdge[] BuildFinalBoundarySlotCandidate( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null, bool allowLateRestabilizedClosure = true) { var focusEdgeIds = restrictedEdgeIds?.Count > 0 ? restrictedEdgeIds : edges .Select(edge => edge.Id) .OrderBy(edgeId => edgeId, StringComparer.Ordinal) .ToArray(); ElkLayoutDiagnostics.LogProgress( $"Boundary-slot candidate start: focus={focusEdgeIds.Count} allowLateRestabilizedClosure={allowLateRestabilizedClosure}"); var best = edges; var candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( edges, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after initial snap"); candidate = CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after terminal closure"); if (focusEdgeIds.Count > 0) { candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after face/shared-lane separation"); } candidate = ClampBelowGraphEdges(candidate, nodes, restrictedEdgeIds); candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes); candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes); candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after normalization snap"); candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, restrictedEdgeIds); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after detour closure"); candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after detour snap"); if (restrictedEdgeIds?.Count > 0) { candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds), nodes); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after restricted terminal recheck"); } candidate = ApplyLateBoundarySlotRestabilization( candidate, nodes, minLineClearance, focusEdgeIds); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late restabilization"); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late restabilization snap"); if (focusEdgeIds.Count > 0) { var lateHardRuleCandidate = ApplyAggressiveSharedLaneClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds); lateHardRuleCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( lateHardRuleCandidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); candidate = ChoosePreferredHardRuleLayout(candidate, lateHardRuleCandidate, nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late hard-rule closure"); } if (focusEdgeIds.Count > 0 && (ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes) > 0 || ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidate, nodes) > 0)) { var lateClosureCandidate = ApplyLateFocusedBoundarySlotClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds, restrictedEdgeIds); candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, lateClosureCandidate, nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after focused late closure"); } if (allowLateRestabilizedClosure && focusEdgeIds.Count > 0 && focusEdgeIds.Count <= MaxLateRestabilizedClosureFocusEdges) { var stagedLateRestabilizedCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( edges, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true); stagedLateRestabilizedCandidate = BuildFinalRestabilizedCandidate( stagedLateRestabilizedCandidate, nodes, direction, minLineClearance, restrictedEdgeIds); stagedLateRestabilizedCandidate = ChoosePreferredBoundarySlotRepairLayout( stagedLateRestabilizedCandidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( stagedLateRestabilizedCandidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, stagedLateRestabilizedCandidate, nodes); best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after staged late restabilized closure"); } ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate complete"); return ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes); } internal static ElkRoutedEdge[] BuildFinalRestabilizedCandidate( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { var focusEdgeIds = restrictedEdgeIds?.Count > 0 ? restrictedEdgeIds : edges .Select(edge => edge.Id) .OrderBy(edgeId => edgeId, StringComparer.Ordinal) .ToArray(); var focusEdgeSet = focusEdgeIds.ToHashSet(StringComparer.Ordinal); var candidate = ChoosePreferredHardRuleLayout( edges, CloseRemainingTerminalViolations(edges, nodes, direction, minLineClearance, restrictedEdgeIds), nodes); if (focusEdgeIds.Count > 0) { candidate = ChoosePreferredHardRuleLayout( candidate, ApplyAggressiveSharedLaneClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds), nodes); } candidate = BuildFinalBoundarySlotCandidate( candidate, nodes, direction, minLineClearance, restrictedEdgeIds, allowLateRestabilizedClosure: false); if (focusEdgeIds.Count > 0) { candidate = ChoosePreferredHardRuleLayout( candidate, ApplyAggressiveSharedLaneClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds), nodes); } candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds), nodes); if (focusEdgeIds.Count > 0) { var lateHardRuleCandidate = ApplyAggressiveSharedLaneClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds); lateHardRuleCandidate = CloseRemainingTerminalViolations( lateHardRuleCandidate, nodes, direction, minLineClearance, focusEdgeIds); candidate = ChoosePreferredHardRuleLayout(candidate, lateHardRuleCandidate, nodes); var remainingBadAngles = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidate, nodes, remainingBadAngles, 10); var remainingTerminalFocus = ExpandWinningSolutionFocus( candidate, remainingBadAngles.Keys.Where(focusEdgeSet.Contains)) .Where(focusEdgeSet.Contains) .OrderBy(edgeId => edgeId, StringComparer.Ordinal) .ToArray(); if (remainingTerminalFocus.Length > 0) { var terminalFocus = (IReadOnlyCollection)remainingTerminalFocus; var lateTerminalCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( candidate, nodes, minLineClearance, terminalFocus); lateTerminalCandidate = BuildFinalBoundarySlotCandidate( lateTerminalCandidate, nodes, direction, minLineClearance, terminalFocus, allowLateRestabilizedClosure: false); lateTerminalCandidate = CloseRemainingTerminalViolations( lateTerminalCandidate, nodes, direction, minLineClearance, terminalFocus); candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, lateTerminalCandidate, nodes); } } candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, restrictedEdgeIds), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); candidate = ApplyLateBoundarySlotRestabilization( candidate, nodes, minLineClearance, focusEdgeIds); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); if (focusEdgeIds.Count > 0) { var finalSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalSharedLaneCandidate, nodes); var finalSourceJoinCandidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins( finalSharedLaneCandidate, nodes, minLineClearance, focusEdgeIds); candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalSourceJoinCandidate, nodes); var finalTargetJoinCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( finalSourceJoinCandidate, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalTargetJoinCandidate, nodes); var finalAggressiveCandidate = ApplyAggressiveSharedLaneClosure( candidate, nodes, direction, minLineClearance, focusEdgeIds); candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalAggressiveCandidate, nodes); var forcedFinalSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); var baselineSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidate, nodes); var forcedSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(forcedFinalSharedLaneCandidate, nodes); if (forcedSharedLaneViolations < baselineSharedLaneViolations) { var baselineScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes); var forcedScore = ElkEdgeRoutingScoring.ComputeScore(forcedFinalSharedLaneCandidate, nodes); if (forcedScore.NodeCrossings <= baselineScore.NodeCrossings) { candidate = forcedFinalSharedLaneCandidate; } } } return candidate; } private static ElkRoutedEdge[] ApplyLateBoundarySlotRestabilization( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection focusEdgeIds) { if (focusEdgeIds.Count == 0) { return edges; } var candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts( edges, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after mixed-face separation"); candidate = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after collector separation"); candidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after source-join spread"); candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after shared-lane separation"); candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after boundary/target repair"); candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after target-join spread"); candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after feeder-band spread"); candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after gateway finalize"); candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes); candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes); candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, focusEdgeIds, enforceAllNodeEndpoints: true); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after first normalization snap"); candidate = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second collector separation"); candidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second source-join spread"); candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second mixed-face separation"); candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second shared-lane separation"); candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second boundary/target repair"); candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second target-join spread"); candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second feeder-band spread"); candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second gateway finalize"); candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, focusEdgeIds); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after detour closure"); candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes); candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes); candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, focusEdgeIds, enforceAllNodeEndpoints: true); ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization complete"); return ChoosePreferredBoundarySlotRepairLayout(edges, candidate, nodes); } private static ElkRoutedEdge[] ApplyLateFocusedBoundarySlotClosure( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection focusEdgeIds, IReadOnlyCollection? restrictedEdgeIds) { var candidate = ChoosePreferredHardRuleLayout( edges, ApplyAggressiveSharedLaneClosure(edges, nodes, direction, minLineClearance, focusEdgeIds), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds), nodes); candidate = ApplyLateBoundarySlotRestabilization(candidate, nodes, minLineClearance, focusEdgeIds); candidate = ChoosePreferredBoundarySlotRepairLayout( candidate, ElkEdgePostProcessor.SnapBoundarySlotAssignments( candidate, nodes, minLineClearance, restrictedEdgeIds, enforceAllNodeEndpoints: true), nodes); return candidate; } private static bool TryPromoteFinalBoundarySlotCandidate( CandidateSolution current, ElkRoutedEdge[] candidateEdges, ElkPositionedNode[] nodes, out CandidateSolution promoted) { promoted = current; var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); if (!IsBetterBoundarySlotRepairCandidate( candidateScore, candidateRetryState, current.Score, current.RetryState)) { return false; } promoted = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; return true; } private static bool HasBlockingBoundarySlotPromotionRegression( RoutingRetryState candidate, RoutingRetryState baseline) { return candidate.RemainingShortHighways > baseline.RemainingShortHighways || candidate.RepeatCollectorCorridorViolations > baseline.RepeatCollectorCorridorViolations || candidate.RepeatCollectorNodeClearanceViolations > baseline.RepeatCollectorNodeClearanceViolations || candidate.BelowGraphViolations > baseline.BelowGraphViolations || candidate.UnderNodeViolations > baseline.UnderNodeViolations; } private static bool HasEdgeGeometryChanged( IReadOnlyList baselineEdges, IReadOnlyList candidateEdges, string edgeId) { var baseline = baselineEdges.FirstOrDefault(edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal)); var candidate = candidateEdges.FirstOrDefault(edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal)); if (baseline is null || candidate is null) { return false; } var baselinePath = ExtractEdgePath(baseline); var candidatePath = ExtractEdgePath(candidate); if (baselinePath.Count != candidatePath.Count) { return true; } for (var i = 0; i < baselinePath.Count; i++) { if (!ElkEdgeRoutingGeometry.PointsEqual(baselinePath[i], candidatePath[i])) { return true; } } return false; } private static List ExtractEdgePath(ElkRoutedEdge edge) { var path = new List(); foreach (var section in edge.Sections) { if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], section.StartPoint)) { path.Add(section.StartPoint); } foreach (var bendPoint in section.BendPoints) { if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], bendPoint)) { path.Add(bendPoint); } } if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], section.EndPoint)) { path.Add(section.EndPoint); } } return path; } private static bool TryPromoteFinalHardRuleCandidate( CandidateSolution current, ElkRoutedEdge[] candidateEdges, ElkPositionedNode[] nodes, out CandidateSolution promoted) { promoted = current; var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); var candidateRetryState = BuildRetryState( candidateScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count : 0); if (!IsBetterBoundarySlotRepairCandidate( candidateScore, candidateRetryState, current.Score, current.RetryState)) { return false; } promoted = current with { Score = candidateScore, RetryState = candidateRetryState, Edges = candidateEdges, }; return true; } private static ElkRoutedEdge[] ApplyAggressiveSharedLaneClosure( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, IReadOnlyCollection focusEdgeIds) { var result = edges; result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(result, nodes, minLineClearance, focusEdgeIds); result = ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes, focusEdgeIds); result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes, focusEdgeIds); result = ClampBelowGraphEdges(result, nodes, focusEdgeIds); result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, focusEdgeIds); result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes); result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes); result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusEdgeIds); result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusEdgeIds); return result; } }