195 lines
6.5 KiB
C#
195 lines
6.5 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgeRouterHighway
|
|
{
|
|
private static Dictionary<string, List<int>> BuildTargetSideGroups(
|
|
IReadOnlyList<ElkRoutedEdge> edges,
|
|
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
var edgesByTargetSide = new Dictionary<string, List<int>>(StringComparer.Ordinal);
|
|
for (var i = 0; i < edges.Count; i++)
|
|
{
|
|
var edge = edges[i];
|
|
if (!ShouldProcessEdge(edge, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
var key = $"{targetNode.Id}|{side}";
|
|
if (!edgesByTargetSide.TryGetValue(key, out var list))
|
|
{
|
|
list = [];
|
|
edgesByTargetSide[key] = list;
|
|
}
|
|
|
|
list.Add(i);
|
|
}
|
|
|
|
return edgesByTargetSide;
|
|
}
|
|
|
|
private static GroupEvaluation? EvaluateTargetSideGroup(
|
|
IReadOnlyList<ElkRoutedEdge> edges,
|
|
IReadOnlyList<int> edgeIndices,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double minLineClearance)
|
|
{
|
|
var members = edgeIndices
|
|
.Select(index => CreateMember(edges[index], index, side))
|
|
.Where(member => member.Path.Count >= 2)
|
|
.OrderBy(member => member.EdgeId, StringComparer.Ordinal)
|
|
.ToList();
|
|
if (members.Count < 2)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var pairMetrics = ComputePairMetrics(members);
|
|
var actualGap = ComputeMinEndpointGap(members);
|
|
var requiredGap = ElkBoundarySlots.ResolveRequiredBoundarySlotGap(
|
|
targetNode,
|
|
side,
|
|
members.Count,
|
|
minLineClearance);
|
|
var requiresSpread = (actualGap + CoordinateTolerance) < requiredGap
|
|
&& !pairMetrics.AllPairsApplicable;
|
|
if (!requiresSpread && pairMetrics.ShortestSharedRatio < MinHighwayRatio)
|
|
{
|
|
requiresSpread = pairMetrics.HasSharedSegment;
|
|
}
|
|
|
|
var diagnostic = new ElkHighwayDiagnostics
|
|
{
|
|
TargetNodeId = targetNode.Id,
|
|
SharedAxis = side,
|
|
SharedCoord = Math.Round(members.Average(member => member.EndpointCoord), 1),
|
|
EdgeIds = members.Select(member => member.EdgeId).ToArray(),
|
|
MinRatio = pairMetrics.HasSharedSegment
|
|
? Math.Round(pairMetrics.ShortestSharedRatio, 3)
|
|
: 0d,
|
|
WasBroken = requiresSpread,
|
|
Reason = requiresSpread
|
|
? pairMetrics.HasSharedSegment && pairMetrics.ShortestSharedRatio < MinHighwayRatio
|
|
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} < {MinHighwayRatio:F2}"
|
|
: $"gap {actualGap:F0}px < required gap {requiredGap:F0}px"
|
|
: pairMetrics.AllPairsApplicable
|
|
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} >= {MinHighwayRatio:F2}"
|
|
: $"gap {actualGap:F0}px >= required gap {requiredGap:F0}px",
|
|
};
|
|
|
|
return new GroupEvaluation(members, diagnostic);
|
|
}
|
|
|
|
private static HighwayPairMetrics ComputePairMetrics(IReadOnlyList<HighwayMember> members)
|
|
{
|
|
var shortestSharedRatio = double.MaxValue;
|
|
var hasSharedSegment = false;
|
|
var allPairsApplicable = true;
|
|
|
|
for (var i = 0; i < members.Count; i++)
|
|
{
|
|
for (var j = i + 1; j < members.Count; j++)
|
|
{
|
|
var sharedLength = ElkEdgeRoutingGeometry.ComputeLongestSharedApproachSegmentLength(
|
|
members[i].Path,
|
|
members[j].Path);
|
|
if (sharedLength <= 1d)
|
|
{
|
|
allPairsApplicable = false;
|
|
continue;
|
|
}
|
|
|
|
hasSharedSegment = true;
|
|
var shortestPath = Math.Min(members[i].PathLength, members[j].PathLength);
|
|
if (shortestPath <= 1d)
|
|
{
|
|
allPairsApplicable = false;
|
|
continue;
|
|
}
|
|
|
|
var ratio = sharedLength / shortestPath;
|
|
shortestSharedRatio = Math.Min(shortestSharedRatio, ratio);
|
|
if (ratio < MinHighwayRatio)
|
|
{
|
|
allPairsApplicable = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new HighwayPairMetrics(
|
|
HasSharedSegment: hasSharedSegment,
|
|
AllPairsApplicable: allPairsApplicable && hasSharedSegment,
|
|
ShortestSharedRatio: hasSharedSegment ? shortestSharedRatio : 0d);
|
|
}
|
|
|
|
private static double ComputeMinEndpointGap(IReadOnlyList<HighwayMember> members)
|
|
{
|
|
var coords = members
|
|
.Select(member => member.EndpointCoord)
|
|
.OrderBy(value => value)
|
|
.ToArray();
|
|
if (coords.Length < 2)
|
|
{
|
|
return double.MaxValue;
|
|
}
|
|
|
|
var minGap = double.MaxValue;
|
|
for (var i = 1; i < coords.Length; i++)
|
|
{
|
|
minGap = Math.Min(minGap, coords[i] - coords[i - 1]);
|
|
}
|
|
|
|
return minGap;
|
|
}
|
|
|
|
private static HighwayMember CreateMember(ElkRoutedEdge edge, int index, string side)
|
|
{
|
|
var path = ExtractFullPath(edge);
|
|
var endpointCoord = side is "left" or "right"
|
|
? path[^1].Y
|
|
: path[^1].X;
|
|
return new HighwayMember(
|
|
Index: index,
|
|
EdgeId: edge.Id,
|
|
Path: path,
|
|
PathLength: ElkEdgeRoutingGeometry.ComputePathLength(edge),
|
|
EndpointCoord: endpointCoord);
|
|
}
|
|
|
|
private readonly record struct HighwayMember(
|
|
int Index,
|
|
string EdgeId,
|
|
List<ElkPoint> Path,
|
|
double PathLength,
|
|
double EndpointCoord);
|
|
|
|
private readonly record struct HighwayPairMetrics(
|
|
bool HasSharedSegment,
|
|
bool AllPairsApplicable,
|
|
double ShortestSharedRatio);
|
|
|
|
private readonly record struct GroupEvaluation(
|
|
IReadOnlyList<HighwayMember> Members,
|
|
ElkHighwayDiagnostics Diagnostic);
|
|
}
|