Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static List<ElkPoint>? TryBuildLocalObstacleSkirtPath(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string sourceId,
|
||||
string targetId,
|
||||
ElkPositionedNode? targetNode,
|
||||
double obstaclePadding)
|
||||
{
|
||||
var obstacles = nodes.Select(node => (
|
||||
Left: node.X - obstaclePadding,
|
||||
Top: node.Y - obstaclePadding,
|
||||
Right: node.X + node.Width + obstaclePadding,
|
||||
Bottom: node.Y + node.Height + obstaclePadding,
|
||||
Id: node.Id)).ToArray();
|
||||
List<ElkPoint>? bestPath = null;
|
||||
var bestScore = double.MaxValue;
|
||||
|
||||
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
|
||||
!ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, obstacles, sourceId, targetId);
|
||||
|
||||
void ConsiderCandidate(IReadOnlyList<ElkPoint> rawCandidate)
|
||||
{
|
||||
var candidate = NormalizePolyline(rawCandidate);
|
||||
if (candidate.Count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 1; i < candidate.Count; i++)
|
||||
{
|
||||
if (!SegmentIsClear(candidate[i - 1], candidate[i]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetNode is not null
|
||||
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
||||
&& HasTargetApproachBacktracking(candidate, targetNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var score = ComputePolylineLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d);
|
||||
if (score >= bestScore - 0.5d)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestPath = candidate;
|
||||
}
|
||||
|
||||
var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y);
|
||||
if (horizontalDominant)
|
||||
{
|
||||
var targetBridgeX = end.X;
|
||||
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
targetBridgeX = ResolveGatewayRoutingApproachPoint(targetNode, start, end).X;
|
||||
}
|
||||
|
||||
var minX = Math.Min(start.X, end.X) + 0.5d;
|
||||
var maxX = Math.Max(start.X, end.X) - 0.5d;
|
||||
var corridorTop = Math.Min(start.Y, end.Y) - obstaclePadding;
|
||||
var corridorBottom = Math.Max(start.Y, end.Y) + obstaclePadding;
|
||||
var bypassYCandidates = new List<double> { start.Y, end.Y };
|
||||
foreach (var obstacle in obstacles)
|
||||
{
|
||||
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|
||||
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|
||||
|| obstacle.Right <= minX
|
||||
|| obstacle.Left >= maxX
|
||||
|| obstacle.Bottom <= corridorTop
|
||||
|| obstacle.Top >= corridorBottom)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddUniqueCoordinate(bypassYCandidates, obstacle.Top);
|
||||
AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom);
|
||||
}
|
||||
|
||||
foreach (var bypassY in bypassYCandidates)
|
||||
{
|
||||
ConsiderCandidate(
|
||||
[
|
||||
start,
|
||||
new ElkPoint { X = start.X, Y = bypassY },
|
||||
new ElkPoint { X = targetBridgeX, Y = bypassY },
|
||||
end,
|
||||
]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetBridgeY = end.Y;
|
||||
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
targetBridgeY = ResolveGatewayRoutingApproachPoint(targetNode, start, end).Y;
|
||||
}
|
||||
|
||||
var minY = Math.Min(start.Y, end.Y) + 0.5d;
|
||||
var maxY = Math.Max(start.Y, end.Y) - 0.5d;
|
||||
var corridorLeft = Math.Min(start.X, end.X) - obstaclePadding;
|
||||
var corridorRight = Math.Max(start.X, end.X) + obstaclePadding;
|
||||
var bypassXCandidates = new List<double> { start.X, end.X };
|
||||
foreach (var obstacle in obstacles)
|
||||
{
|
||||
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|
||||
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|
||||
|| obstacle.Bottom <= minY
|
||||
|| obstacle.Top >= maxY
|
||||
|| obstacle.Right <= corridorLeft
|
||||
|| obstacle.Left >= corridorRight)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddUniqueCoordinate(bypassXCandidates, obstacle.Left);
|
||||
AddUniqueCoordinate(bypassXCandidates, obstacle.Right);
|
||||
}
|
||||
|
||||
foreach (var bypassX in bypassXCandidates)
|
||||
{
|
||||
ConsiderCandidate(
|
||||
[
|
||||
start,
|
||||
new ElkPoint { X = bypassX, Y = start.Y },
|
||||
new ElkPoint { X = bypassX, Y = targetBridgeY },
|
||||
end,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return bestPath;
|
||||
}
|
||||
|
||||
private static double ComputePolylineLength(IReadOnlyList<ElkPoint> points)
|
||||
{
|
||||
var length = 0d;
|
||||
for (var i = 1; i < points.Count; i++)
|
||||
{
|
||||
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> nodes)
|
||||
{
|
||||
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
||||
return serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
||||
: 50d;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user