301 lines
13 KiB
C#
301 lines
13 KiB
C#
namespace StellaOps.ElkSharp;
|
|
internal static partial class ElkCompoundLayout
|
|
{
|
|
internal static ElkLayoutResult Layout(
|
|
ElkGraph graph,
|
|
ElkLayoutOptions options,
|
|
ElkCompoundHierarchy hierarchy,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var nodesById = graph.Nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var visibleNodes = graph.Nodes
|
|
.Where(node => hierarchy.IsLayoutVisibleNode(node.Id))
|
|
.ToArray();
|
|
var visibleNodesById = visibleNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var (inputOrder, backEdgeIds) = ElkLayerAssignment.BuildTraversalInputOrder(visibleNodes, graph.Edges, visibleNodesById);
|
|
|
|
var outgoing = visibleNodes.ToDictionary(node => node.Id, _ => new List<ElkEdge>(), StringComparer.Ordinal);
|
|
var incomingNodeIds = visibleNodes.ToDictionary(node => node.Id, _ => new List<string>(), StringComparer.Ordinal);
|
|
var outgoingNodeIds = visibleNodes.ToDictionary(node => node.Id, _ => new List<string>(), StringComparer.Ordinal);
|
|
foreach (var edge in graph.Edges)
|
|
{
|
|
outgoing[edge.SourceNodeId].Add(edge);
|
|
incomingNodeIds[edge.TargetNodeId].Add(edge.SourceNodeId);
|
|
outgoingNodeIds[edge.SourceNodeId].Add(edge.TargetNodeId);
|
|
}
|
|
|
|
var layersByNodeId = ElkLayerAssignment.AssignLayersByInputOrder(visibleNodes, outgoing, inputOrder, backEdgeIds);
|
|
var dummyResult = ElkLayerAssignment.InsertDummyNodes(visibleNodes, graph.Edges, layersByNodeId, inputOrder, backEdgeIds);
|
|
var allNodes = dummyResult.AllNodes;
|
|
var allEdges = dummyResult.AllEdges;
|
|
var augmentedNodesById = allNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var augmentedInputOrder = dummyResult.AugmentedInputOrder;
|
|
var augmentedIncoming = allNodes.ToDictionary(node => node.Id, _ => new List<string>(), StringComparer.Ordinal);
|
|
var augmentedOutgoing = allNodes.ToDictionary(node => node.Id, _ => new List<string>(), StringComparer.Ordinal);
|
|
foreach (var edge in allEdges)
|
|
{
|
|
augmentedIncoming[edge.TargetNodeId].Add(edge.SourceNodeId);
|
|
augmentedOutgoing[edge.SourceNodeId].Add(edge.TargetNodeId);
|
|
}
|
|
|
|
var orderingIterations = ElkNodePlacement.ResolveOrderingIterationCount(options, allEdges.Count, layersByNodeId.Count);
|
|
var layers = allNodes
|
|
.GroupBy(node => dummyResult.AugmentedLayers[node.Id])
|
|
.OrderBy(group => group.Key)
|
|
.Select(group => group.OrderBy(node => augmentedInputOrder[node.Id]).ToArray())
|
|
.ToArray();
|
|
layers = OptimizeLayerOrderingForHierarchy(
|
|
layers,
|
|
augmentedIncoming,
|
|
augmentedOutgoing,
|
|
augmentedInputOrder,
|
|
orderingIterations,
|
|
hierarchy,
|
|
dummyResult.DummyNodeIds);
|
|
|
|
var placementIterations = ElkNodePlacement.ResolvePlacementIterationCount(options, allNodes.Count, layers.Length);
|
|
var placementGrid = ElkNodePlacement.ResolvePlacementGrid(visibleNodes);
|
|
var positionedVisibleNodes = new Dictionary<string, ElkPositionedNode>(StringComparer.Ordinal);
|
|
var globalNodeWidth = visibleNodes.Max(node => node.Width);
|
|
var edgeDensityFactor = Math.Min(1.8d, 1d + (Math.Max(0, allEdges.Count - 15) * 0.02d));
|
|
var adaptiveNodeSpacing = options.NodeSpacing * edgeDensityFactor;
|
|
if (options.Direction == ElkLayoutDirection.LeftToRight)
|
|
{
|
|
ElkSharpLayoutInitialPlacement.PlaceNodesLeftToRight(
|
|
positionedVisibleNodes,
|
|
layers,
|
|
dummyResult,
|
|
augmentedIncoming,
|
|
augmentedOutgoing,
|
|
augmentedNodesById,
|
|
incomingNodeIds,
|
|
outgoingNodeIds,
|
|
visibleNodesById,
|
|
adaptiveNodeSpacing,
|
|
options,
|
|
placementIterations,
|
|
placementGrid);
|
|
}
|
|
else
|
|
{
|
|
ElkSharpLayoutInitialPlacement.PlaceNodesTopToBottom(
|
|
positionedVisibleNodes,
|
|
layers,
|
|
dummyResult,
|
|
augmentedIncoming,
|
|
augmentedOutgoing,
|
|
augmentedNodesById,
|
|
incomingNodeIds,
|
|
outgoingNodeIds,
|
|
visibleNodesById,
|
|
globalNodeWidth,
|
|
adaptiveNodeSpacing,
|
|
options,
|
|
placementIterations,
|
|
placementGrid);
|
|
}
|
|
|
|
var graphBounds = ElkGraphValidator.ComputeGraphBounds(positionedVisibleNodes.Values
|
|
.Where(node => !dummyResult.DummyNodeIds.Contains(node.Id))
|
|
.ToArray());
|
|
var layerBoundariesByNodeId = ElkLayoutHelpers.BuildLayerBoundariesByNodeId(positionedVisibleNodes, dummyResult.AugmentedLayers);
|
|
var edgeChannels = ElkEdgeChannels.ComputeEdgeChannels(graph.Edges, positionedVisibleNodes, options.Direction, layerBoundariesByNodeId);
|
|
var reconstructedEdges = ElkEdgeRouter.ReconstructDummyEdges(
|
|
graph.Edges,
|
|
dummyResult,
|
|
positionedVisibleNodes,
|
|
augmentedNodesById,
|
|
options.Direction,
|
|
graphBounds,
|
|
edgeChannels,
|
|
layerBoundariesByNodeId);
|
|
var routedEdges = graph.Edges
|
|
.Select(edge =>
|
|
{
|
|
if (reconstructedEdges.TryGetValue(edge.Id, out var routed))
|
|
{
|
|
return routed;
|
|
}
|
|
|
|
var channel = ElkSharpLayoutHelpers.ResolveSinkOverride(
|
|
edgeChannels.GetValueOrDefault(edge.Id),
|
|
edge.Id,
|
|
dummyResult,
|
|
edgeChannels,
|
|
graph.Edges);
|
|
return ElkEdgeRouter.RouteEdge(
|
|
edge,
|
|
visibleNodesById,
|
|
positionedVisibleNodes,
|
|
options.Direction,
|
|
graphBounds,
|
|
channel,
|
|
layerBoundariesByNodeId);
|
|
})
|
|
.ToArray();
|
|
|
|
for (var gutterPass = 0; gutterPass < 3; gutterPass++)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (!ElkEdgeChannelGutters.ExpandVerticalCorridorGutters(
|
|
positionedVisibleNodes,
|
|
routedEdges,
|
|
dummyResult.AugmentedLayers,
|
|
augmentedNodesById,
|
|
options.LayerSpacing,
|
|
options.Direction))
|
|
{
|
|
break;
|
|
}
|
|
|
|
graphBounds = ElkGraphValidator.ComputeGraphBounds(positionedVisibleNodes.Values
|
|
.Where(node => !dummyResult.DummyNodeIds.Contains(node.Id))
|
|
.ToArray());
|
|
layerBoundariesByNodeId = ElkLayoutHelpers.BuildLayerBoundariesByNodeId(positionedVisibleNodes, dummyResult.AugmentedLayers);
|
|
edgeChannels = ElkEdgeChannels.ComputeEdgeChannels(graph.Edges, positionedVisibleNodes, options.Direction, layerBoundariesByNodeId);
|
|
reconstructedEdges = ElkEdgeRouter.ReconstructDummyEdges(
|
|
graph.Edges,
|
|
dummyResult,
|
|
positionedVisibleNodes,
|
|
augmentedNodesById,
|
|
options.Direction,
|
|
graphBounds,
|
|
edgeChannels,
|
|
layerBoundariesByNodeId);
|
|
routedEdges = graph.Edges
|
|
.Select(edge => reconstructedEdges.TryGetValue(edge.Id, out var rerouted)
|
|
? rerouted
|
|
: ElkEdgeRouter.RouteEdge(
|
|
edge,
|
|
visibleNodesById,
|
|
positionedVisibleNodes,
|
|
options.Direction,
|
|
graphBounds,
|
|
ElkSharpLayoutHelpers.ResolveSinkOverride(
|
|
edgeChannels.GetValueOrDefault(edge.Id),
|
|
edge.Id,
|
|
dummyResult,
|
|
edgeChannels,
|
|
graph.Edges),
|
|
layerBoundariesByNodeId))
|
|
.ToArray();
|
|
}
|
|
|
|
for (var compactPass = 0; compactPass < 2; compactPass++)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (!ElkEdgeChannelGutters.CompactSparseVerticalCorridorGutters(
|
|
positionedVisibleNodes,
|
|
routedEdges,
|
|
dummyResult.AugmentedLayers,
|
|
augmentedNodesById,
|
|
options.LayerSpacing,
|
|
options.Direction))
|
|
{
|
|
break;
|
|
}
|
|
|
|
graphBounds = ElkGraphValidator.ComputeGraphBounds(positionedVisibleNodes.Values
|
|
.Where(node => !dummyResult.DummyNodeIds.Contains(node.Id))
|
|
.ToArray());
|
|
layerBoundariesByNodeId = ElkLayoutHelpers.BuildLayerBoundariesByNodeId(positionedVisibleNodes, dummyResult.AugmentedLayers);
|
|
edgeChannels = ElkEdgeChannels.ComputeEdgeChannels(graph.Edges, positionedVisibleNodes, options.Direction, layerBoundariesByNodeId);
|
|
reconstructedEdges = ElkEdgeRouter.ReconstructDummyEdges(
|
|
graph.Edges,
|
|
dummyResult,
|
|
positionedVisibleNodes,
|
|
augmentedNodesById,
|
|
options.Direction,
|
|
graphBounds,
|
|
edgeChannels,
|
|
layerBoundariesByNodeId);
|
|
routedEdges = graph.Edges
|
|
.Select(edge => reconstructedEdges.TryGetValue(edge.Id, out var rerouted)
|
|
? rerouted
|
|
: ElkEdgeRouter.RouteEdge(
|
|
edge,
|
|
visibleNodesById,
|
|
positionedVisibleNodes,
|
|
options.Direction,
|
|
graphBounds,
|
|
ElkSharpLayoutHelpers.ResolveSinkOverride(
|
|
edgeChannels.GetValueOrDefault(edge.Id),
|
|
edge.Id,
|
|
dummyResult,
|
|
edgeChannels,
|
|
graph.Edges),
|
|
layerBoundariesByNodeId))
|
|
.ToArray();
|
|
|
|
if (!ElkEdgeChannelGutters.ExpandVerticalCorridorGutters(
|
|
positionedVisibleNodes,
|
|
routedEdges,
|
|
dummyResult.AugmentedLayers,
|
|
augmentedNodesById,
|
|
options.LayerSpacing,
|
|
options.Direction))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
graphBounds = ElkGraphValidator.ComputeGraphBounds(positionedVisibleNodes.Values
|
|
.Where(node => !dummyResult.DummyNodeIds.Contains(node.Id))
|
|
.ToArray());
|
|
layerBoundariesByNodeId = ElkLayoutHelpers.BuildLayerBoundariesByNodeId(positionedVisibleNodes, dummyResult.AugmentedLayers);
|
|
edgeChannels = ElkEdgeChannels.ComputeEdgeChannels(graph.Edges, positionedVisibleNodes, options.Direction, layerBoundariesByNodeId);
|
|
reconstructedEdges = ElkEdgeRouter.ReconstructDummyEdges(
|
|
graph.Edges,
|
|
dummyResult,
|
|
positionedVisibleNodes,
|
|
augmentedNodesById,
|
|
options.Direction,
|
|
graphBounds,
|
|
edgeChannels,
|
|
layerBoundariesByNodeId);
|
|
routedEdges = graph.Edges
|
|
.Select(edge => reconstructedEdges.TryGetValue(edge.Id, out var rerouted)
|
|
? rerouted
|
|
: ElkEdgeRouter.RouteEdge(
|
|
edge,
|
|
visibleNodesById,
|
|
positionedVisibleNodes,
|
|
options.Direction,
|
|
graphBounds,
|
|
ElkSharpLayoutHelpers.ResolveSinkOverride(
|
|
edgeChannels.GetValueOrDefault(edge.Id),
|
|
edge.Id,
|
|
dummyResult,
|
|
edgeChannels,
|
|
graph.Edges),
|
|
layerBoundariesByNodeId))
|
|
.ToArray();
|
|
}
|
|
|
|
var finalVisibleNodes = positionedVisibleNodes.Values
|
|
.Where(node => !dummyResult.DummyNodeIds.Contains(node.Id))
|
|
.OrderBy(node => inputOrder.GetValueOrDefault(node.Id, int.MaxValue))
|
|
.ToArray();
|
|
|
|
routedEdges = ElkEdgePostProcessor.SnapAnchorsToNodeBoundary(routedEdges, finalVisibleNodes);
|
|
routedEdges = ElkEdgeRouterIterative.Optimize(routedEdges, finalVisibleNodes, options, cancellationToken);
|
|
|
|
var compoundNodes = BuildCompoundPositionedNodes(graph.Nodes, hierarchy, positionedVisibleNodes, options);
|
|
if (TryResolveNegativeCoordinateShift(compoundNodes, out var shiftX, out var shiftY))
|
|
{
|
|
compoundNodes = ShiftNodes(graph.Nodes, compoundNodes, shiftX, shiftY, options.Direction);
|
|
routedEdges = ShiftEdges(routedEdges, shiftX, shiftY);
|
|
}
|
|
|
|
routedEdges = InsertCompoundBoundaryCrossings(routedEdges, compoundNodes, hierarchy);
|
|
ElkLayoutDiagnostics.LogProgress("ElkSharp compound layout optimize returned");
|
|
|
|
return new ElkLayoutResult
|
|
{
|
|
GraphId = graph.Id,
|
|
Nodes = graph.Nodes.Select(node => compoundNodes[node.Id]).ToArray(),
|
|
Edges = routedEdges,
|
|
};
|
|
}
|
|
|
|
}
|