Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.Eligibility.cs

271 lines
9.4 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool CanRepairEdgeLocally(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes,
double graphMinY,
double graphMaxY)
{
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
return true;
}
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
{
return false;
}
return ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)
&& HasClearOrthogonalShortcut(edge, nodes);
}
private static bool CanRouteSelectedRepairEdge(
ElkRoutedEdge edge,
double graphMinY,
double graphMaxY,
IReadOnlySet<string> routeRepairEdgeIds)
{
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
return true;
}
if (!routeRepairEdgeIds.Contains(edge.Id))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
&& !ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
}
private static bool HasClearOrthogonalShortcut(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var firstSection = edge.Sections.FirstOrDefault();
var lastSection = edge.Sections.LastOrDefault();
if (firstSection is null || lastSection is null)
{
return false;
}
var start = firstSection.StartPoint;
var end = lastSection.EndPoint;
var obstacles = nodes.Select(node => (
Left: node.X,
Top: node.Y,
Right: node.X + node.Width,
Bottom: node.Y + node.Height,
Id: node.Id)).ToArray();
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
!ElkEdgePostProcessor.SegmentCrossesObstacle(
from,
to,
obstacles,
edge.SourceNodeId,
edge.TargetNodeId);
if (Math.Abs(start.X - end.X) < 2d || Math.Abs(start.Y - end.Y) < 2d)
{
return SegmentIsClear(start, end);
}
var horizontalThenVertical = new ElkPoint { X = end.X, Y = start.Y };
if (SegmentIsClear(start, horizontalThenVertical) && SegmentIsClear(horizontalThenVertical, end))
{
return true;
}
var verticalThenHorizontal = new ElkPoint { X = start.X, Y = end.Y };
return SegmentIsClear(start, verticalThenHorizontal)
&& SegmentIsClear(verticalThenHorizontal, end);
}
private static int CountRepeatCollectorNodeClearanceViolations(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
Dictionary<string, int>? severityByEdgeId,
int severityWeight = 1)
{
if (edges.Count == 0 || nodes.Count == 0)
{
return 0;
}
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
var minClearance = serviceNodes.Length > 0
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
: 50d;
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var count = 0;
foreach (var edge in edges)
{
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
{
continue;
}
var edgeViolations = 0;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
var horizontal = Math.Abs(segment.Start.Y - segment.End.Y) < 2d;
var vertical = Math.Abs(segment.Start.X - segment.End.X) < 2d;
if (!horizontal && !vertical)
{
continue;
}
foreach (var node in nodes)
{
if (node.Id == edge.SourceNodeId || node.Id == edge.TargetNodeId)
{
continue;
}
if (horizontal)
{
var overlapX = Math.Max(segment.Start.X, segment.End.X) > node.X
&& Math.Min(segment.Start.X, segment.End.X) < node.X + node.Width;
if (!overlapX)
{
continue;
}
var distance = Math.Min(
Math.Abs(segment.Start.Y - node.Y),
Math.Abs(segment.Start.Y - (node.Y + node.Height)));
if (distance > 0.5d && distance < minClearance)
{
edgeViolations++;
}
}
else
{
var overlapY = Math.Max(segment.Start.Y, segment.End.Y) > node.Y
&& Math.Min(segment.Start.Y, segment.End.Y) < node.Y + node.Height;
if (!overlapY)
{
continue;
}
var distance = Math.Min(
Math.Abs(segment.Start.X - node.X),
Math.Abs(segment.Start.X - (node.X + node.Width)));
if (distance > 0.5d && distance < minClearance)
{
edgeViolations++;
}
}
}
}
if (edgeViolations <= 0)
{
continue;
}
count += edgeViolations;
if (severityByEdgeId is not null)
{
severityByEdgeId[edge.Id] = severityByEdgeId.GetValueOrDefault(edge.Id) + (edgeViolations * severityWeight);
}
}
return count;
}
private static (ElkPoint StartPoint, ElkPoint EndPoint) ResolveRoutingEndpoints(
ElkPoint startPoint,
ElkPoint endPoint,
string? sourceNodeId,
string? targetNodeId,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
{
var adjustedStart = startPoint;
if (nodesById.TryGetValue(sourceNodeId ?? string.Empty, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
adjustedStart = ResolveGatewayRoutingDeparturePoint(sourceNode, endPoint, startPoint);
}
var adjustedEnd = endPoint;
if (nodesById.TryGetValue(targetNodeId ?? string.Empty, out var targetNode)
&& ElkShapeBoundaries.IsGatewayShape(targetNode))
{
adjustedEnd = ResolveGatewayRoutingApproachPoint(targetNode, adjustedStart, endPoint);
}
return (adjustedStart, adjustedEnd);
}
private static ElkPoint ResolveGatewayRoutingBoundary(
ElkPositionedNode node,
ElkPoint referencePoint)
{
var boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint);
return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, boundary, referencePoint);
}
private static ElkPoint ResolveGatewayRoutingDeparturePoint(
ElkPositionedNode node,
ElkPoint referencePoint,
ElkPoint preferredBoundary)
{
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
: ResolveGatewayRoutingBoundary(node, referencePoint);
var exteriorDeparture = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorDeparture)
? boundary
: exteriorDeparture;
}
private static ElkPoint ResolveGatewayRoutingApproachPoint(
ElkPositionedNode node,
ElkPoint referencePoint,
ElkPoint preferredBoundary)
{
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
: ResolveGatewayRoutingBoundary(node, referencePoint);
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorApproach)
? boundary
: exteriorApproach;
}
}