Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterHighway.Groups.cs

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);
}