3400 lines
141 KiB
C#
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;
|
|
}
|
|
|
|
} |