elksharp: stabilize document-processing terminal routing
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -171,7 +171,14 @@ internal static partial class ElkEdgeRouterIterative
|
||||
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
||||
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
||||
{
|
||||
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]))
|
||||
var suppressForkBypassGatewayChecks = ElkEdgeRoutingScoring.ShouldSuppressForkBypassGatewaySourceExitChecks(
|
||||
edge,
|
||||
path,
|
||||
edges,
|
||||
nodesById,
|
||||
sourceNode);
|
||||
|
||||
if (ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(path, sourceNode))
|
||||
{
|
||||
sourceVertexExits++;
|
||||
focus.Add(edge.Id);
|
||||
@@ -189,7 +196,8 @@ internal static partial class ElkEdgeRouterIterative
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewaySourcePreferredFaceMismatchArtifact(
|
||||
if (!suppressForkBypassGatewayChecks
|
||||
&& HasGatewaySourcePreferredFaceMismatchArtifact(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
@@ -200,7 +208,8 @@ internal static partial class ElkEdgeRouterIterative
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewaySourceDominantAxisDetourArtifact(
|
||||
if (!suppressForkBypassGatewayChecks
|
||||
&& HasGatewaySourceDominantAxisDetourArtifact(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
@@ -211,7 +220,8 @@ internal static partial class ElkEdgeRouterIterative
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (ElkEdgePostProcessor.HasClearGatewaySourceScoringOpportunity(
|
||||
if (!suppressForkBypassGatewayChecks
|
||||
&& ElkEdgePostProcessor.HasClearGatewaySourceScoringOpportunity(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
|
||||
@@ -32,7 +32,9 @@ internal static partial class ElkEdgeRoutingScoring
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, firstSection.StartPoint))
|
||||
if (!ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(
|
||||
[firstSection.StartPoint, firstSection.BendPoints.FirstOrDefault() ?? firstSection.EndPoint],
|
||||
sourceNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -130,14 +132,24 @@ internal static partial class ElkEdgeRoutingScoring
|
||||
sourceSideCounts,
|
||||
boundarySlotSeverityByEdgeId);
|
||||
var suppressSoftGatewayChecks = allowsSaturatedAlternateFace || isResolvedDiscreteSlotExit;
|
||||
var suppressForkBypassGatewayChecks = ShouldSuppressForkBypassGatewaySourceExitChecks(
|
||||
edge,
|
||||
path,
|
||||
edges,
|
||||
nodesById,
|
||||
sourceNode);
|
||||
var suppressStableOpportunity = suppressSoftGatewayChecks || suppressForkBypassGatewayChecks;
|
||||
var hasViolation = HasGatewaySourceExitBacktracking(path)
|
||||
|| (!suppressSoftGatewayChecks
|
||||
&& !suppressForkBypassGatewayChecks
|
||||
&& HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
||||
|| (!suppressSoftGatewayChecks
|
||||
&& ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]))
|
||||
&& !suppressForkBypassGatewayChecks
|
||||
&& ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(path, sourceNode))
|
||||
|| (!suppressSoftGatewayChecks
|
||||
&& !suppressForkBypassGatewayChecks
|
||||
&& HasGatewaySourcePreferredFaceMismatch(path, sourceNode))
|
||||
|| (!suppressSoftGatewayChecks
|
||||
|| (!suppressStableOpportunity
|
||||
&& currentBoundarySlotViolations == 0
|
||||
&& currentBadBoundaryAngles == 0
|
||||
&& HasGraphStableGatewaySourceOpportunity(
|
||||
@@ -163,6 +175,145 @@ internal static partial class ElkEdgeRoutingScoring
|
||||
return count;
|
||||
}
|
||||
|
||||
internal static bool ShouldSuppressForkBypassGatewaySourceExitChecks(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
IReadOnlyCollection<ElkRoutedEdge> edges,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
if (!string.Equals(sourceNode.Kind, "Fork", StringComparison.Ordinal)
|
||||
|| path.Count < 2
|
||||
|| !HasCleanOrthogonalGatewayDeparture(path, sourceNode)
|
||||
|| HasGatewaySourceExitBacktracking(path)
|
||||
|| ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(path, sourceNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
||||
|| !string.Equals(targetNode.Kind, "Join", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var desiredSide = ResolvePrimaryGatewayDirection(sourceNode, targetNode);
|
||||
if (desiredSide is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode);
|
||||
if (string.Equals(currentSide, desiredSide, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsOrthogonalGatewaySideTransition(currentSide, desiredSide))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return edges.Any(peer =>
|
||||
!string.Equals(peer.Id, edge.Id, StringComparison.Ordinal)
|
||||
&& string.Equals(peer.SourceNodeId, edge.SourceNodeId, StringComparison.Ordinal)
|
||||
&& DoesPeerOwnForkPrimaryAxis(peer, nodesById, sourceNode, desiredSide));
|
||||
}
|
||||
|
||||
private static bool DoesPeerOwnForkPrimaryAxis(
|
||||
ElkRoutedEdge peer,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
||||
ElkPositionedNode sourceNode,
|
||||
string desiredSide)
|
||||
{
|
||||
var peerPath = ExtractPath(peer);
|
||||
if (peerPath.Count < 2
|
||||
|| !HasCleanOrthogonalGatewayDeparture(peerPath, sourceNode)
|
||||
|| HasGatewaySourceExitBacktracking(peerPath)
|
||||
|| ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(peerPath, sourceNode)
|
||||
|| !string.Equals(
|
||||
ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(peerPath[0], peerPath[1], sourceNode),
|
||||
desiredSide,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!nodesById.TryGetValue(peer.TargetNodeId ?? string.Empty, out var peerTargetNode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !string.Equals(peerTargetNode.Kind, "Join", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string? ResolvePrimaryGatewayDirection(
|
||||
ElkPositionedNode sourceNode,
|
||||
ElkPositionedNode targetNode)
|
||||
{
|
||||
var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d);
|
||||
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
|
||||
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
||||
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
||||
var dx = targetCenterX - sourceCenterX;
|
||||
var dy = targetCenterY - sourceCenterY;
|
||||
|
||||
if (Math.Abs(dx) >= Math.Abs(dy) * 1.15d && Math.Sign(dx) != 0)
|
||||
{
|
||||
return dx > 0d ? "right" : "left";
|
||||
}
|
||||
|
||||
if (Math.Abs(dy) >= Math.Abs(dx) * 1.15d && Math.Sign(dy) != 0)
|
||||
{
|
||||
return dy > 0d ? "bottom" : "top";
|
||||
}
|
||||
|
||||
if (Math.Sign(dx) != 0)
|
||||
{
|
||||
return dx > 0d ? "right" : "left";
|
||||
}
|
||||
|
||||
return Math.Sign(dy) == 0
|
||||
? null
|
||||
: (dy > 0d ? "bottom" : "top");
|
||||
}
|
||||
|
||||
private static bool HasCleanOrthogonalGatewayDeparture(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double tolerance = 0.5d;
|
||||
var boundary = path[0];
|
||||
var adjacent = path[1];
|
||||
var side = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(boundary, adjacent, sourceNode);
|
||||
return side switch
|
||||
{
|
||||
"left" => adjacent.X < boundary.X - tolerance && Math.Abs(adjacent.Y - boundary.Y) <= tolerance,
|
||||
"right" => adjacent.X > boundary.X + tolerance && Math.Abs(adjacent.Y - boundary.Y) <= tolerance,
|
||||
"top" => adjacent.Y < boundary.Y - tolerance && Math.Abs(adjacent.X - boundary.X) <= tolerance,
|
||||
"bottom" => adjacent.Y > boundary.Y + tolerance && Math.Abs(adjacent.X - boundary.X) <= tolerance,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsOrthogonalGatewaySideTransition(string? currentSide, string? desiredSide)
|
||||
{
|
||||
if (currentSide is not ("left" or "right" or "top" or "bottom")
|
||||
|| desiredSide is not ("left" or "right" or "top" or "bottom"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIsHorizontal = currentSide is "left" or "right";
|
||||
var desiredIsHorizontal = desiredSide is "left" or "right";
|
||||
return currentIsHorizontal != desiredIsHorizontal;
|
||||
}
|
||||
|
||||
private static bool IsResolvedGatewaySourceSlotExit(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
|
||||
428
src/__Libraries/StellaOps.ElkSharp/ElkTopCorridorOwnership.cs
Normal file
428
src/__Libraries/StellaOps.ElkSharp/ElkTopCorridorOwnership.cs
Normal file
@@ -0,0 +1,428 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkTopCorridorOwnership
|
||||
{
|
||||
private const double CoordinateTolerance = 0.5d;
|
||||
|
||||
internal static ElkRoutedEdge[] SpreadAboveGraphCorridorLanes(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance)
|
||||
{
|
||||
if (edges.Length < 2 || nodes.Length == 0)
|
||||
{
|
||||
return edges;
|
||||
}
|
||||
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var result = edges.ToArray();
|
||||
var changed = false;
|
||||
|
||||
var candidates = result
|
||||
.Select((edge, index) => CreateCandidate(edge, index, nodesById, graphMinY))
|
||||
.Where(candidate => candidate is not null)
|
||||
.Select(candidate => candidate!.Value)
|
||||
.OrderBy(candidate => candidate.MinX)
|
||||
.ThenBy(candidate => candidate.MaxX)
|
||||
.ToArray();
|
||||
if (candidates.Length < 2)
|
||||
{
|
||||
return edges;
|
||||
}
|
||||
|
||||
foreach (var cluster in BuildOverlapClusters(candidates))
|
||||
{
|
||||
if (cluster.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var baselineMetrics = MeasureClusterMetrics(cluster, result, nodes, nodesById, graphMinY);
|
||||
var candidateEdges = RewriteCluster(result, cluster, graphMinY, minLineClearance);
|
||||
if (ReferenceEquals(candidateEdges, result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateMetrics = MeasureClusterMetrics(cluster, candidateEdges, nodes, nodesById, graphMinY);
|
||||
if (!candidateMetrics.IsBetterThan(baselineMetrics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result = candidateEdges;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed ? result : edges;
|
||||
}
|
||||
|
||||
private static double ResolveLaneStep(
|
||||
AboveGraphCorridorCandidate? previousAssignedLane,
|
||||
AboveGraphCorridorCandidate currentLane,
|
||||
double minLineClearance)
|
||||
{
|
||||
var compactGap = Math.Max(14d, (minLineClearance * 0.55d) + 4d);
|
||||
if (previousAssignedLane is null)
|
||||
{
|
||||
return compactGap;
|
||||
}
|
||||
|
||||
return previousAssignedLane.Value.Priority == currentLane.Priority
|
||||
? compactGap
|
||||
: Math.Max(compactGap + 4d, Math.Min(26d, minLineClearance + 2d));
|
||||
}
|
||||
|
||||
private static List<AboveGraphCorridorCandidate[]> BuildOverlapClusters(
|
||||
IReadOnlyList<AboveGraphCorridorCandidate> candidates)
|
||||
{
|
||||
var clusters = new List<AboveGraphCorridorCandidate[]>();
|
||||
var current = new List<AboveGraphCorridorCandidate>();
|
||||
var currentMaxX = double.NegativeInfinity;
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (current.Count == 0
|
||||
|| candidate.MinX <= currentMaxX + CoordinateTolerance)
|
||||
{
|
||||
current.Add(candidate);
|
||||
currentMaxX = Math.Max(currentMaxX, candidate.MaxX);
|
||||
continue;
|
||||
}
|
||||
|
||||
clusters.Add(current.ToArray());
|
||||
current =
|
||||
[
|
||||
candidate,
|
||||
];
|
||||
currentMaxX = candidate.MaxX;
|
||||
}
|
||||
|
||||
if (current.Count > 0)
|
||||
{
|
||||
clusters.Add(current.ToArray());
|
||||
}
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
private static AboveGraphCorridorCandidate? CreateCandidate(
|
||||
ElkRoutedEdge edge,
|
||||
int index,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
||||
double graphMinY)
|
||||
{
|
||||
var path = ExtractPath(edge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AboveGraphCorridorCandidate? best = null;
|
||||
for (var i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
var start = path[i];
|
||||
var end = path[i + 1];
|
||||
if (Math.Abs(start.Y - end.Y) > CoordinateTolerance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var corridorY = (start.Y + end.Y) / 2d;
|
||||
if (corridorY >= graphMinY - 8d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = Math.Abs(end.X - start.X);
|
||||
if (length <= 1d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var priority = ResolvePriority(edge, nodesById);
|
||||
var ownershipKey = ResolveOwnershipKey(edge, nodesById);
|
||||
var candidate = new AboveGraphCorridorCandidate(
|
||||
index,
|
||||
edge.Id,
|
||||
corridorY,
|
||||
Math.Min(start.X, end.X),
|
||||
Math.Max(start.X, end.X),
|
||||
length,
|
||||
priority,
|
||||
ownershipKey);
|
||||
if (best is null || candidate.Length > best.Value.Length)
|
||||
{
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int ResolvePriority(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
|
||||
{
|
||||
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(edge.TargetNodeId)
|
||||
&& nodesById.TryGetValue(edge.TargetNodeId, out var targetNode)
|
||||
&& string.Equals(targetNode.Kind, "End", StringComparison.Ordinal))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static string ResolveOwnershipKey(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(edge.TargetNodeId)
|
||||
&& nodesById.TryGetValue(edge.TargetNodeId, out var targetNode)
|
||||
&& string.Equals(targetNode.Kind, "End", StringComparison.Ordinal))
|
||||
{
|
||||
return $"end:{targetNode.Id}";
|
||||
}
|
||||
|
||||
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
||||
{
|
||||
return $"repeat:{edge.TargetNodeId ?? edge.Id}";
|
||||
}
|
||||
|
||||
return edge.Id;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] RewriteCluster(
|
||||
IReadOnlyList<ElkRoutedEdge> edges,
|
||||
IReadOnlyList<AboveGraphCorridorCandidate> cluster,
|
||||
double graphMinY,
|
||||
double minLineClearance)
|
||||
{
|
||||
var groups = cluster
|
||||
.GroupBy(candidate => candidate.OwnershipKey, StringComparer.Ordinal)
|
||||
.Select(group =>
|
||||
{
|
||||
var orderedMembers = group
|
||||
.OrderBy(member => member.Index)
|
||||
.ToArray();
|
||||
var representative = orderedMembers
|
||||
.OrderByDescending(member => member.Length)
|
||||
.ThenByDescending(member => member.CorridorY)
|
||||
.ThenBy(member => member.EdgeId, StringComparer.Ordinal)
|
||||
.First();
|
||||
return new AboveGraphOwnershipGroup(
|
||||
OwnershipKey: group.Key,
|
||||
Members: orderedMembers,
|
||||
Representative: representative,
|
||||
Priority: orderedMembers.Min(member => member.Priority),
|
||||
PreferredLaneY: orderedMembers.Max(member => member.CorridorY),
|
||||
Span: orderedMembers.Max(member => member.MaxX) - orderedMembers.Min(member => member.MinX));
|
||||
})
|
||||
.OrderBy(group => group.Priority)
|
||||
.ThenByDescending(group => group.PreferredLaneY)
|
||||
.ThenByDescending(group => group.Span)
|
||||
.ThenBy(group => group.OwnershipKey, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (groups.Length < 2 && groups[0].Members.All(member => Math.Abs(member.CorridorY - groups[0].PreferredLaneY) <= CoordinateTolerance))
|
||||
{
|
||||
return edges as ElkRoutedEdge[] ?? edges.ToArray();
|
||||
}
|
||||
|
||||
var result = edges.ToArray();
|
||||
var anchorY = groups.Max(group => group.PreferredLaneY);
|
||||
AboveGraphCorridorCandidate? previousAssignedLane = null;
|
||||
var assignedY = anchorY;
|
||||
var changed = false;
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (previousAssignedLane is not null)
|
||||
{
|
||||
assignedY -= ResolveLaneStep(previousAssignedLane.Value, group.Representative, minLineClearance);
|
||||
}
|
||||
|
||||
foreach (var member in group.Members)
|
||||
{
|
||||
var rewritten = RewriteCorridorLane(result[member.Index], member.CorridorY, assignedY, graphMinY);
|
||||
if (!ReferenceEquals(rewritten, result[member.Index]))
|
||||
{
|
||||
result[member.Index] = rewritten;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
previousAssignedLane = group.Representative;
|
||||
}
|
||||
|
||||
return changed ? result : (edges as ElkRoutedEdge[] ?? edges.ToArray());
|
||||
}
|
||||
|
||||
private static CorridorClusterMetrics MeasureClusterMetrics(
|
||||
IReadOnlyList<AboveGraphCorridorCandidate> cluster,
|
||||
IReadOnlyList<ElkRoutedEdge> edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
||||
double graphMinY)
|
||||
{
|
||||
var liveCandidates = cluster
|
||||
.Select(member => CreateCandidate(edges[member.Index], member.Index, nodesById, graphMinY))
|
||||
.Where(candidate => candidate is not null)
|
||||
.Select(candidate => candidate!.Value)
|
||||
.ToArray();
|
||||
var ownershipSplits = liveCandidates
|
||||
.GroupBy(candidate => candidate.OwnershipKey, StringComparer.Ordinal)
|
||||
.Count(group =>
|
||||
group.Select(candidate => Math.Round(candidate.CorridorY, 1))
|
||||
.Distinct()
|
||||
.Count() > 1);
|
||||
var orderedOwnershipLanes = liveCandidates
|
||||
.GroupBy(candidate => candidate.OwnershipKey, StringComparer.Ordinal)
|
||||
.Select(group => new
|
||||
{
|
||||
Priority = group.Min(candidate => candidate.Priority),
|
||||
LaneY = group.Max(candidate => candidate.CorridorY),
|
||||
})
|
||||
.OrderBy(entry => entry.Priority)
|
||||
.ThenByDescending(entry => entry.LaneY)
|
||||
.ToArray();
|
||||
var priorityInversions = 0;
|
||||
for (var i = 1; i < orderedOwnershipLanes.Length; i++)
|
||||
{
|
||||
if (orderedOwnershipLanes[i - 1].Priority == orderedOwnershipLanes[i].Priority)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (orderedOwnershipLanes[i - 1].LaneY + CoordinateTolerance < orderedOwnershipLanes[i].LaneY)
|
||||
{
|
||||
priorityInversions++;
|
||||
}
|
||||
}
|
||||
|
||||
var clusterEdgeIds = cluster
|
||||
.Select(candidate => candidate.EdgeId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var brokenTopHighways = ElkEdgeRouterHighway
|
||||
.DetectRemainingBrokenHighways(edges as ElkRoutedEdge[] ?? edges.ToArray(), nodes)
|
||||
.Count(diagnostic =>
|
||||
diagnostic.WasBroken
|
||||
&& string.Equals(diagnostic.SharedAxis, "top", StringComparison.Ordinal)
|
||||
&& diagnostic.EdgeIds.Any(clusterEdgeIds.Contains));
|
||||
var laneSpan = liveCandidates.Length == 0
|
||||
? 0d
|
||||
: liveCandidates.Max(candidate => candidate.CorridorY) - liveCandidates.Min(candidate => candidate.CorridorY);
|
||||
|
||||
return new CorridorClusterMetrics(
|
||||
BrokenTopHighways: brokenTopHighways,
|
||||
OwnershipSplits: ownershipSplits,
|
||||
PriorityInversions: priorityInversions,
|
||||
LaneSpan: laneSpan);
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge RewriteCorridorLane(
|
||||
ElkRoutedEdge edge,
|
||||
double currentY,
|
||||
double assignedY,
|
||||
double graphMinY)
|
||||
{
|
||||
if (Math.Abs(currentY - assignedY) <= CoordinateTolerance)
|
||||
{
|
||||
return edge;
|
||||
}
|
||||
|
||||
bool ShouldShift(ElkPoint point) =>
|
||||
Math.Abs(point.Y - currentY) <= CoordinateTolerance
|
||||
&& point.Y < graphMinY - 8d;
|
||||
|
||||
ElkPoint Shift(ElkPoint point) => ShouldShift(point)
|
||||
? new ElkPoint { X = point.X, Y = assignedY }
|
||||
: point;
|
||||
|
||||
return new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
SourcePortId = edge.SourcePortId,
|
||||
TargetPortId = edge.TargetPortId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections = edge.Sections
|
||||
.Select(section => new ElkEdgeSection
|
||||
{
|
||||
StartPoint = Shift(section.StartPoint),
|
||||
EndPoint = Shift(section.EndPoint),
|
||||
BendPoints = section.BendPoints.Select(Shift).ToArray(),
|
||||
})
|
||||
.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ElkPoint> ExtractPath(ElkRoutedEdge edge)
|
||||
{
|
||||
var path = new List<ElkPoint>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
{
|
||||
path.Add(section.StartPoint);
|
||||
}
|
||||
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private readonly record struct AboveGraphCorridorCandidate(
|
||||
int Index,
|
||||
string EdgeId,
|
||||
double CorridorY,
|
||||
double MinX,
|
||||
double MaxX,
|
||||
double Length,
|
||||
int Priority,
|
||||
string OwnershipKey);
|
||||
|
||||
private readonly record struct AboveGraphOwnershipGroup(
|
||||
string OwnershipKey,
|
||||
AboveGraphCorridorCandidate[] Members,
|
||||
AboveGraphCorridorCandidate Representative,
|
||||
int Priority,
|
||||
double PreferredLaneY,
|
||||
double Span);
|
||||
|
||||
private readonly record struct CorridorClusterMetrics(
|
||||
int BrokenTopHighways,
|
||||
int OwnershipSplits,
|
||||
int PriorityInversions,
|
||||
double LaneSpan)
|
||||
{
|
||||
internal bool IsBetterThan(CorridorClusterMetrics baseline)
|
||||
{
|
||||
if (BrokenTopHighways != baseline.BrokenTopHighways)
|
||||
{
|
||||
return BrokenTopHighways < baseline.BrokenTopHighways;
|
||||
}
|
||||
|
||||
if (OwnershipSplits != baseline.OwnershipSplits)
|
||||
{
|
||||
return OwnershipSplits < baseline.OwnershipSplits;
|
||||
}
|
||||
|
||||
if (PriorityInversions != baseline.PriorityInversions)
|
||||
{
|
||||
return PriorityInversions < baseline.PriorityInversions;
|
||||
}
|
||||
|
||||
return LaneSpan + CoordinateTolerance < baseline.LaneSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user