Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkEdgePostProcessorSimplify
|
||||
internal static partial class ElkEdgePostProcessorSimplify
|
||||
{
|
||||
internal static ElkRoutedEdge[] SimplifyEdgePaths(
|
||||
ElkRoutedEdge[] edges,
|
||||
@@ -108,341 +108,4 @@ internal static class ElkEdgePostProcessorSimplify
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static ElkRoutedEdge[] TightenOuterCorridors(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (nodes.Length == 0) return edges;
|
||||
|
||||
var graphMinY = nodes.Min(n => n.Y);
|
||||
var graphMaxY = nodes.Max(n => n.Y + n.Height);
|
||||
var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
|
||||
var minLineClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d
|
||||
: 50d;
|
||||
var minMargin = Math.Max(12d, minLineClearance + 4d);
|
||||
var laneGap = Math.Max(8d, minLineClearance + 4d);
|
||||
|
||||
var outerEdges = new List<(int Index, double CorridorY, bool IsAbove)>();
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
var aboveYs = new List<double>();
|
||||
var belowYs = new List<double>();
|
||||
foreach (var section in edges[i].Sections)
|
||||
{
|
||||
foreach (var bp in section.BendPoints)
|
||||
{
|
||||
if (bp.Y < graphMinY - 8d)
|
||||
{
|
||||
aboveYs.Add(bp.Y);
|
||||
}
|
||||
else if (bp.Y > graphMaxY + 8d)
|
||||
{
|
||||
belowYs.Add(bp.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aboveYs.Count > 0)
|
||||
{
|
||||
outerEdges.Add((i, aboveYs.Min(), true));
|
||||
}
|
||||
|
||||
if (belowYs.Count > 0)
|
||||
{
|
||||
outerEdges.Add((i, belowYs.Max(), false));
|
||||
}
|
||||
}
|
||||
|
||||
if (outerEdges.Count == 0) return edges;
|
||||
|
||||
NormalizeCorridorYValues(outerEdges, edges, graphMinY, graphMaxY);
|
||||
|
||||
var aboveLanes = outerEdges.Where(e => e.IsAbove)
|
||||
.GroupBy(e => Math.Round(e.CorridorY, 1))
|
||||
.OrderBy(g => g.Key)
|
||||
.ToArray();
|
||||
var belowLanes = outerEdges.Where(e => !e.IsAbove)
|
||||
.GroupBy(e => Math.Round(e.CorridorY, 1))
|
||||
.OrderByDescending(g => g.Key)
|
||||
.ToArray();
|
||||
|
||||
var result = edges.ToArray();
|
||||
var shifts = new Dictionary<int, double>();
|
||||
|
||||
for (var lane = 0; lane < aboveLanes.Length; lane++)
|
||||
{
|
||||
var targetY = graphMinY - minMargin - (lane * laneGap);
|
||||
var currentY = aboveLanes[lane].Key;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) > 2d)
|
||||
{
|
||||
foreach (var entry in aboveLanes[lane])
|
||||
{
|
||||
shifts[entry.Index] = shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var lane = 0; lane < belowLanes.Length; lane++)
|
||||
{
|
||||
var targetY = graphMaxY + minMargin + (lane * laneGap);
|
||||
var currentY = belowLanes[lane].Key;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) > 2d)
|
||||
{
|
||||
foreach (var entry in belowLanes[lane])
|
||||
{
|
||||
shifts[entry.Index] = shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (edgeIndex, shift) in shifts)
|
||||
{
|
||||
var edge = result[edgeIndex];
|
||||
var boundary = shift > 0 ? graphMaxY : graphMinY;
|
||||
var newSections = new List<ElkEdgeSection>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var newBendPoints = section.BendPoints.Select(bp =>
|
||||
{
|
||||
if ((shift < 0 && bp.Y < graphMinY - 4d) || (shift > 0 && bp.Y > graphMaxY + 4d)
|
||||
|| (shift > 0 && bp.Y < graphMinY - 4d) || (shift < 0 && bp.Y > graphMaxY + 4d))
|
||||
{
|
||||
return new ElkPoint { X = bp.X, Y = bp.Y + shift };
|
||||
}
|
||||
return bp;
|
||||
}).ToArray();
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = section.StartPoint,
|
||||
EndPoint = section.EndPoint,
|
||||
BendPoints = newBendPoints,
|
||||
});
|
||||
}
|
||||
|
||||
result[edgeIndex] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections = newSections,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool SegmentClearsObstacles(
|
||||
ElkPoint p1, ElkPoint p2,
|
||||
(double L, double T, double R, double B, string Id)[] obstacles,
|
||||
HashSet<string> excludeIds)
|
||||
{
|
||||
var isH = Math.Abs(p1.Y - p2.Y) < 1d;
|
||||
var isV = Math.Abs(p1.X - p2.X) < 1d;
|
||||
if (!isH && !isV) return true;
|
||||
|
||||
foreach (var ob in obstacles)
|
||||
{
|
||||
if (excludeIds.Contains(ob.Id)) continue;
|
||||
if (isH && p1.Y > ob.T && p1.Y < ob.B)
|
||||
{
|
||||
if (Math.Max(p1.X, p2.X) > ob.L && Math.Min(p1.X, p2.X) < ob.R) return false;
|
||||
}
|
||||
else if (isV && p1.X > ob.L && p1.X < ob.R)
|
||||
{
|
||||
if (Math.Max(p1.Y, p2.Y) > ob.T && Math.Min(p1.Y, p2.Y) < ob.B) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryApplyOrthogonalShortcut(
|
||||
List<ElkPoint> points,
|
||||
(double L, double T, double R, double B, string Id)[] obstacles,
|
||||
HashSet<string> excludeIds)
|
||||
{
|
||||
if (points.Count < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var startIndex = 0; startIndex < points.Count - 2; startIndex++)
|
||||
{
|
||||
for (var endIndex = points.Count - 1; endIndex >= startIndex + 2; endIndex--)
|
||||
{
|
||||
var start = points[startIndex];
|
||||
var end = points[endIndex];
|
||||
var existingLength = ComputeSubpathLength(points, startIndex, endIndex);
|
||||
|
||||
foreach (var shortcut in BuildShortcutCandidates(start, end))
|
||||
{
|
||||
if (shortcut.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ShortcutClearsObstacles(shortcut, obstacles, excludeIds))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var shortcutLength = ComputePathLength(shortcut);
|
||||
if (shortcutLength >= existingLength - 8d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
points.RemoveRange(startIndex + 1, endIndex - startIndex - 1);
|
||||
if (shortcut.Count > 2)
|
||||
{
|
||||
points.InsertRange(startIndex + 1, shortcut.Skip(1).Take(shortcut.Count - 2));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<List<ElkPoint>> BuildShortcutCandidates(ElkPoint start, ElkPoint end)
|
||||
{
|
||||
var candidates = new List<List<ElkPoint>>();
|
||||
if (Math.Abs(start.X - end.X) < 1d || Math.Abs(start.Y - end.Y) < 1d)
|
||||
{
|
||||
candidates.Add([start, end]);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var corner1 = new ElkPoint { X = start.X, Y = end.Y };
|
||||
var corner2 = new ElkPoint { X = end.X, Y = start.Y };
|
||||
candidates.Add([start, corner1, end]);
|
||||
candidates.Add([start, corner2, end]);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static bool ShortcutClearsObstacles(
|
||||
IReadOnlyList<ElkPoint> shortcut,
|
||||
(double L, double T, double R, double B, string Id)[] obstacles,
|
||||
HashSet<string> excludeIds)
|
||||
{
|
||||
for (var i = 0; i < shortcut.Count - 1; i++)
|
||||
{
|
||||
if (!SegmentClearsObstacles(shortcut[i], shortcut[i + 1], obstacles, excludeIds))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static double ComputeSubpathLength(IReadOnlyList<ElkPoint> points, int startIndex, int endIndex)
|
||||
{
|
||||
var length = 0d;
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static double ComputePathLength(IReadOnlyList<ElkPoint> points)
|
||||
{
|
||||
var length = 0d;
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static void NormalizeCorridorYValues(
|
||||
List<(int Index, double CorridorY, bool IsAbove)> outerEdges,
|
||||
ElkRoutedEdge[] edges,
|
||||
double graphMinY, double graphMaxY)
|
||||
{
|
||||
const double mergeThreshold = 6d;
|
||||
var groups = new List<List<int>>();
|
||||
var sorted = outerEdges.OrderBy(e => e.CorridorY).ToArray();
|
||||
foreach (var entry in sorted)
|
||||
{
|
||||
var merged = false;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var groupY = outerEdges[group[0]].CorridorY;
|
||||
if (Math.Abs(entry.CorridorY - groupY) <= mergeThreshold && entry.IsAbove == outerEdges[group[0]].IsAbove)
|
||||
{
|
||||
group.Add(outerEdges.IndexOf(entry));
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!merged)
|
||||
{
|
||||
groups.Add([outerEdges.IndexOf(entry)]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetY = outerEdges[group[0]].CorridorY;
|
||||
for (var gi = 1; gi < group.Count; gi++)
|
||||
{
|
||||
var idx = group[gi];
|
||||
var edgeIndex = outerEdges[idx].Index;
|
||||
var currentY = outerEdges[idx].CorridorY;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) < 0.5d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var edge = edges[edgeIndex];
|
||||
var newSections = new List<ElkEdgeSection>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var newBendPoints = section.BendPoints.Select(bp =>
|
||||
{
|
||||
if (Math.Abs(bp.Y - currentY) < 2d)
|
||||
{
|
||||
return new ElkPoint { X = bp.X, Y = targetY };
|
||||
}
|
||||
|
||||
return bp;
|
||||
}).ToArray();
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = section.StartPoint,
|
||||
EndPoint = section.EndPoint,
|
||||
BendPoints = newBendPoints,
|
||||
});
|
||||
}
|
||||
|
||||
edges[edgeIndex] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections = newSections,
|
||||
};
|
||||
outerEdges[idx] = (edgeIndex, targetY, outerEdges[idx].IsAbove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user