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

1665 lines
71 KiB
C#

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<string, int>(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<string> ExpandWinningSolutionFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IEnumerable<string> focusEdgeIds)
{
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var expanded = new HashSet<string>(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<string> ExpandSharedLanePolishFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
string focusEdgeId)
{
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
if (!edgesById.TryGetValue(focusEdgeId, out var focusEdge))
{
return [];
}
var focusedEdgeIds = new HashSet<string>(StringComparer.Ordinal)
{
focusEdgeId,
};
var sharedNodeIds = new HashSet<string>(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<string> 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<string, int>(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<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10);
var focusSeverity = new Dictionary<string, int>(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<string, int>(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<string, int>(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<string, int>(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<string> 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<string> 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<string, int>(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<string>? 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<string>? 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<string, int>(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<string>)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<string> 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<string> focusEdgeIds,
IReadOnlyCollection<string>? 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<ElkRoutedEdge> baselineEdges,
IReadOnlyList<ElkRoutedEdge> 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<ElkPoint> ExtractEdgePath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
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<string> 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;
}
}