Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.FaceConflictRepair.cs

3400 lines
141 KiB
C#

namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessor
{
internal static ElkRoutedEdge[] RepairBoundaryAnglesAndTargetApproaches(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var targetSlots = ResolveTargetApproachSlots(edges, nodesById, graphMinY, graphMaxY, minLineClearance, restrictedSet);
var result = new ElkRoutedEdge[edges.Length];
for (var i = 0; i < edges.Length; i++)
{
var edge = edges[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
result[i] = edge;
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
result[i] = edge;
continue;
}
var normalized = path
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
if (!preserveSourceExit)
{
var gatewaySourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId);
if (PathStartsAtDecisionVertex(gatewaySourceNormalized, sourceNode))
{
gatewaySourceNormalized = ForceDecisionSourceExitOffVertex(
gatewaySourceNormalized,
sourceNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId);
}
if (PathChanged(normalized, gatewaySourceNormalized)
&& HasAcceptableGatewayBoundaryPath(gatewaySourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
{
normalized = gatewaySourceNormalized;
}
}
}
else if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
&& !HasValidBoundaryAngle(normalized[0], normalized[1], sourceNode))
{
var sourceSide = ResolvePreferredRectSourceExitSide(normalized, sourceNode);
var sourcePath = normalized
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]);
var sourceNormalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide);
if (HasClearBoundarySegments(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 3))
{
normalized = sourceNormalized;
}
}
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
{
var assignedEndpoint = targetSlots.TryGetValue(edge.Id, out var slot)
? slot
: normalized[^1];
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
List<ElkPoint>? preferredGatewayTargetNormalized = null;
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var gatewaySourceNode)
&& TryBuildPreferredGatewayTargetEntryPath(
normalized,
gatewaySourceNode,
targetNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
out var preferredGatewayTargetRepair))
{
preferredGatewayTargetNormalized = preferredGatewayTargetRepair;
}
var gatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, assignedEndpoint);
if (gatewayTargetNormalized.Count >= 2
&& !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, gatewayTargetNormalized[^1], gatewayTargetNormalized[^2]))
{
var projectedBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, gatewayTargetNormalized[^2]);
projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedBoundary, gatewayTargetNormalized[^2]);
var projectedGatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, projectedBoundary);
if (PathChanged(gatewayTargetNormalized, projectedGatewayTargetNormalized)
&& HasAcceptableGatewayBoundaryPath(projectedGatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
{
gatewayTargetNormalized = projectedGatewayTargetNormalized;
}
}
if (preferredGatewayTargetNormalized is not null
&& (gatewayTargetNormalized.Count < 2
|| NeedsGatewayTargetBoundaryRepair(gatewayTargetNormalized, targetNode)
|| !string.Equals(
ElkEdgeRoutingGeometry.ResolveBoundarySide(gatewayTargetNormalized[^1], targetNode),
ElkEdgeRoutingGeometry.ResolveBoundarySide(preferredGatewayTargetNormalized[^1], targetNode),
StringComparison.Ordinal)
|| ComputePathLength(preferredGatewayTargetNormalized) + 4d < ComputePathLength(gatewayTargetNormalized)
|| HasTargetApproachBacktracking(gatewayTargetNormalized, targetNode)))
{
gatewayTargetNormalized = preferredGatewayTargetNormalized;
}
if (PathChanged(normalized, gatewayTargetNormalized)
&& HasAcceptableGatewayBoundaryPath(gatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
{
normalized = gatewayTargetNormalized;
}
}
else
{
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var shortcutSourceNode)
&& TryApplyPreferredBoundaryShortcut(
normalized,
shortcutSourceNode,
targetNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
requireUnderNodeImprovement: false,
minLineClearance,
out var preferredShortcut))
{
normalized = preferredShortcut;
}
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(assignedEndpoint, targetNode);
if (IsOnWrongSideOfTarget(normalized[^2], targetNode, targetSide, 0.5d)
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var correctedSide, out var correctedBoundary))
{
targetSide = correctedSide;
assignedEndpoint = correctedBoundary;
}
if (HasTargetApproachBacktracking(normalized, targetNode)
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary))
{
targetSide = preferredSide;
assignedEndpoint = preferredBoundary;
}
if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
{
var alignedAssignedSideEntry = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint);
if (HasClearBoundarySegments(alignedAssignedSideEntry, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(alignedAssignedSideEntry[^1], alignedAssignedSideEntry[^2], targetNode))
{
normalized = alignedAssignedSideEntry;
}
else
{
var preferredEntrySide = ResolvePreferredRectTargetEntrySide(normalized, targetNode);
if (!string.Equals(preferredEntrySide, targetSide, StringComparison.Ordinal))
{
targetSide = preferredEntrySide;
assignedEndpoint = BuildRectBoundaryPointForSide(targetNode, targetSide, normalized[^2]);
}
}
}
if (!ElkEdgeRoutingGeometry.PointsEqual(assignedEndpoint, normalized[^1])
|| !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
{
var targetNormalized = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint);
if (HasClearBoundarySegments(targetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
{
normalized = targetNormalized;
}
}
var shortenedApproach = TrimTargetApproachBacktracking(normalized, targetNode, targetSide, assignedEndpoint);
if (PathChanged(normalized, shortenedApproach)
&& HasClearBoundarySegments(shortenedApproach, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(shortenedApproach[^1], shortenedApproach[^2], targetNode))
{
normalized = shortenedApproach;
}
if (HasTargetApproachBacktracking(normalized, targetNode)
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair)
&& HasClearBoundarySegments(backtrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(backtrackingRepair[^1], backtrackingRepair[^2], targetNode))
{
normalized = backtrackingRepair;
}
}
}
if (normalized.Count == path.Count
&& normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
{
result[i] = edge;
continue;
}
result[i] = BuildSingleSectionEdge(edge, normalized);
}
return result;
}
internal static ElkRoutedEdge[] SpreadTargetApproachJoins(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null,
bool forceOutwardAxisSpacing = false)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
var groups = result
.Select((edge, index) => new
{
Edge = edge,
Index = index,
Path = ExtractFullPath(edge),
})
.Where(item => item.Path.Count >= 2
&& nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out _))
.GroupBy(
item =>
{
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
var side = ResolveTargetApproachSide(item.Path, targetNode);
return $"{targetNode.Id}|{side}";
},
StringComparer.Ordinal);
foreach (var group in groups)
{
var entries = group
.Select(item =>
{
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
var side = ResolveTargetApproachSide(item.Path, targetNode);
var endpoint = item.Path[^1];
return new
{
item.Edge,
item.Index,
item.Path,
TargetNode = targetNode,
Side = side,
Endpoint = endpoint,
};
})
.ToArray();
if (entries.Length < 2)
{
continue;
}
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
{
continue;
}
var targetNode = entries[0].TargetNode;
var side = entries[0].Side;
var targetBoundaryEntries = entries
.Select(entry => (
entry.Edge.Id,
Coordinate: side is "left" or "right" ? entry.Endpoint.Y : entry.Endpoint.X,
IsOutgoing: false))
.ToArray();
var joinEntries = entries
.Select(entry => (Path: (IReadOnlyList<ElkPoint>)entry.Path, Side: entry.Side))
.ToArray();
var requiredJoinGap = ElkBoundarySlots.ResolveRequiredBoundarySlotGap(
targetNode,
side,
entries.Length,
minLineClearance);
var hasRunJoin = GroupHasTargetApproachJoin(joinEntries, requiredJoinGap);
var hasBandJoin = GroupHasTargetApproachBandJoin(joinEntries, requiredJoinGap);
var hasBoundarySlotIssue = HasBoundarySlotAlignmentIssue(
targetBoundaryEntries,
targetNode,
side,
minLineClearance);
if (!hasRunJoin && !hasBandJoin && !hasBoundarySlotIssue)
{
continue;
}
var isGatewayTarget = ElkShapeBoundaries.IsGatewayShape(targetNode);
var slotOnlyRepair = !hasRunJoin && !hasBandJoin && hasBoundarySlotIssue;
var sorted = side is "left" or "right"
? entries.OrderBy(entry => entry.Endpoint.Y).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray()
: entries.OrderBy(entry => entry.Endpoint.X).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray();
var sideLength = side is "left" or "right"
? Math.Max(8d, targetNode.Height - 8d)
: Math.Max(8d, targetNode.Width - 8d);
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(
targetNode,
side,
sorted.Select(entry => side is "left" or "right" ? entry.Endpoint.Y : entry.Endpoint.X).ToArray());
var currentApproachAxes = sorted
.Select(entry => ResolveSpreadableTargetApproachAxis(
entry.Path,
targetNode,
entry.Side,
minLineClearance))
.Where(axis => !double.IsNaN(axis))
.ToArray();
var baseApproachAxis = isGatewayTarget
? ResolveDefaultTargetApproachAxis(targetNode, side)
: currentApproachAxes.Length > 0
? forceOutwardAxisSpacing
? side switch
{
"left" or "top" => currentApproachAxes.Max(),
"right" or "bottom" => currentApproachAxes.Min(),
_ => ResolveDefaultTargetApproachAxis(targetNode, side),
}
: currentApproachAxes.Min()
: ResolveDefaultTargetApproachAxis(targetNode, side);
var approachAxisSpacing = sorted.Length > 1
? ResolveBoundaryJoinSlotSpacing(
minLineClearance,
sideLength,
Math.Min(sorted.Length, ElkBoundarySlots.ResolveBoundarySlotCapacity(targetNode, side)))
: 0d;
for (var i = 0; i < sorted.Length; i++)
{
var currentSpreadableAxis = ResolveSpreadableTargetApproachAxis(
sorted[i].Path,
targetNode,
sorted[i].Side,
minLineClearance);
var desiredApproachAxis = slotOnlyRepair
? currentSpreadableAxis
: ResolveDesiredTargetApproachAxis(
targetNode,
side,
baseApproachAxis,
approachAxisSpacing,
i,
forceOutwardAxisSpacing);
if (isGatewayTarget)
{
var slotPoint = ElkBoundarySlots.BuildBoundarySlotPoint(targetNode, side, assignedSlotCoordinates[i]);
var exteriorIndex = FindLastGatewayExteriorPointIndex(sorted[i].Path, targetNode);
var exteriorAnchor = sorted[i].Path[exteriorIndex];
var gatewayCandidate = TryBuildSlottedGatewayEntryPath(
sorted[i].Path,
targetNode,
exteriorIndex,
exteriorAnchor,
slotPoint)
?? NormalizeGatewayEntryPath(sorted[i].Path, targetNode, slotPoint);
var gatewayApproachAxis = double.IsNaN(desiredApproachAxis)
? ResolveTargetApproachAxisValue(gatewayCandidate, sorted[i].Side)
: desiredApproachAxis;
if (double.IsNaN(gatewayApproachAxis))
{
gatewayApproachAxis = ResolveDefaultTargetApproachAxis(targetNode, side);
}
var spreadGatewayCandidate = RewriteTargetApproachRun(
gatewayCandidate,
sorted[i].Side,
slotPoint,
gatewayApproachAxis);
spreadGatewayCandidate = PreferGatewayDiagonalTargetEntry(spreadGatewayCandidate, targetNode);
if (PathChanged(gatewayCandidate, spreadGatewayCandidate)
&& CanAcceptGatewayTargetRepair(spreadGatewayCandidate, targetNode))
{
gatewayCandidate = spreadGatewayCandidate;
}
if (!PathChanged(sorted[i].Path, gatewayCandidate)
|| !CanAcceptGatewayTargetRepair(gatewayCandidate, targetNode))
{
continue;
}
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, gatewayCandidate);
continue;
}
var desiredEndpoint = ElkBoundarySlots.BuildBoundarySlotPoint(targetNode, side, assignedSlotCoordinates[i]);
var currentRunAxis = ResolveTargetApproachAxisValue(sorted[i].Path, sorted[i].Side);
var preserveApproachBand = HasProtectedUnderNodeGeometry(sorted[i].Edge)
|| HasCorridorBendPoints(sorted[i].Edge, graphMinY, graphMaxY);
var desiredRunAxis = preserveApproachBand
? currentRunAxis
: double.IsNaN(desiredApproachAxis)
? currentRunAxis
: desiredApproachAxis;
if (double.IsNaN(desiredRunAxis))
{
desiredRunAxis = double.IsNaN(desiredApproachAxis)
? ResolveDefaultTargetApproachAxis(targetNode, side)
: desiredApproachAxis;
}
var candidatePath = sorted[i].Path;
if (hasBandJoin && !preserveApproachBand)
{
var desiredBandCoordinate = side is "left" or "right"
? desiredEndpoint.Y
: desiredEndpoint.X;
var bandCandidate = RewriteTargetApproachBand(
candidatePath,
sorted[i].Side,
desiredBandCoordinate,
desiredRunAxis,
targetNode);
if (PathChanged(candidatePath, bandCandidate))
{
candidatePath = bandCandidate;
}
}
var candidate = RewriteTargetApproachRun(
candidatePath,
sorted[i].Side,
desiredEndpoint,
desiredRunAxis);
if (!PathChanged(sorted[i].Path, candidate)
|| !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4))
{
continue;
}
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate);
}
}
return result;
}
private static Dictionary<string, double> ResolveGatewayBoundaryBandSlotCoordinates(
IReadOnlyList<(string EdgeId, ElkPoint Endpoint)> entries,
ElkPositionedNode targetNode,
string side,
double minLineClearance)
{
var result = new Dictionary<string, double>(StringComparer.Ordinal);
if (entries.Count == 0)
{
return result;
}
var centerX = targetNode.X + (targetNode.Width / 2d);
var centerY = targetNode.Y + (targetNode.Height / 2d);
var bandGroups = entries
.GroupBy(entry =>
{
if (side is "top" or "bottom")
{
return entry.Endpoint.X <= centerX ? "near-left" : "near-right";
}
return entry.Endpoint.Y <= centerY ? "near-top" : "near-bottom";
}, StringComparer.Ordinal);
foreach (var bandGroup in bandGroups)
{
var bandEntries = (side is "top" or "bottom"
? bandGroup.OrderBy(entry => entry.Endpoint.X)
: bandGroup.OrderBy(entry => entry.Endpoint.Y))
.ThenBy(entry => entry.EdgeId, StringComparer.Ordinal)
.ToArray();
var (bandMin, bandMax) = ResolveGatewayBoundaryBandRange(targetNode, side, bandGroup.Key, centerX, centerY);
var bandLength = Math.Max(8d, bandMax - bandMin);
var bandSlotSpacing = bandEntries.Length > 1
? ResolveBoundaryJoinSlotSpacing(minLineClearance, bandLength, bandEntries.Length)
: 0d;
var bandTotalSpan = (bandEntries.Length - 1) * bandSlotSpacing;
var bandCenter = (bandMin + bandMax) / 2d;
var bandStart = Math.Max(bandMin, bandCenter - (bandTotalSpan / 2d));
for (var i = 0; i < bandEntries.Length; i++)
{
result[bandEntries[i].EdgeId] = Math.Min(bandMax, bandStart + (i * bandSlotSpacing));
}
}
return result;
}
private static (double Min, double Max) ResolveGatewayBoundaryBandRange(
ElkPositionedNode targetNode,
string side,
string bandKey,
double centerX,
double centerY)
{
return side switch
{
"top" or "bottom" when string.Equals(bandKey, "near-left", StringComparison.Ordinal)
=> (targetNode.X + 4d, centerX),
"top" or "bottom" => (centerX, targetNode.X + targetNode.Width - 4d),
"left" or "right" when string.Equals(bandKey, "near-top", StringComparison.Ordinal)
=> (targetNode.Y + 4d, centerY),
"left" or "right" => (centerY, targetNode.Y + targetNode.Height - 4d),
_ => side is "top" or "bottom"
? (targetNode.X + 4d, targetNode.X + targetNode.Width - 4d)
: (targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d),
};
}
internal static ElkRoutedEdge[] SpreadSourceDepartureJoins(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
var groups = result
.Select((edge, index) => new
{
Edge = edge,
Index = index,
Path = ExtractFullPath(edge),
})
.Where(item => item.Path.Count >= 2
&& nodesById.TryGetValue(item.Edge.SourceNodeId ?? string.Empty, out _)
&& ShouldSpreadSourceDeparture(item.Edge, graphMinY, graphMaxY))
.GroupBy(
item =>
{
var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty];
var side = ResolveSourceDepartureSide(item.Path, sourceNode);
return $"{sourceNode.Id}|{side}";
},
StringComparer.Ordinal);
foreach (var group in groups)
{
var entries = group
.Select(item =>
{
var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty];
var side = ResolveSourceDepartureSide(item.Path, sourceNode);
return new
{
item.Edge,
item.Index,
item.Path,
SourceNode = sourceNode,
Side = side,
Boundary = item.Path[0],
TargetReference = side is "left" or "right"
? item.Path[^1].Y
: item.Path[^1].X,
PathLength = ComputePathLength(item.Path),
};
})
.ToArray();
if (entries.Length < 2)
{
continue;
}
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
{
continue;
}
var sourceNode = entries[0].SourceNode;
var side = entries[0].Side;
var sourceBoundaryEntries = entries
.Select(entry => (
entry.Edge.Id,
Coordinate: side is "left" or "right" ? entry.Boundary.Y : entry.Boundary.X,
IsOutgoing: true))
.ToArray();
var joinEntries = entries
.Select(entry => (Path: (IReadOnlyList<ElkPoint>)entry.Path, Side: entry.Side))
.ToArray();
var hasDepartureJoin = GroupHasSourceDepartureJoin(joinEntries, minLineClearance);
var hasBoundarySlotIssue = HasBoundarySlotAlignmentIssue(
sourceBoundaryEntries,
sourceNode,
side,
minLineClearance);
if (!hasDepartureJoin && !hasBoundarySlotIssue)
{
continue;
}
var isGatewaySource = ElkShapeBoundaries.IsGatewayShape(sourceNode);
var slotOnlyRepair = !hasDepartureJoin && hasBoundarySlotIssue;
var boundaryCoordinate = side is "left" or "right"
? entries[0].Boundary.Y
: entries[0].Boundary.X;
var anchor = entries
.OrderBy(entry => Math.Abs(entry.TargetReference - boundaryCoordinate))
.ThenBy(entry => entry.PathLength)
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
.First();
var sorted = entries
.OrderBy(entry => entry.TargetReference)
.ThenBy(entry => entry.PathLength)
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
.ToArray();
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(
sourceNode,
side,
sorted.Select(entry => entry.TargetReference).ToArray());
var desiredCoordinateByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal);
var anchorDepartureAxis = TryExtractSourceDepartureRun(anchor.Path, side, out _, out var anchorRunEndIndex)
? side is "left" or "right"
? anchor.Path[anchorRunEndIndex].X
: anchor.Path[anchorRunEndIndex].Y
: side switch
{
"left" => sourceNode.X - 24d,
"right" => sourceNode.X + sourceNode.Width + 24d,
"top" => sourceNode.Y - 24d,
"bottom" => sourceNode.Y + sourceNode.Height + 24d,
_ => 0d,
};
var desiredAxisByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal);
for (var i = 0; i < sorted.Length; i++)
{
desiredCoordinateByEdgeId[sorted[i].Edge.Id] = assignedSlotCoordinates[i];
desiredAxisByEdgeId[sorted[i].Edge.Id] = slotOnlyRepair
? TryExtractSourceDepartureRun(sorted[i].Path, side, out _, out var sortedRunEndIndex)
? side is "left" or "right"
? sorted[i].Path[sortedRunEndIndex].X
: sorted[i].Path[sortedRunEndIndex].Y
: anchorDepartureAxis
: anchorDepartureAxis;
}
foreach (var entry in entries)
{
if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var slotCoordinate))
{
continue;
}
if (!desiredAxisByEdgeId.TryGetValue(entry.Edge.Id, out var desiredAxis))
{
continue;
}
var originalCoordinate = side is "left" or "right"
? entry.Boundary.Y
: entry.Boundary.X;
var originalAxis = TryExtractSourceDepartureRun(entry.Path, side, out _, out var runEndIndex)
? side is "left" or "right"
? entry.Path[runEndIndex].X
: entry.Path[runEndIndex].Y
: desiredAxis;
if (Math.Abs(originalCoordinate - slotCoordinate) <= 0.5d
&& Math.Abs(originalAxis - desiredAxis) <= 0.5d)
{
continue;
}
var boundaryPoint = ElkBoundarySlots.BuildBoundarySlotPoint(sourceNode, side, slotCoordinate);
if (isGatewaySource)
{
var continuation = entry.Path.Count > 1 ? entry.Path[1] : entry.Path[0];
boundaryPoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundaryPoint, continuation);
}
var candidate = BuildSourceDepartureCandidatePath(
entry.Path,
sourceNode,
side,
boundaryPoint,
desiredAxis,
nodes,
entry.Edge.SourceNodeId,
entry.Edge.TargetNodeId);
if (!PathChanged(entry.Path, candidate))
{
continue;
}
if (isGatewaySource)
{
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, sourceNode, fromStart: true))
{
continue;
}
}
else
{
if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2)
|| !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode)
|| HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId))
{
continue;
}
}
result[entry.Index] = BuildSingleSectionEdge(entry.Edge, candidate);
}
}
return result;
}
internal static ElkRoutedEdge[] SpreadRectTargetApproachFeederBands(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
var groups = result
.Select((edge, index) => new
{
Edge = edge,
Index = index,
Path = ExtractFullPath(edge),
})
.Where(item => item.Path.Count >= 3
&& nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out var targetNode)
&& !ElkShapeBoundaries.IsGatewayShape(targetNode))
.GroupBy(
item =>
{
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
var side = ResolveTargetApproachSide(item.Path, targetNode);
return $"{targetNode.Id}|{side}";
},
StringComparer.Ordinal);
foreach (var group in groups)
{
var entries = group
.Select(item =>
{
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
var side = ResolveTargetApproachSide(item.Path, targetNode);
return TryExtractTargetApproachFeeder(item.Path, side, out var feeder)
? new
{
item.Edge,
item.Index,
item.Path,
TargetNode = targetNode,
Side = side,
Feeder = feeder,
}
: null;
})
.Where(entry => entry is not null)
.Select(entry => entry!)
.ToArray();
if (entries.Length < 2)
{
continue;
}
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
{
continue;
}
var conflictNeighbors = new List<int>[entries.Length];
for (var i = 0; i < entries.Length; i++)
{
conflictNeighbors[i] = [];
}
var hasConflict = false;
for (var i = 0; i < entries.Length; i++)
{
for (var j = i + 1; j < entries.Length; j++)
{
if (ElkEdgeRoutingGeometry.AreParallelAndClose(
entries[i].Feeder.Start,
entries[i].Feeder.End,
entries[j].Feeder.Start,
entries[j].Feeder.End,
minLineClearance))
{
hasConflict = true;
conflictNeighbors[i].Add(j);
conflictNeighbors[j].Add(i);
}
}
}
if (!hasConflict)
{
continue;
}
var visited = new bool[entries.Length];
for (var componentStart = 0; componentStart < entries.Length; componentStart++)
{
if (visited[componentStart] || conflictNeighbors[componentStart].Count == 0)
{
continue;
}
var queue = new Queue<int>();
var componentIndices = new List<int>();
queue.Enqueue(componentStart);
visited[componentStart] = true;
while (queue.Count > 0)
{
var current = queue.Dequeue();
componentIndices.Add(current);
foreach (var neighbor in conflictNeighbors[current])
{
if (visited[neighbor])
{
continue;
}
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
if (componentIndices.Count < 2)
{
continue;
}
var componentEntries = componentIndices
.Select(index => entries[index])
.ToArray();
if (restrictedSet is not null && !componentEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
{
continue;
}
var spacing = Math.Max(12d, minLineClearance + 4d);
var sorted = componentEntries
.OrderBy(entry => entry.Feeder.BandCoordinate)
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
.ToArray();
var baseBand = sorted[0].Side is "left" or "top"
? sorted.Max(entry => entry.Feeder.BandCoordinate)
: sorted.Min(entry => entry.Feeder.BandCoordinate);
for (var i = 0; i < sorted.Length; i++)
{
var desiredBand = ResolveDesiredTargetApproachAxis(
sorted[i].TargetNode,
sorted[i].Side,
baseBand,
spacing,
i,
forceOutwardFromBoundary: true);
if (Math.Abs(sorted[i].Feeder.BandCoordinate - desiredBand) <= 0.5d)
{
continue;
}
var candidate = RewriteTargetApproachFeederBand(sorted[i].Path, sorted[i].Side, desiredBand);
if (!PathChanged(sorted[i].Path, candidate)
|| !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4)
|| HasNodeObstacleCrossing(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId))
{
continue;
}
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate);
}
}
}
return result;
}
internal static ElkRoutedEdge[] SeparateMixedNodeFaceLaneConflicts(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
var entries = new List<(int Index, ElkRoutedEdge Edge, IReadOnlyList<ElkPoint> Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)>();
for (var index = 0; index < result.Length; index++)
{
var edge = result[index];
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
continue;
}
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
&& (ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|| ElkShapeBoundaries.IsGatewayShape(sourceNode)))
{
var side = ResolveSourceDepartureSide(path, sourceNode);
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
? side is "left" or "right"
? path[runEndIndex].X
: path[runEndIndex].Y
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
entries.Add((
index,
edge,
path,
sourceNode,
side,
true,
path[0],
side is "left" or "right" ? path[0].Y : path[0].X,
axisValue));
}
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
&& ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY))
{
var side = ResolveTargetApproachSide(path, targetNode);
var axisValue = ResolveTargetApproachAxisValue(path, side);
if (double.IsNaN(axisValue))
{
axisValue = side is "left" or "right" ? path[^1].Y : path[^1].X;
}
entries.Add((
index,
edge,
path,
targetNode,
side,
false,
path[^1],
side is "left" or "right" ? path[^1].Y : path[^1].X,
axisValue));
}
}
foreach (var group in entries.GroupBy(
entry => $"{entry.Node.Id}|{entry.Side}",
StringComparer.Ordinal))
{
var groupEntries = group.ToArray();
var hasBoundarySlotIssue = groupEntries.Length >= 2
&& HasBoundarySlotAlignmentIssue(
groupEntries
.Select(entry => (entry.Edge.Id, entry.BoundaryCoordinate, entry.IsOutgoing))
.ToArray(),
groupEntries[0].Node,
groupEntries[0].Side,
minLineClearance);
if (groupEntries.Length < 2
|| !groupEntries.Any(entry => entry.IsOutgoing)
|| !groupEntries.Any(entry => !entry.IsOutgoing)
|| (!GroupHasMixedNodeFaceLaneConflict(groupEntries, minLineClearance) && !hasBoundarySlotIssue))
{
continue;
}
if (restrictedSet is not null && !groupEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
{
continue;
}
var node = groupEntries[0].Node;
var side = groupEntries[0].Side;
var orderedEntries = groupEntries
.OrderBy(entry => entry.BoundaryCoordinate)
.ThenBy(entry => entry.IsOutgoing ? 0 : 1)
.ThenBy(entry => IsRepeatCollectorLabel(entry.Edge.Label) ? 1 : 0)
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
.ToArray();
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(
node,
side,
orderedEntries.Select(entry => entry.BoundaryCoordinate).ToArray());
var desiredCoordinateByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal);
for (var i = 0; i < orderedEntries.Length; i++)
{
desiredCoordinateByEdgeId[orderedEntries[i].Edge.Id] = assignedSlotCoordinates[i];
}
var hasAssignedSlotCollision = HasDuplicateBoundarySlotCoordinates(assignedSlotCoordinates);
foreach (var entry in groupEntries)
{
var forceAlternateGatewayFaceCandidate = hasAssignedSlotCollision
&& ElkShapeBoundaries.IsGatewayShape(entry.Node);
if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var desiredCoordinate)
|| (!forceAlternateGatewayFaceCandidate
&& Math.Abs(desiredCoordinate - entry.BoundaryCoordinate) <= 0.5d))
{
continue;
}
var bestEdge = result[entry.Index];
var currentGroupEdges = groupEntries
.Select(item => result[item.Index])
.ToArray();
var bestSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentGroupEdges, nodes);
var bestTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentGroupEdges, nodes);
var bestBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(currentGroupEdges, nodes);
var bestBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(currentGroupEdges, nodes);
var bestGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(currentGroupEdges, nodes);
var bestUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(currentGroupEdges, nodes);
var bestPathLength = ComputePathLength(entry.Path);
var prefersAlternateRepeatFace = !entry.IsOutgoing
&& !ElkShapeBoundaries.IsGatewayShape(entry.Node)
&& IsRepeatCollectorLabel(entry.Edge.Label)
&& groupEntries.Any(other => other.IsOutgoing);
var candidatePaths = new List<IReadOnlyList<ElkPoint>>();
var directCandidate = entry.IsOutgoing
? BuildMixedSourceFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue)
: BuildMixedTargetFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue);
AddUniquePathCandidate(candidatePaths, directCandidate);
var availableSpan = Math.Abs(desiredCoordinate - entry.BoundaryCoordinate);
if ((forceAlternateGatewayFaceCandidate || prefersAlternateRepeatFace || availableSpan + 0.5d < minLineClearance)
&& TryBuildAlternateMixedFaceCandidate(entry, nodes, minLineClearance, out var alternateCandidate))
{
AddUniquePathCandidate(candidatePaths, alternateCandidate);
}
foreach (var candidate in candidatePaths)
{
if (!PathChanged(entry.Path, candidate)
|| HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId))
{
continue;
}
if (entry.IsOutgoing)
{
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
{
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: true))
{
continue;
}
}
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2)
|| !HasValidBoundaryAngle(candidate[0], candidate[1], entry.Node))
{
continue;
}
}
else
{
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
{
if (!CanAcceptGatewayTargetRepair(candidate, entry.Node)
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: false))
{
continue;
}
}
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, false, 4)
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], entry.Node)
|| HasTargetApproachBacktracking(candidate, entry.Node))
{
continue;
}
}
var candidateEdge = BuildSingleSectionEdge(entry.Edge, candidate);
var candidateGroupEdges = groupEntries
.Select(item => item.Index == entry.Index ? candidateEdge : result[item.Index])
.ToArray();
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateGroupEdges, nodes);
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateGroupEdges, nodes);
var candidateBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidateGroupEdges, nodes);
var candidateBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidateGroupEdges, nodes);
var candidateGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateGroupEdges, nodes);
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateGroupEdges, nodes);
var candidatePathLength = ComputePathLength(candidate);
var prefersForcedAlternateGatewayFace = forceAlternateGatewayFaceCandidate
&& entry.IsOutgoing
&& ResolveSourceDepartureSide(candidate, entry.Node) != entry.Side
&& ResolveSourceDepartureSide(ExtractFullPath(bestEdge), entry.Node) == entry.Side
&& candidateSharedLaneViolations <= bestSharedLaneViolations
&& candidateTargetJoinViolations <= bestTargetJoinViolations
&& candidateBoundarySlotViolations <= bestBoundarySlotViolations
&& candidateBoundaryAngleViolations <= bestBoundaryAngleViolations
&& candidateGatewaySourceExitViolations <= bestGatewaySourceExitViolations
&& candidateUnderNodeViolations <= bestUnderNodeViolations;
if (prefersForcedAlternateGatewayFace)
{
bestEdge = candidateEdge;
bestSharedLaneViolations = candidateSharedLaneViolations;
bestTargetJoinViolations = candidateTargetJoinViolations;
bestBoundarySlotViolations = candidateBoundarySlotViolations;
bestBoundaryAngleViolations = candidateBoundaryAngleViolations;
bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations;
bestUnderNodeViolations = candidateUnderNodeViolations;
bestPathLength = candidatePathLength;
continue;
}
if (!IsBetterMixedNodeFaceCandidate(
candidateSharedLaneViolations,
candidateTargetJoinViolations,
candidateBoundarySlotViolations,
candidateBoundaryAngleViolations,
candidateGatewaySourceExitViolations,
candidateUnderNodeViolations,
candidatePathLength,
bestSharedLaneViolations,
bestTargetJoinViolations,
bestBoundarySlotViolations,
bestBoundaryAngleViolations,
bestGatewaySourceExitViolations,
bestUnderNodeViolations,
bestPathLength))
{
continue;
}
bestEdge = candidateEdge;
bestSharedLaneViolations = candidateSharedLaneViolations;
bestTargetJoinViolations = candidateTargetJoinViolations;
bestBoundarySlotViolations = candidateBoundarySlotViolations;
bestBoundaryAngleViolations = candidateBoundaryAngleViolations;
bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations;
bestUnderNodeViolations = candidateUnderNodeViolations;
bestPathLength = candidatePathLength;
}
result[entry.Index] = bestEdge;
}
}
return result;
}
internal static ElkRoutedEdge[] SeparateRepeatCollectorLocalLaneConflicts(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var result = edges.ToArray();
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var nodeObstacles = nodes.Select(node => (
Left: node.X,
Top: node.Y,
Right: node.X + node.Width,
Bottom: node.Y + node.Height,
Id: node.Id)).ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
if (!IsRepeatCollectorLabel(edge.Label))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 3)
{
continue;
}
for (var segmentIndex = 0; segmentIndex < path.Count - 1; segmentIndex++)
{
var start = path[segmentIndex];
var end = path[segmentIndex + 1];
var isHorizontal = Math.Abs(start.Y - end.Y) <= 0.5d;
var isVertical = Math.Abs(start.X - end.X) <= 0.5d;
if (!isHorizontal && !isVertical)
{
continue;
}
var conflictFound = false;
var desiredCoordinate = 0d;
foreach (var otherEdge in result)
{
if (otherEdge.Id == edge.Id)
{
continue;
}
foreach (var otherSegment in ElkEdgeRoutingGeometry.FlattenSegments(otherEdge))
{
if (!ElkEdgeRoutingGeometry.AreParallelAndClose(start, end, otherSegment.Start, otherSegment.End, minLineClearance))
{
continue;
}
if (isHorizontal)
{
desiredCoordinate = start.Y <= otherSegment.Start.Y
? otherSegment.Start.Y - (minLineClearance + 4d)
: otherSegment.Start.Y + (minLineClearance + 4d);
}
else
{
desiredCoordinate = start.X <= otherSegment.Start.X
? otherSegment.Start.X - (minLineClearance + 4d)
: otherSegment.Start.X + (minLineClearance + 4d);
}
conflictFound = true;
break;
}
if (conflictFound)
{
break;
}
}
if (!conflictFound)
{
continue;
}
var preferredCoordinate = desiredCoordinate;
var fallbackCoordinate = isHorizontal
? start.Y + (start.Y - desiredCoordinate)
: start.X + (start.X - desiredCoordinate);
foreach (var alternateCoordinate in new[] { preferredCoordinate, fallbackCoordinate }.Distinct())
{
var candidate = ShiftSingleOrthogonalRun(path, segmentIndex, alternateCoordinate);
if (!PathChanged(path, candidate)
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY))
{
continue;
}
var crossesObstacle = false;
for (var candidateIndex = 0; candidateIndex < candidate.Count - 1; candidateIndex++)
{
if (!SegmentCrossesObstacle(candidate[candidateIndex], candidate[candidateIndex + 1], nodeObstacles, edge.SourceNodeId, edge.TargetNodeId))
{
continue;
}
crossesObstacle = true;
break;
}
if (crossesObstacle)
{
continue;
}
var repairedEdge = BuildSingleSectionEdge(edge, candidate);
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
[repairedEdge],
nodes,
minLineClearance)[0];
var repairedPath = ExtractFullPath(repairedEdge);
if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY))
{
continue;
}
result[i] = repairedEdge;
break;
}
}
}
return result;
}
internal static ElkRoutedEdge[] ElevateRepeatCollectorNodeClearanceViolations(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var corridorY = graphMinY - Math.Max(24d, minLineClearance * 0.6d);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
if (!IsRepeatCollectorLabel(edge.Label)
|| !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2
|| !HasRepeatCollectorNodeClearanceViolation(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance))
{
continue;
}
var targetApproachY = Math.Min(corridorY, targetNode.Y - 24d);
ElkPoint targetEndpoint;
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
var slotCoordinate = Math.Max(targetNode.X + 4d, Math.Min(targetNode.X + targetNode.Width - 4d, path[^1].X));
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", slotCoordinate, out targetEndpoint))
{
continue;
}
targetEndpoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
targetNode,
targetEndpoint,
new ElkPoint { X = targetEndpoint.X, Y = targetApproachY });
}
else
{
targetEndpoint = BuildRectBoundaryPointForSide(targetNode, "top", path[0]);
}
var rebuilt = new List<ElkPoint>
{
new() { X = path[0].X, Y = path[0].Y },
};
if (Math.Abs(rebuilt[^1].Y - corridorY) > 0.5d)
{
rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = corridorY });
}
if (Math.Abs(rebuilt[^1].X - targetEndpoint.X) > 0.5d)
{
rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = rebuilt[^1].Y });
}
if (Math.Abs(rebuilt[^1].Y - targetApproachY) > 0.5d)
{
rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = targetApproachY });
}
rebuilt.Add(targetEndpoint);
var candidate = NormalizePathPoints(rebuilt);
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
candidate = NormalizeGatewayEntryPath(candidate, targetNode, targetEndpoint);
}
if (!PathChanged(path, candidate)
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| HasRepeatCollectorNodeClearanceViolation(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance))
{
continue;
}
var repairedEdge = BuildSingleSectionEdge(edge, candidate);
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
var repairedPath = ExtractFullPath(repairedEdge);
if (repairedPath.Count < 2
|| HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| HasRepeatCollectorNodeClearanceViolation(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
|| (ElkShapeBoundaries.IsGatewayShape(targetNode)
? !CanAcceptGatewayTargetRepair(repairedPath, targetNode)
: !HasValidBoundaryAngle(repairedPath[^1], repairedPath[^2], targetNode)))
{
continue;
}
result[i] = repairedEdge;
}
return result;
}
internal static ElkRoutedEdge[] ElevateUnderNodeViolations(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
continue;
}
if (TryResolveUnderNodeWithPreferredShortcut(
edge,
path,
nodes,
minLineClearance,
out var directRepair))
{
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(edge, nodes);
var repairedEdge = BuildSingleSectionEdge(edge, directRepair);
repairedEdge = ResolveUnderNodePeerTargetConflicts(
repairedEdge,
result,
i,
nodes,
minLineClearance);
var repairedPath = ExtractFullPath(repairedEdge);
var repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
var repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
var repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
WriteUnderNodeDebug(
edge.Id,
$"accept-check raw current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
if (repairedUnderNodeSegments < currentUnderNodeSegments
&& !repairedCrossesNode
&& repairedLocalHardPressure <= currentLocalHardPressure)
{
WriteUnderNodeDebug(edge.Id, "accept-check raw accepted");
result[i] = repairedUnderNodeSegments == 0
? ProtectUnderNodeGeometry(repairedEdge)
: repairedEdge;
continue;
}
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
[repairedEdge],
nodes,
minLineClearance)[0];
repairedEdge = FinalizeGatewayBoundaryGeometry([repairedEdge], nodes)[0];
repairedEdge = NormalizeBoundaryAngles([repairedEdge], nodes)[0];
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
repairedEdge = ResolveUnderNodePeerTargetConflicts(
repairedEdge,
result,
i,
nodes,
minLineClearance);
repairedPath = ExtractFullPath(repairedEdge);
repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
WriteUnderNodeDebug(
edge.Id,
$"accept-check normalized current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
if (repairedUnderNodeSegments < currentUnderNodeSegments
&& !repairedCrossesNode
&& repairedLocalHardPressure <= currentLocalHardPressure)
{
WriteUnderNodeDebug(edge.Id, "accept-check normalized accepted");
result[i] = repairedUnderNodeSegments == 0
? ProtectUnderNodeGeometry(repairedEdge)
: repairedEdge;
}
continue;
}
var lifted = TryLiftUnderNodeSegments(
path,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
minLineClearance);
if (!PathChanged(path, lifted))
{
continue;
}
var liftedEdge = BuildSingleSectionEdge(edge, lifted);
liftedEdge = NormalizeBoundaryAngles([liftedEdge], nodes)[0];
liftedEdge = NormalizeSourceExitAngles([liftedEdge], nodes)[0];
var liftedPath = ExtractFullPath(liftedEdge);
if (CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
< CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
&& !HasNodeObstacleCrossing(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId))
{
result[i] = CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) == 0
? ProtectUnderNodeGeometry(liftedEdge)
: liftedEdge;
}
}
return result;
}
private static int ComputeUnderNodeRepairLocalHardPressure(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
return ElkEdgeRoutingScoring.CountBelowGraphViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountLongDiagonalViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], nodes)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountSharedLaneViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations([edge], nodes);
}
internal static ElkRoutedEdge[] PolishTargetPeerConflicts(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
if (restrictedSet is not null && !restrictedSet.Contains(result[i].Id))
{
continue;
}
result[i] = ResolveUnderNodePeerTargetConflicts(
result[i],
result,
i,
nodes,
minLineClearance);
}
return result;
}
private static ElkRoutedEdge ResolveUnderNodePeerTargetConflicts(
ElkRoutedEdge candidateEdge,
IReadOnlyList<ElkRoutedEdge> currentEdges,
int candidateIndex,
ElkPositionedNode[] nodes,
double minLineClearance)
{
if (TryPolishGatewayUnderNodeTargetPeerConflicts(
candidateEdge,
currentEdges,
candidateIndex,
nodes,
minLineClearance,
out var gatewayPolishedEdge))
{
return gatewayPolishedEdge;
}
return TryPolishRectUnderNodeTargetPeerConflicts(
candidateEdge,
currentEdges,
candidateIndex,
nodes,
minLineClearance,
out var polishedEdge)
? polishedEdge
: candidateEdge;
}
private static bool TryPolishGatewayUnderNodeTargetPeerConflicts(
ElkRoutedEdge candidateEdge,
IReadOnlyList<ElkRoutedEdge> currentEdges,
int candidateIndex,
ElkPositionedNode[] nodes,
double minLineClearance,
out ElkRoutedEdge polishedEdge)
{
polishedEdge = candidateEdge;
if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId))
{
return false;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode)
|| !ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return false;
}
nodesById.TryGetValue(candidateEdge.SourceNodeId ?? string.Empty, out var sourceNode);
var peerEdges = currentEdges
.Where((edge, index) =>
index != candidateIndex
&& string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal))
.ToArray();
if (peerEdges.Length == 0)
{
return false;
}
var path = ExtractFullPath(candidateEdge);
if (path.Count < 2)
{
return false;
}
var sourceNodeId = candidateEdge.SourceNodeId;
var targetNodeId = candidateEdge.TargetNodeId;
var currentBundle = peerEdges
.Append(candidateEdge)
.ToArray();
var currentTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes);
var currentSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentBundle, nodes);
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance);
var currentUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([candidateEdge], nodes);
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes);
var currentPathLength = ComputePathLength(path);
if (currentTargetJoinViolations == 0
&& currentSharedLaneViolations == 0
&& currentUnderNodeSegments == 0
&& currentUnderNodeViolations == 0)
{
return false;
}
var bestEdge = default(ElkRoutedEdge);
var bestTargetJoinViolations = currentTargetJoinViolations;
var bestSharedLaneViolations = currentSharedLaneViolations;
var bestUnderNodeSegments = currentUnderNodeSegments;
var bestUnderNodeViolations = currentUnderNodeViolations;
var bestLocalHardPressure = currentLocalHardPressure;
var bestPathLength = currentPathLength;
foreach (var candidatePath in EnumerateGatewayUnderNodePeerConflictCandidates(
path,
targetNode,
sourceNode,
peerEdges,
nodes,
sourceNodeId,
targetNodeId,
minLineClearance))
{
if (!PathChanged(path, candidatePath)
|| candidatePath.Count < 2
|| HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId)
|| !CanAcceptGatewayTargetRepair(candidatePath, targetNode)
|| !HasAcceptableGatewayBoundaryPath(candidatePath, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
{
continue;
}
var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath);
var localBundle = peerEdges
.Append(localCandidateEdge)
.ToArray();
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes);
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(localBundle, nodes);
var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance);
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([localCandidateEdge], nodes);
var candidateLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(localCandidateEdge, nodes);
var candidatePathLength = ComputePathLength(candidatePath);
if (!IsBetterGatewayUnderNodePeerConflictCandidate(
candidateTargetJoinViolations,
candidateSharedLaneViolations,
candidateUnderNodeSegments,
candidateUnderNodeViolations,
candidateLocalHardPressure,
candidatePathLength,
bestTargetJoinViolations,
bestSharedLaneViolations,
bestUnderNodeSegments,
bestUnderNodeViolations,
bestLocalHardPressure,
bestPathLength))
{
continue;
}
bestEdge = localCandidateEdge;
bestTargetJoinViolations = candidateTargetJoinViolations;
bestSharedLaneViolations = candidateSharedLaneViolations;
bestUnderNodeSegments = candidateUnderNodeSegments;
bestUnderNodeViolations = candidateUnderNodeViolations;
bestLocalHardPressure = candidateLocalHardPressure;
bestPathLength = candidatePathLength;
}
if (bestEdge is null)
{
return false;
}
polishedEdge = bestEdge;
return true;
}
private static bool TryPolishRectUnderNodeTargetPeerConflicts(
ElkRoutedEdge candidateEdge,
IReadOnlyList<ElkRoutedEdge> currentEdges,
int candidateIndex,
ElkPositionedNode[] nodes,
double minLineClearance,
out ElkRoutedEdge polishedEdge)
{
polishedEdge = candidateEdge;
if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId))
{
return false;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode)
|| ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return false;
}
var peerEdges = currentEdges
.Where((edge, index) =>
index != candidateIndex
&& string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal))
.ToArray();
if (peerEdges.Length == 0)
{
return false;
}
var currentBundle = peerEdges
.Append(candidateEdge)
.ToArray();
if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes) == 0)
{
return false;
}
var path = ExtractFullPath(candidateEdge);
if (path.Count < 2)
{
return false;
}
var sourceNodeId = candidateEdge.SourceNodeId;
var targetNodeId = candidateEdge.TargetNodeId;
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance);
var currentSide = ResolveTargetApproachSide(path, targetNode);
var bestScore = double.PositiveInfinity;
ElkRoutedEdge? bestEdge = null;
foreach (var side in EnumerateRectTargetPeerConflictSides(path, targetNode, currentSide))
{
var axisCandidates = EnumerateRectTargetPeerConflictAxes(path, targetNode, side, minLineClearance).ToArray();
if (axisCandidates.Length == 0)
{
continue;
}
var boundaryCoordinates = EnumerateRectTargetPeerConflictBoundaryCoordinates(path, targetNode, side).ToArray();
if (boundaryCoordinates.Length == 0)
{
continue;
}
foreach (var axis in axisCandidates)
{
foreach (var boundaryCoordinate in boundaryCoordinates)
{
var candidatePath = BuildMixedTargetFaceCandidate(path, targetNode, side, boundaryCoordinate, axis);
if (!PathChanged(path, candidatePath)
|| HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId)
|| HasTargetApproachBacktracking(candidatePath, targetNode)
|| !HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], targetNode))
{
continue;
}
var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance);
if (candidateUnderNodeSegments > currentUnderNodeSegments)
{
continue;
}
var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath);
var localBundle = peerEdges
.Append(localCandidateEdge)
.ToArray();
if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes) > 0)
{
continue;
}
var score = ComputeRectTargetPeerConflictPolishScore(candidatePath, currentSide, side);
if (score >= bestScore)
{
continue;
}
bestScore = score;
bestEdge = localCandidateEdge;
}
}
}
if (bestEdge is null)
{
return false;
}
polishedEdge = bestEdge;
return true;
}
private static IEnumerable<List<ElkPoint>> EnumerateGatewayUnderNodePeerConflictCandidates(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
ElkPositionedNode? sourceNode,
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minLineClearance)
{
foreach (var side in EnumerateGatewayUnderNodePeerConflictSides(path, targetNode, peerEdges))
{
var slotCoordinates = EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
path,
targetNode,
sourceNode,
peerEdges,
side,
minLineClearance)
.ToArray();
if (slotCoordinates.Length == 0)
{
continue;
}
foreach (var slotCoordinate in slotCoordinates)
{
if (sourceNode is not null
&& ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var bandBoundary)
&& TryBuildSafeHorizontalBandCandidate(
sourceNode,
targetNode,
nodes,
sourceNodeId,
targetNodeId,
path[0],
bandBoundary,
minLineClearance,
preferredSourceExterior: null,
out var bandCandidate))
{
yield return bandCandidate;
}
foreach (var axis in EnumerateGatewayUnderNodePeerConflictAxes(
path,
targetNode,
side,
nodes,
sourceNodeId,
targetNodeId,
minLineClearance))
{
yield return BuildMixedTargetFaceCandidate(path, targetNode, side, slotCoordinate, axis);
}
}
}
}
private static IEnumerable<string> EnumerateGatewayUnderNodePeerConflictSides(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkRoutedEdge> peerEdges)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var currentSide = ResolveTargetApproachSide(path, targetNode);
var peerSides = peerEdges
.Select(edge => ExtractFullPath(edge))
.Where(peerPath => peerPath.Count >= 2)
.Select(peerPath => ResolveTargetApproachSide(peerPath, targetNode))
.ToHashSet(StringComparer.Ordinal);
foreach (var side in new[] { "top", "bottom", "right", "left" })
{
if (!string.Equals(side, currentSide, StringComparison.Ordinal)
&& !peerSides.Contains(side)
&& seen.Add(side))
{
yield return side;
}
}
if (seen.Add(currentSide))
{
yield return currentSide;
}
foreach (var side in new[] { "top", "bottom", "right", "left" })
{
if (seen.Add(side))
{
yield return side;
}
}
}
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
ElkPositionedNode? sourceNode,
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
string side,
double minLineClearance)
{
var coordinates = new List<double>();
var inset = 10d;
var spacing = Math.Max(14d, minLineClearance + 6d);
var centerX = targetNode.X + (targetNode.Width / 2d);
var centerY = targetNode.Y + (targetNode.Height / 2d);
var slotMinimum = side is "left" or "right" ? targetNode.Y + inset : targetNode.X + inset;
var slotMaximum = side is "left" or "right"
? targetNode.Y + targetNode.Height - inset
: targetNode.X + targetNode.Width - inset;
void AddClamped(double value)
{
AddUniqueCoordinate(coordinates, Math.Max(slotMinimum, Math.Min(slotMaximum, value)));
}
if (side is "left" or "right")
{
AddClamped(path[^1].Y);
foreach (var peer in peerEdges)
{
var peerPath = ExtractFullPath(peer);
if (peerPath.Count > 0)
{
AddClamped(peerPath[^1].Y - spacing);
AddClamped(peerPath[^1].Y + spacing);
AddClamped(peerPath[^1].Y);
}
}
if (sourceNode is not null)
{
AddClamped(sourceNode.Y + (sourceNode.Height / 2d));
}
AddClamped(centerY - spacing);
AddClamped(centerY);
AddClamped(centerY + spacing);
}
else
{
AddClamped(path[^1].X);
foreach (var peer in peerEdges)
{
var peerPath = ExtractFullPath(peer);
if (peerPath.Count > 0)
{
AddClamped(peerPath[^1].X - spacing);
AddClamped(peerPath[^1].X + spacing);
AddClamped(peerPath[^1].X);
}
}
if (sourceNode is not null)
{
AddClamped(sourceNode.X + (sourceNode.Width / 2d));
}
AddClamped(centerX - spacing);
AddClamped(centerX);
AddClamped(centerX + spacing);
}
foreach (var coordinate in coordinates.Take(8))
{
yield return coordinate;
}
}
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictAxes(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minLineClearance)
{
var coordinates = new List<double>();
var currentAxis = ResolveTargetApproachAxisValue(path, side);
if (!double.IsNaN(currentAxis))
{
AddUniqueCoordinate(coordinates, currentAxis);
}
AddUniqueCoordinate(coordinates, ResolveDefaultTargetApproachAxis(targetNode, side));
var clearance = Math.Max(24d, minLineClearance * 0.6d);
if (side is "top" or "bottom")
{
var minX = Math.Min(path[0].X, targetNode.X);
var maxX = Math.Max(path[0].X, targetNode.X + targetNode.Width);
var blockers = nodes
.Where(node =>
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
&& maxX > node.X + 0.5d
&& minX < node.X + node.Width - 0.5d)
.ToArray();
if (side == "top")
{
var highestBlockerY = blockers.Length > 0
? blockers.Min(node => node.Y)
: Math.Min(path[0].Y, targetNode.Y);
AddUniqueCoordinate(coordinates, Math.Min(targetNode.Y - 8d, highestBlockerY - clearance));
}
else
{
var lowestBlockerY = blockers.Length > 0
? blockers.Max(node => node.Y + node.Height)
: Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
AddUniqueCoordinate(coordinates, Math.Max(targetNode.Y + targetNode.Height + 8d, lowestBlockerY + clearance));
}
}
else
{
var minY = Math.Min(path[0].Y, targetNode.Y);
var maxY = Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
var blockers = nodes
.Where(node =>
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
&& maxY > node.Y + 0.5d
&& minY < node.Y + node.Height - 0.5d)
.ToArray();
if (side == "left")
{
var leftmostBlockerX = blockers.Length > 0
? blockers.Min(node => node.X)
: Math.Min(path[0].X, targetNode.X);
AddUniqueCoordinate(coordinates, Math.Min(targetNode.X - 8d, leftmostBlockerX - clearance));
}
else
{
var rightmostBlockerX = blockers.Length > 0
? blockers.Max(node => node.X + node.Width)
: Math.Max(path[0].X, targetNode.X + targetNode.Width);
AddUniqueCoordinate(coordinates, Math.Max(targetNode.X + targetNode.Width + 8d, rightmostBlockerX + clearance));
}
}
foreach (var coordinate in coordinates.Take(6))
{
yield return coordinate;
}
}
private static bool IsBetterGatewayUnderNodePeerConflictCandidate(
int candidateTargetJoinViolations,
int candidateSharedLaneViolations,
int candidateUnderNodeSegments,
int candidateUnderNodeViolations,
int candidateLocalHardPressure,
double candidatePathLength,
int currentTargetJoinViolations,
int currentSharedLaneViolations,
int currentUnderNodeSegments,
int currentUnderNodeViolations,
int currentLocalHardPressure,
double currentPathLength)
{
if (candidateTargetJoinViolations != currentTargetJoinViolations)
{
return candidateTargetJoinViolations < currentTargetJoinViolations;
}
if (candidateUnderNodeViolations != currentUnderNodeViolations)
{
return candidateUnderNodeViolations < currentUnderNodeViolations;
}
if (candidateUnderNodeSegments != currentUnderNodeSegments)
{
return candidateUnderNodeSegments < currentUnderNodeSegments;
}
if (candidateSharedLaneViolations != currentSharedLaneViolations)
{
return candidateSharedLaneViolations < currentSharedLaneViolations;
}
if (candidateLocalHardPressure != currentLocalHardPressure)
{
return candidateLocalHardPressure < currentLocalHardPressure;
}
return candidatePathLength + 0.5d < currentPathLength;
}
private static IEnumerable<string> EnumerateRectTargetPeerConflictSides(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string currentSide)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
const double tolerance = 0.5d;
if (path.Any(point => point.Y < targetNode.Y - tolerance) && seen.Add("top"))
{
yield return "top";
}
if (path.Any(point => point.Y > targetNode.Y + targetNode.Height + tolerance) && seen.Add("bottom"))
{
yield return "bottom";
}
if (seen.Add(currentSide))
{
yield return currentSide;
}
}
private static IEnumerable<double> EnumerateRectTargetPeerConflictAxes(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double minLineClearance)
{
var coordinates = new List<double>();
var clearance = Math.Max(24d, minLineClearance * 0.6d);
const double tolerance = 0.5d;
switch (side)
{
case "top":
foreach (var value in path
.Select(point => point.Y)
.Where(coordinate => coordinate < targetNode.Y - tolerance)
.OrderByDescending(coordinate => coordinate))
{
AddUniqueCoordinate(coordinates, value);
}
AddUniqueCoordinate(coordinates, targetNode.Y - clearance);
break;
case "bottom":
foreach (var value in path
.Select(point => point.Y)
.Where(coordinate => coordinate > targetNode.Y + targetNode.Height + tolerance)
.OrderBy(coordinate => coordinate))
{
AddUniqueCoordinate(coordinates, value);
}
AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height + clearance);
break;
case "left":
foreach (var value in path
.Select(point => point.X)
.Where(coordinate => coordinate < targetNode.X - tolerance)
.OrderByDescending(coordinate => coordinate))
{
AddUniqueCoordinate(coordinates, value);
}
AddUniqueCoordinate(coordinates, targetNode.X - clearance);
break;
case "right":
foreach (var value in path
.Select(point => point.X)
.Where(coordinate => coordinate > targetNode.X + targetNode.Width + tolerance)
.OrderBy(coordinate => coordinate))
{
AddUniqueCoordinate(coordinates, value);
}
AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width + clearance);
break;
}
foreach (var coordinate in coordinates.Take(6))
{
yield return coordinate;
}
}
private static IEnumerable<double> EnumerateRectTargetPeerConflictBoundaryCoordinates(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side)
{
var coordinates = new List<double>();
var insetX = Math.Min(24d, Math.Max(8d, targetNode.Width / 4d));
var insetY = Math.Min(24d, Math.Max(8d, targetNode.Height / 4d));
if (side is "top" or "bottom")
{
var referenceX = path.Count > 1 ? path[^2].X : path[^1].X;
AddUniqueCoordinate(coordinates, referenceX);
AddUniqueCoordinate(coordinates, targetNode.X + insetX);
AddUniqueCoordinate(coordinates, targetNode.X + (targetNode.Width / 2d));
AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width - insetX);
foreach (var coordinate in coordinates
.OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.X + insetX, (targetNode.X + targetNode.Width) - insetX) - referenceX))
.Take(6))
{
yield return coordinate;
}
yield break;
}
var referenceY = path[^1].Y;
AddUniqueCoordinate(coordinates, referenceY);
AddUniqueCoordinate(coordinates, targetNode.Y + insetY);
AddUniqueCoordinate(coordinates, targetNode.Y + (targetNode.Height / 2d));
AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height - insetY);
foreach (var coordinate in coordinates
.OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.Y + insetY, (targetNode.Y + targetNode.Height) - insetY) - referenceY))
.Take(6))
{
yield return coordinate;
}
}
private static double ComputeRectTargetPeerConflictPolishScore(
IReadOnlyList<ElkPoint> candidatePath,
string currentSide,
string candidateSide)
{
var score = ComputePathLength(candidatePath)
+ (Math.Max(0, candidatePath.Count - 2) * 8d);
if (!string.Equals(currentSide, candidateSide, StringComparison.Ordinal))
{
score += 12d;
}
return score;
}
internal static ElkRoutedEdge[] SeparateSharedLaneConflicts(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var result = edges.ToArray();
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var nodeObstacles = nodes.Select(node => (
Left: node.X,
Top: node.Y,
Right: node.X + node.Width,
Bottom: node.Y + node.Height,
Id: node.Id)).ToArray();
var conflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(result, nodes)
.Where(conflict => restrictedSet is null
|| restrictedSet.Contains(conflict.LeftEdgeId)
|| restrictedSet.Contains(conflict.RightEdgeId))
.Distinct()
.ToArray();
foreach (var conflict in conflicts)
{
var leftIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.LeftEdgeId, StringComparison.Ordinal));
var rightIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.RightEdgeId, StringComparison.Ordinal));
if (leftIndex < 0 || rightIndex < 0)
{
continue;
}
var leftEdge = result[leftIndex];
var rightEdge = result[rightIndex];
if (TryResolveSharedLaneByPairedNodeHandoffSlotRepair(
result,
leftIndex,
leftEdge,
rightIndex,
rightEdge,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var pairedLeftEdge,
out var pairedRightEdge))
{
result[leftIndex] = pairedLeftEdge;
result[rightIndex] = pairedRightEdge;
continue;
}
var repairOrder = new[]
{
(Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftIndex : rightIndex,
Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightEdge : leftEdge),
(Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightIndex : leftIndex,
Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftEdge : rightEdge),
};
foreach (var repairCandidate in repairOrder)
{
if (TryResolveSharedLaneByAlternateRepeatFace(
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var alternateFaceEdge))
{
result[repairCandidate.Index] = alternateFaceEdge;
break;
}
if (TryResolveSharedLaneByDirectSourceSlotRepair(
result,
repairCandidate.Index,
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var directSourceSlotEdge))
{
result[repairCandidate.Index] = directSourceSlotEdge;
break;
}
if (TryResolveSharedLaneByDirectNodeHandoffSlotRepair(
result,
repairCandidate.Index,
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var directNodeHandoffEdge))
{
result[repairCandidate.Index] = directNodeHandoffEdge;
break;
}
if (TryResolveSharedLaneByFocusedSourceDepartureSpread(
result,
repairCandidate.Index,
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var sourceSpreadEdge))
{
result[repairCandidate.Index] = sourceSpreadEdge;
break;
}
if (TryResolveSharedLaneByFocusedMixedNodeFaceRepair(
result,
repairCandidate.Index,
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
out var mixedFaceEdge))
{
result[repairCandidate.Index] = mixedFaceEdge;
break;
}
if (!TrySeparateSharedLaneConflict(
result,
repairCandidate.Index,
result[repairCandidate.Index],
repairCandidate.Other,
nodes,
minLineClearance,
graphMinY,
graphMaxY,
nodeObstacles,
out var repairedEdge))
{
continue;
}
result[repairCandidate.Index] = repairedEdge;
break;
}
}
return result;
}
private static bool TryResolveSharedLaneByPairedNodeHandoffSlotRepair(
ElkRoutedEdge[] currentEdges,
int leftIndex,
ElkRoutedEdge leftEdge,
int rightIndex,
ElkRoutedEdge rightEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedLeftEdge,
out ElkRoutedEdge repairedRightEdge)
{
repairedLeftEdge = leftEdge;
repairedRightEdge = rightEdge;
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!TryResolveSharedLaneNodeHandoffContext(leftEdge, rightEdge, nodesById, graphMinY, graphMaxY, out var leftContext)
|| !TryResolveSharedLaneNodeHandoffContext(rightEdge, leftEdge, nodesById, graphMinY, graphMaxY, out var rightContext)
|| !string.Equals(leftContext.SharedNode.Id, rightContext.SharedNode.Id, StringComparison.Ordinal)
|| !string.Equals(leftContext.Side, rightContext.Side, StringComparison.Ordinal)
|| leftContext.IsOutgoing == rightContext.IsOutgoing)
{
return false;
}
var baselineConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes);
var baselineConflictCount = baselineConflicts.Count;
var baselineLeftConflictCount = baselineConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
var baselineRightConflictCount = baselineConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
var baselineCombinedPathLength = ComputePathLength(leftContext.Path) + ComputePathLength(rightContext.Path);
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
currentEdges,
leftContext.SharedNode,
leftContext.Side,
graphMinY,
graphMaxY,
leftEdge.Id);
var leftRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
leftContext.SharedNode,
leftContext.Side,
leftContext.CurrentBoundaryCoordinate,
peerCoordinates)
.ToArray();
var rightRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
rightContext.SharedNode,
rightContext.Side,
rightContext.CurrentBoundaryCoordinate,
peerCoordinates)
.ToArray();
ElkRoutedEdge? bestLeft = null;
ElkRoutedEdge? bestRight = null;
var bestConflictCount = baselineConflictCount;
var bestLeftConflictCount = baselineLeftConflictCount;
var bestRightConflictCount = baselineRightConflictCount;
var bestCombinedPathLength = baselineCombinedPathLength;
foreach (var leftCoordinate in leftRepairCoordinates)
{
var leftCandidatePath = leftContext.IsOutgoing
? BuildMixedSourceFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue)
: BuildMixedTargetFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue);
if (!IsValidSharedLaneBoundaryRepairCandidate(
leftEdge,
leftContext.Path,
leftCandidatePath,
leftContext.SharedNode,
leftContext.IsOutgoing,
nodes,
graphMinY,
graphMaxY))
{
continue;
}
foreach (var rightCoordinate in rightRepairCoordinates)
{
var rightCandidatePath = rightContext.IsOutgoing
? BuildMixedSourceFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue)
: BuildMixedTargetFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue);
if (!IsValidSharedLaneBoundaryRepairCandidate(
rightEdge,
rightContext.Path,
rightCandidatePath,
rightContext.SharedNode,
rightContext.IsOutgoing,
nodes,
graphMinY,
graphMaxY))
{
continue;
}
var candidateLeft = BuildSingleSectionEdge(leftEdge, leftCandidatePath);
var candidateRight = BuildSingleSectionEdge(rightEdge, rightCandidatePath);
if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateLeft, candidateRight], nodes).Count > 0
|| ComputeUnderNodeRepairLocalHardPressure(candidateLeft, nodes) > ComputeUnderNodeRepairLocalHardPressure(leftEdge, nodes)
|| ComputeUnderNodeRepairLocalHardPressure(candidateRight, nodes) > ComputeUnderNodeRepairLocalHardPressure(rightEdge, nodes))
{
continue;
}
var candidateEdges = currentEdges.ToArray();
candidateEdges[leftIndex] = candidateLeft;
candidateEdges[rightIndex] = candidateRight;
var candidateConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes);
var candidateConflictCount = candidateConflicts.Count;
var candidateLeftConflictCount = candidateConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
var candidateRightConflictCount = candidateConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
if (candidateConflictCount > bestConflictCount
|| candidateLeftConflictCount > bestLeftConflictCount
|| candidateRightConflictCount > bestRightConflictCount)
{
continue;
}
var candidateCombinedPathLength = ComputePathLength(leftCandidatePath) + ComputePathLength(rightCandidatePath);
var isBetter =
candidateConflictCount < bestConflictCount
|| candidateLeftConflictCount < bestLeftConflictCount
|| candidateRightConflictCount < bestRightConflictCount
|| candidateCombinedPathLength + 0.5d < bestCombinedPathLength;
if (!isBetter)
{
continue;
}
bestLeft = candidateLeft;
bestRight = candidateRight;
bestConflictCount = candidateConflictCount;
bestLeftConflictCount = candidateLeftConflictCount;
bestRightConflictCount = candidateRightConflictCount;
bestCombinedPathLength = candidateCombinedPathLength;
}
}
if (bestLeft is null || bestRight is null || bestConflictCount >= baselineConflictCount)
{
return false;
}
repairedLeftEdge = bestLeft;
repairedRightEdge = bestRight;
return true;
}
private static bool TryResolveSharedLaneByDirectSourceSlotRepair(
ElkRoutedEdge[] currentEdges,
int repairIndex,
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
if (string.IsNullOrWhiteSpace(edge.SourceNodeId)
|| !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
{
return false;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
{
return false;
}
var path = ExtractFullPath(edge);
var otherPath = ExtractFullPath(otherEdge);
if (path.Count < 2
|| otherPath.Count < 2
|| !ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|| !ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY))
{
return false;
}
var side = ResolveSourceDepartureSide(path, sourceNode);
var otherSide = ResolveSourceDepartureSide(otherPath, sourceNode);
if (!string.Equals(side, otherSide, StringComparison.Ordinal))
{
return false;
}
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
? side is "left" or "right"
? path[runEndIndex].X
: path[runEndIndex].Y
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
var currentBoundaryCoordinate = side is "left" or "right" ? path[0].Y : path[0].X;
var peerCoordinates = CollectSharedLaneSourceBoundaryCoordinates(
currentEdges,
sourceNode,
side,
graphMinY,
graphMaxY,
edge.Id);
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
sourceNode,
side,
currentBoundaryCoordinate,
peerCoordinates))
{
var candidatePath = BuildMixedSourceFaceCandidate(path, sourceNode, side, desiredCoordinate, axisValue);
if (!IsValidSharedLaneBoundaryRepairCandidate(
edge,
path,
candidatePath,
sourceNode,
isOutgoing: true,
nodes,
graphMinY,
graphMaxY))
{
continue;
}
var candidateEdges = currentEdges.ToArray();
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
if (TryAcceptFocusedSharedLanePairRepair(
currentEdges,
candidateEdges,
repairIndex,
edge,
otherEdge,
nodes,
graphMinY,
graphMaxY,
out repairedEdge))
{
return true;
}
}
return false;
}
private static bool TryResolveSharedLaneByDirectNodeHandoffSlotRepair(
ElkRoutedEdge[] currentEdges,
int repairIndex,
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!TryResolveSharedLaneNodeHandoffContext(edge, otherEdge, nodesById, graphMinY, graphMaxY, out var context))
{
return false;
}
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
currentEdges,
context.SharedNode,
context.Side,
graphMinY,
graphMaxY,
edge.Id);
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
context.SharedNode,
context.Side,
context.CurrentBoundaryCoordinate,
peerCoordinates))
{
var candidatePath = context.IsOutgoing
? BuildMixedSourceFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue)
: BuildMixedTargetFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue);
if (!IsValidSharedLaneBoundaryRepairCandidate(
edge,
context.Path,
candidatePath,
context.SharedNode,
context.IsOutgoing,
nodes,
graphMinY,
graphMaxY))
{
continue;
}
var candidateEdges = currentEdges.ToArray();
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
if (TryAcceptFocusedSharedLanePairRepair(
currentEdges,
candidateEdges,
repairIndex,
edge,
otherEdge,
nodes,
graphMinY,
graphMaxY,
out repairedEdge))
{
return true;
}
}
return false;
}
private static bool TryResolveSharedLaneByFocusedSourceDepartureSpread(
ElkRoutedEdge[] currentEdges,
int repairIndex,
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
if (string.IsNullOrWhiteSpace(edge.SourceNodeId)
|| !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
{
return false;
}
var focusedIds = new[] { edge.Id, otherEdge.Id };
var candidateEdges = SpreadSourceDepartureJoins(currentEdges, nodes, minLineClearance, focusedIds);
return TryAcceptFocusedSharedLanePairRepair(
currentEdges,
candidateEdges,
repairIndex,
edge,
otherEdge,
nodes,
graphMinY,
graphMaxY,
out repairedEdge);
}
private static bool TryResolveSharedLaneByFocusedMixedNodeFaceRepair(
ElkRoutedEdge[] currentEdges,
int repairIndex,
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
var sharesIncomingOutgoingNode =
(!string.IsNullOrWhiteSpace(edge.TargetNodeId)
&& string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
|| (!string.IsNullOrWhiteSpace(edge.SourceNodeId)
&& string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal));
if (!sharesIncomingOutgoingNode)
{
return false;
}
var focusedIds = new[] { edge.Id, otherEdge.Id };
var candidateEdges = SeparateMixedNodeFaceLaneConflicts(currentEdges, nodes, minLineClearance, focusedIds);
return TryAcceptFocusedSharedLanePairRepair(
currentEdges,
candidateEdges,
repairIndex,
edge,
otherEdge,
nodes,
graphMinY,
graphMaxY,
out repairedEdge);
}
private static bool TryAcceptFocusedSharedLanePairRepair(
ElkRoutedEdge[] currentEdges,
ElkRoutedEdge[] candidateEdges,
int repairIndex,
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
if (repairIndex < 0 || repairIndex >= candidateEdges.Length)
{
return false;
}
var candidateEdge = candidateEdges[repairIndex];
var currentPath = ExtractFullPath(edge);
var candidatePath = ExtractFullPath(candidateEdge);
if (!PathChanged(currentPath, candidatePath)
|| HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| SegmentLeavesGraphBand(candidatePath, graphMinY, graphMaxY))
{
return false;
}
var currentSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes);
var candidateSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes);
var currentSharedLaneCount = currentSharedLaneConflicts.Count;
var candidateSharedLaneCount = candidateSharedLaneConflicts.Count;
var currentBoundarySlotCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(currentEdges, nodes);
var candidateBoundarySlotCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidateEdges, nodes);
var currentEdgeSharedLaneCount = currentSharedLaneConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, edge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, edge.Id, StringComparison.Ordinal));
var candidateEdgeSharedLaneCount = candidateSharedLaneConflicts.Count(conflict =>
string.Equals(conflict.LeftEdgeId, candidateEdge.Id, StringComparison.Ordinal)
|| string.Equals(conflict.RightEdgeId, candidateEdge.Id, StringComparison.Ordinal));
var improvedSharedLanePressure = candidateSharedLaneCount < currentSharedLaneCount
|| candidateEdgeSharedLaneCount < currentEdgeSharedLaneCount;
// Shared-lane cleanup can require a temporary slot move on the same node face.
// Allow a bounded slot regression when we are strictly reducing the shared-lane
// pressure and the graph already has remaining boundary-slot debt for the later
// slot-restabilization pass to clean up.
var allowTemporaryBoundarySlotTrade =
improvedSharedLanePressure
&& currentSharedLaneCount > 0
&& currentBoundarySlotCount > 0
&& candidateBoundarySlotCount <= currentBoundarySlotCount + 1;
if (candidateSharedLaneCount > currentSharedLaneCount
|| (!allowTemporaryBoundarySlotTrade
&& candidateBoundarySlotCount > currentBoundarySlotCount)
|| candidateEdgeSharedLaneCount >= currentEdgeSharedLaneCount
|| ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateEdge, otherEdge], nodes).Count > 0
|| ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes) > ComputeUnderNodeRepairLocalHardPressure(edge, nodes))
{
return false;
}
repairedEdge = candidateEdge;
return true;
}
private static IReadOnlyList<double> CollectSharedLaneSourceBoundaryCoordinates(
IReadOnlyCollection<ElkRoutedEdge> edges,
ElkPositionedNode sourceNode,
string side,
double graphMinY,
double graphMaxY,
string excludeEdgeId)
{
var coordinates = new List<double>();
foreach (var peerEdge in edges)
{
if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal)
|| !string.Equals(peerEdge.SourceNodeId, sourceNode.Id, StringComparison.Ordinal)
|| !ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY))
{
continue;
}
var peerPath = ExtractFullPath(peerEdge);
if (peerPath.Count < 2)
{
continue;
}
var peerSide = ResolveSourceDepartureSide(peerPath, sourceNode);
if (!string.Equals(peerSide, side, StringComparison.Ordinal))
{
continue;
}
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X);
}
return coordinates
.OrderBy(value => value)
.ToArray();
}
private static IReadOnlyList<double> CollectSharedLaneNodeFaceBoundaryCoordinates(
IReadOnlyCollection<ElkRoutedEdge> edges,
ElkPositionedNode node,
string side,
double graphMinY,
double graphMaxY,
string excludeEdgeId)
{
var coordinates = new List<double>();
foreach (var peerEdge in edges)
{
if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal))
{
continue;
}
var peerPath = ExtractFullPath(peerEdge);
if (peerPath.Count < 2)
{
continue;
}
if (string.Equals(peerEdge.SourceNodeId, node.Id, StringComparison.Ordinal)
&& ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY))
{
var peerSide = ResolveSourceDepartureSide(peerPath, node);
if (string.Equals(peerSide, side, StringComparison.Ordinal))
{
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X);
}
}
if (string.Equals(peerEdge.TargetNodeId, node.Id, StringComparison.Ordinal)
&& ShouldSpreadTargetApproach(peerEdge, graphMinY, graphMaxY))
{
var peerSide = ResolveTargetApproachSide(peerPath, node);
if (string.Equals(peerSide, side, StringComparison.Ordinal))
{
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[^1].Y : peerPath[^1].X);
}
}
}
return coordinates
.OrderBy(value => value)
.ToArray();
}
private static IEnumerable<double> EnumerateSharedLaneBoundaryRepairCoordinates(
ElkPositionedNode node,
string side,
double currentCoordinate,
IReadOnlyList<double> peerCoordinates)
{
const double coordinateTolerance = 0.5d;
foreach (var coordinate in ElkBoundarySlots.BuildUniqueBoundarySlotCoordinates(
node,
side,
Math.Max(1, peerCoordinates.Count + 1))
.Where(value => Math.Abs(value - currentCoordinate) > coordinateTolerance)
.Select(value => new
{
Value = value,
Occupancy = peerCoordinates.Count(peer => Math.Abs(peer - value) <= coordinateTolerance),
})
.OrderBy(item => item.Occupancy)
.ThenBy(item => Math.Abs(item.Value - currentCoordinate))
.ThenBy(item => item.Value))
{
yield return coordinate.Value;
}
}
private static void AddUniquePathCandidate(
ICollection<IReadOnlyList<ElkPoint>> candidates,
IReadOnlyList<ElkPoint> candidate)
{
if (candidates.Any(existing =>
existing.Count == candidate.Count
&& existing.Zip(candidate, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)))
{
return;
}
candidates.Add(candidate);
}
private static bool IsBetterMixedNodeFaceCandidate(
int candidateSharedLaneViolations,
int candidateTargetJoinViolations,
int candidateBoundarySlotViolations,
int candidateBoundaryAngleViolations,
int candidateGatewaySourceExitViolations,
int candidateUnderNodeViolations,
double candidatePathLength,
int currentSharedLaneViolations,
int currentTargetJoinViolations,
int currentBoundarySlotViolations,
int currentBoundaryAngleViolations,
int currentGatewaySourceExitViolations,
int currentUnderNodeViolations,
double currentPathLength)
{
if (candidateSharedLaneViolations != currentSharedLaneViolations)
{
return candidateSharedLaneViolations < currentSharedLaneViolations;
}
if (candidateTargetJoinViolations != currentTargetJoinViolations)
{
return candidateTargetJoinViolations < currentTargetJoinViolations;
}
if (candidateBoundarySlotViolations != currentBoundarySlotViolations)
{
return candidateBoundarySlotViolations < currentBoundarySlotViolations;
}
if (candidateBoundaryAngleViolations != currentBoundaryAngleViolations)
{
return candidateBoundaryAngleViolations < currentBoundaryAngleViolations;
}
if (candidateGatewaySourceExitViolations != currentGatewaySourceExitViolations)
{
return candidateGatewaySourceExitViolations < currentGatewaySourceExitViolations;
}
if (candidateUnderNodeViolations != currentUnderNodeViolations)
{
return candidateUnderNodeViolations < currentUnderNodeViolations;
}
return candidatePathLength + 0.5d < currentPathLength;
}
private static bool TryResolveSharedLaneNodeHandoffContext(
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
double graphMinY,
double graphMaxY,
out (ElkPositionedNode SharedNode, string Side, bool IsOutgoing, IReadOnlyList<ElkPoint> Path, double CurrentBoundaryCoordinate, double AxisValue) context)
{
context = default;
var path = ExtractFullPath(edge);
var otherPath = ExtractFullPath(otherEdge);
if (path.Count < 2 || otherPath.Count < 2)
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.TargetNodeId)
&& string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal)
&& nodesById.TryGetValue(edge.TargetNodeId, out var incomingTargetNode)
&& ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY)
&& ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY))
{
var incomingSide = ResolveTargetApproachSide(path, incomingTargetNode);
var outgoingSide = ResolveSourceDepartureSide(otherPath, incomingTargetNode);
if (string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal))
{
var axisValue = ResolveTargetApproachAxisValue(path, incomingSide);
if (double.IsNaN(axisValue))
{
axisValue = ResolveDefaultTargetApproachAxis(incomingTargetNode, incomingSide);
}
context = (
incomingTargetNode,
incomingSide,
IsOutgoing: false,
path,
incomingSide is "left" or "right" ? path[^1].Y : path[^1].X,
axisValue);
return true;
}
}
if (!string.IsNullOrWhiteSpace(edge.SourceNodeId)
&& string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal)
&& nodesById.TryGetValue(edge.SourceNodeId, out var outgoingSourceNode)
&& ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
&& ShouldSpreadTargetApproach(otherEdge, graphMinY, graphMaxY))
{
var outgoingSide = ResolveSourceDepartureSide(path, outgoingSourceNode);
var incomingSide = ResolveTargetApproachSide(otherPath, outgoingSourceNode);
if (string.Equals(outgoingSide, incomingSide, StringComparison.Ordinal))
{
var axisValue = TryExtractSourceDepartureRun(path, outgoingSide, out _, out var runEndIndex)
? outgoingSide is "left" or "right"
? path[runEndIndex].X
: path[runEndIndex].Y
: ResolveDefaultSourceDepartureAxis(outgoingSourceNode, outgoingSide);
context = (
outgoingSourceNode,
outgoingSide,
IsOutgoing: true,
path,
outgoingSide is "left" or "right" ? path[0].Y : path[0].X,
axisValue);
return true;
}
}
return false;
}
private static bool IsValidSharedLaneBoundaryRepairCandidate(
ElkRoutedEdge edge,
IReadOnlyList<ElkPoint> currentPath,
IReadOnlyList<ElkPoint> candidatePath,
ElkPositionedNode node,
bool isOutgoing,
IReadOnlyCollection<ElkPositionedNode> nodes,
double graphMinY,
double graphMaxY)
{
if (!PathChanged(currentPath, candidatePath)
|| HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| WorsensGraphBandDeparture(currentPath, candidatePath, graphMinY, graphMaxY))
{
return false;
}
if (isOutgoing)
{
if (ElkShapeBoundaries.IsGatewayShape(node))
{
return HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: true);
}
return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 2)
&& HasValidBoundaryAngle(candidatePath[0], candidatePath[1], node);
}
if (ElkShapeBoundaries.IsGatewayShape(node))
{
return CanAcceptGatewayTargetRepair(candidatePath, node)
&& HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: false);
}
return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4)
&& HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], node)
&& !HasTargetApproachBacktracking(candidatePath, node);
}
private static bool IsAcceptableStrictBoundarySlotCandidate(
ElkRoutedEdge edge,
IReadOnlyList<ElkPoint> currentPath,
IReadOnlyList<ElkPoint> candidatePath,
ElkPositionedNode node,
bool isOutgoing,
IReadOnlyCollection<ElkPositionedNode> nodes,
double graphMinY,
double graphMaxY)
{
if (!PathChanged(currentPath, candidatePath)
|| HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| WorsensGraphBandDeparture(currentPath, candidatePath, graphMinY, graphMaxY))
{
return false;
}
if (isOutgoing)
{
if (ElkShapeBoundaries.IsGatewayShape(node))
{
return HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: true);
}
return candidatePath.Count >= 2
&& HasValidBoundaryAngle(candidatePath[0], candidatePath[1], node);
}
if (ElkShapeBoundaries.IsGatewayShape(node))
{
return CanAcceptGatewayTargetRepair(candidatePath, node)
&& HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: false);
}
return candidatePath.Count >= 2
&& HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], node)
&& !HasTargetApproachBacktracking(candidatePath, node);
}
private static bool HasDisallowedGatewaySourceSlotIssue(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyList<ElkPoint> candidatePath,
ElkPositionedNode sourceNode)
{
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
return false;
}
var allowsSaturatedAlternateFace = ShouldAllowSaturatedGatewaySourceAlternateFace(
edge,
edges,
sourceNode,
candidatePath);
return HasGatewaySourceExitBacktracking(candidatePath)
|| HasGatewaySourceExitCurl(candidatePath)
|| (!allowsSaturatedAlternateFace && HasGatewaySourceDominantAxisDetour(candidatePath, sourceNode))
|| (!allowsSaturatedAlternateFace && HasGatewaySourcePreferredFaceMismatch(candidatePath, sourceNode))
|| (!allowsSaturatedAlternateFace && NeedsDecisionSourcePreferredFaceRepair(candidatePath, sourceNode));
}
private static bool TryResolveSharedLaneByAlternateRepeatFace(
ElkRoutedEdge edge,
ElkRoutedEdge otherEdge,
ElkPositionedNode[] nodes,
double minLineClearance,
double graphMinY,
double graphMaxY,
out ElkRoutedEdge repairedEdge)
{
repairedEdge = edge;
if (!IsRepeatCollectorLabel(edge.Label))
{
return false;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|| !nodesById.TryGetValue(otherEdge.SourceNodeId ?? string.Empty, out var sourceNode)
|| !string.Equals(targetNode.Id, sourceNode.Id, StringComparison.Ordinal)
|| ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return false;
}
var path = ExtractFullPath(edge);
var otherPath = ExtractFullPath(otherEdge);
if (path.Count < 2 || otherPath.Count < 2)
{
return false;
}
var incomingSide = ResolveTargetApproachSide(path, targetNode);
var outgoingSide = ResolveSourceDepartureSide(otherPath, sourceNode);
if (!string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal))
{
return false;
}
var axisValue = ResolveTargetApproachAxisValue(path, incomingSide);
if (double.IsNaN(axisValue))
{
axisValue = incomingSide is "left" or "right" ? path[^1].Y : path[^1].X;
}
var incomingEntry = (
Index: 0,
Edge: edge,
Path: (IReadOnlyList<ElkPoint>)path,
Node: targetNode,
Side: incomingSide,
IsOutgoing: false,
Boundary: path[^1],
BoundaryCoordinate: incomingSide is "left" or "right" ? path[^1].Y : path[^1].X,
AxisValue: axisValue);
if (!TryBuildAlternateMixedFaceCandidate(incomingEntry, nodes, minLineClearance, out var candidate)
|| !PathChanged(path, candidate)
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY)
|| !HasClearBoundarySegments(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4)
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
{
return false;
}
repairedEdge = BuildSingleSectionEdge(edge, candidate);
var repairedPath = ExtractFullPath(repairedEdge);
if (!HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
&& !SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)
&& ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count == 0)
{
return true;
}
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
[repairedEdge],
nodes,
minLineClearance)[0];
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
repairedPath = ExtractFullPath(repairedEdge);
if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|| SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)
|| ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count > 0)
{
repairedEdge = edge;
return false;
}
return true;
}
}