using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static RepairPlan? BuildRepairPlan( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, EdgeRoutingScore score, RoutingRetryState retryState, RoutingStrategy strategy, int attempt) { if (edges.Length == 0) { return null; } var severityByEdgeId = new Dictionary(StringComparer.Ordinal); var preferredShortestEdgeIds = new HashSet(StringComparer.Ordinal); var routeRepairEdgeIds = new HashSet(StringComparer.Ordinal); var mandatoryEdgeIds = new HashSet(StringComparer.Ordinal); var severityByReason = new Dictionary>(StringComparer.Ordinal); var reasons = new List(); var prioritizeBlockingAndLengthOnly = retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry; void AddReason(string reason) { if (!reasons.Contains(reason, StringComparer.Ordinal)) { reasons.Add(reason); } } void AddEdgeIds( IEnumerable edgeIds, int severity, string reason, bool requiresRouteRepair = false, bool mandatoryRepair = false) { AddReason(reason); if (!severityByReason.TryGetValue(reason, out var reasonSeverity)) { reasonSeverity = new Dictionary(StringComparer.Ordinal); severityByReason[reason] = reasonSeverity; } foreach (var edgeId in edgeIds) { severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity; reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity; if (requiresRouteRepair) { routeRepairEdgeIds.Add(edgeId); } if (mandatoryRepair) { mandatoryEdgeIds.Add(edgeId); } } } void MergeSeverity( Dictionary metricSeverity, string reason, bool preferShortestRepair = false, bool requiresRouteRepair = false, bool mandatoryRepair = false) { if (metricSeverity.Count == 0) { return; } AddReason(reason); if (!severityByReason.TryGetValue(reason, out var reasonSeverity)) { reasonSeverity = new Dictionary(StringComparer.Ordinal); severityByReason[reason] = reasonSeverity; } foreach (var (edgeId, severity) in metricSeverity) { severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity; reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity; if (preferShortestRepair || requiresRouteRepair) { routeRepairEdgeIds.Add(edgeId); } if (mandatoryRepair) { mandatoryEdgeIds.Add(edgeId); } if (preferShortestRepair) { preferredShortestEdgeIds.Add(edgeId); } } } if (attempt == 1 && retryState.RequiresLengthRetry && !retryState.RequiresBlockingRetry) { if (retryState.TargetApproachBacktrackingViolations > 0) { var backtrackingSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 2_250); MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, mandatoryRepair: true); } if (retryState.ExcessiveDetourViolations > 0) { var detourSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 2_000); MergeSeverity(detourSeverity, "detour-priority", preferShortestRepair: true, mandatoryRepair: true); } } else { if (score.NodeCrossings > 0) { var nodeCrossingSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountEdgeNodeCrossings(edges, nodes, nodeCrossingSeverity, 2_500); MergeSeverity(nodeCrossingSeverity, "node-crossings", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.RemainingShortHighways > 0) { var brokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(edges, nodes); AddEdgeIds( brokenHighways.SelectMany(highway => highway.EdgeIds).Distinct(StringComparer.Ordinal), 2_000, "short-highways", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.RepeatCollectorCorridorViolations > 0) { var collectorSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountRepeatCollectorCorridorViolations(edges, nodes, collectorSeverity, 2_000); MergeSeverity(collectorSeverity, "collector-corridors", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.RepeatCollectorNodeClearanceViolations > 0) { var collectorClearanceSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountRepeatCollectorNodeClearanceViolations(edges, nodes, collectorClearanceSeverity, 2_000); MergeSeverity(collectorClearanceSeverity, "collector-clearance", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.TargetApproachJoinViolations > 0) { var targetJoinSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, targetJoinSeverity, 1_500); MergeSeverity(targetJoinSeverity, "target-joins", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.TargetApproachBacktrackingViolations > 0) { var backtrackingSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 1_600); MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.BelowGraphViolations > 0) { var belowGraphSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBelowGraphViolations(edges, nodes, belowGraphSeverity, 2_500); MergeSeverity(belowGraphSeverity, "below-graph", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.UnderNodeViolations > 0) { var underNodeSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes, underNodeSeverity, 2_500); MergeSeverity(underNodeSeverity, "under-node", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.LongDiagonalViolations > 0) { var longDiagonalSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountLongDiagonalViolations(edges, nodes, longDiagonalSeverity, 2_250); MergeSeverity(longDiagonalSeverity, "long-diagonal", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.ExcessiveDetourViolations > 0) { var detourSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 1_250); MergeSeverity(detourSeverity, "detour", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.SharedLaneViolations > 0) { var sharedLaneSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountSharedLaneViolations(edges, nodes, sharedLaneSeverity, 2_000); MergeSeverity(sharedLaneSeverity, "shared-lanes", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.BoundarySlotViolations > 0) { var boundarySlotSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, boundarySlotSeverity, 2_000); MergeSeverity(boundarySlotSeverity, "boundary-slots", requiresRouteRepair: true, mandatoryRepair: true); } if (!prioritizeBlockingAndLengthOnly && retryState.ProximityViolations > 0) { var proximitySeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountProximityViolations(edges, nodes, proximitySeverity, 350); MergeSeverity(proximitySeverity, "proximity", requiresRouteRepair: true); } if (retryState.EntryAngleViolations > 0) { var entrySeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes, entrySeverity, 450); MergeSeverity(entrySeverity, "entry", requiresRouteRepair: true, mandatoryRepair: true); } if (retryState.GatewaySourceExitViolations > 0) { var gatewaySourceSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes, gatewaySourceSeverity, 2_500); MergeSeverity(gatewaySourceSeverity, "gateway-source-exit", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true); } if (!prioritizeBlockingAndLengthOnly && retryState.LabelProximityViolations > 0) { var labelSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountLabelProximityViolations(edges, nodes, labelSeverity, 300); MergeSeverity(labelSeverity, "label", requiresRouteRepair: true); } if (!prioritizeBlockingAndLengthOnly && retryState.EdgeCrossings > 0) { var edgeCrossingSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountEdgeEdgeCrossings(edges, edgeCrossingSeverity, 200); MergeSeverity(edgeCrossingSeverity, "edge-crossings", requiresRouteRepair: true); } } if (severityByEdgeId.Count == 0) { return null; } var orderRankByEdgeId = strategy.EdgeOrder .Select((edgeIndex, rank) => new { edgeIndex, rank }) .Where(item => item.edgeIndex >= 0 && item.edgeIndex < edges.Length) .ToDictionary(item => edges[item.edgeIndex].Id, item => item.rank, StringComparer.Ordinal); var seedSelectedEdgeIds = new List(); foreach (var reason in reasons) { if (!severityByReason.TryGetValue(reason, out var reasonSeverity) || reasonSeverity.Count == 0) { continue; } var picksForReason = reason is "detour" or "detour-priority" or "approach-backtracking" ? 2 : 1; var rankedReasonEdgeIds = reasonSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue)) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key) .ToArray(); foreach (var edgeId in RotateOrderedEdgeIds(rankedReasonEdgeIds, attempt)) { if (seedSelectedEdgeIds.Contains(edgeId, StringComparer.Ordinal)) { continue; } seedSelectedEdgeIds.Add(edgeId); if (--picksForReason == 0) { break; } } } var maxEdgeRepairs = DetermineRepairEdgeBudget( retryState, attempt == 1 && retryState.RequiresLengthRetry, seedSelectedEdgeIds.Count, mandatoryEdgeIds.Count); var orderedEdgeIds = severityByEdgeId .OrderByDescending(pair => pair.Value) .ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue)) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => pair.Key) .ToArray(); var prioritizedShortestEdgeIds = orderedEdgeIds .Where(preferredShortestEdgeIds.Contains) .Take(Math.Min(Math.Max(2, seedSelectedEdgeIds.Count), maxEdgeRepairs)) .ToArray(); var orderedMandatoryEdgeIds = orderedEdgeIds .Where(mandatoryEdgeIds.Contains) .ToArray(); var mandatoryFocusBudget = DetermineMandatoryFocusBudget( orderedMandatoryEdgeIds.Length, maxEdgeRepairs, seedSelectedEdgeIds.Count); var focusedMandatoryEdgeIds = RotateOrderedEdgeIds(orderedMandatoryEdgeIds, attempt) .Take(mandatoryFocusBudget) .ToArray(); var effectiveEdgeRepairBudget = Math.Max(maxEdgeRepairs, focusedMandatoryEdgeIds.Length); var selectedEdgeIds = focusedMandatoryEdgeIds .Concat(seedSelectedEdgeIds) .Concat(prioritizedShortestEdgeIds) .Distinct(StringComparer.Ordinal) .Take(effectiveEdgeRepairBudget) .ToArray(); if (reasons.Contains("collector-corridors", StringComparer.Ordinal)) { selectedEdgeIds = ExpandRepeatCollectorRepairSet(selectedEdgeIds, edges, nodes); } if (reasons.Contains("target-joins", StringComparer.Ordinal)) { selectedEdgeIds = ExpandTargetApproachJoinRepairSet(selectedEdgeIds, edges, nodes, strategy.MinLineClearance); } if (reasons.Contains("under-node", StringComparer.Ordinal)) { selectedEdgeIds = ExpandUnderNodeRepairSet(selectedEdgeIds, edges, nodes); } if (reasons.Contains("shared-lanes", StringComparer.Ordinal)) { selectedEdgeIds = ExpandSharedLaneRepairSet(selectedEdgeIds, edges, nodes); } if (selectedEdgeIds.Length == 0) { return null; } var preferredSelectedEdgeIds = selectedEdgeIds .Where(preferredShortestEdgeIds.Contains) .ToArray(); var routeRepairSelectedEdgeIds = selectedEdgeIds .Where(routeRepairEdgeIds.Contains) .ToArray(); var edgeIndices = selectedEdgeIds .Select(edgeId => Array.FindIndex(edges, edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal))) .Where(edgeIndex => edgeIndex >= 0) .ToArray(); if (edgeIndices.Length == 0) { return null; } return new RepairPlan( edgeIndices, selectedEdgeIds, preferredSelectedEdgeIds, routeRepairSelectedEdgeIds, reasons.ToArray()); } }