Add ElkSharp compound node support
This commit is contained in:
188
src/__Libraries/StellaOps.ElkSharp/ElkCompoundHierarchy.cs
Normal file
188
src/__Libraries/StellaOps.ElkSharp/ElkCompoundHierarchy.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal sealed class ElkCompoundHierarchy
|
||||
{
|
||||
private const string RootKey = "\u0001__root__";
|
||||
|
||||
private readonly Dictionary<string, ElkNode> nodesById;
|
||||
private readonly Dictionary<string, string?> parentByNodeId;
|
||||
private readonly Dictionary<string, List<string>> childrenByParentId;
|
||||
private readonly Dictionary<string, string[]> ancestorsNearestFirstByNodeId;
|
||||
private readonly Dictionary<string, int> depthByNodeId;
|
||||
private readonly Dictionary<string, int> originalOrderByNodeId;
|
||||
|
||||
private ElkCompoundHierarchy(
|
||||
Dictionary<string, ElkNode> nodesById,
|
||||
Dictionary<string, string?> parentByNodeId,
|
||||
Dictionary<string, List<string>> childrenByParentId,
|
||||
Dictionary<string, string[]> ancestorsNearestFirstByNodeId,
|
||||
Dictionary<string, int> depthByNodeId,
|
||||
Dictionary<string, int> originalOrderByNodeId)
|
||||
{
|
||||
this.nodesById = nodesById;
|
||||
this.parentByNodeId = parentByNodeId;
|
||||
this.childrenByParentId = childrenByParentId;
|
||||
this.ancestorsNearestFirstByNodeId = ancestorsNearestFirstByNodeId;
|
||||
this.depthByNodeId = depthByNodeId;
|
||||
this.originalOrderByNodeId = originalOrderByNodeId;
|
||||
}
|
||||
|
||||
internal bool HasCompoundNodes =>
|
||||
nodesById.Keys.Any(IsCompoundNode);
|
||||
|
||||
internal IEnumerable<string> NodeIds => nodesById.Keys;
|
||||
|
||||
internal IEnumerable<string> NonLeafNodeIds =>
|
||||
nodesById.Keys.Where(IsCompoundNode);
|
||||
|
||||
internal static ElkCompoundHierarchy Build(IReadOnlyCollection<ElkNode> nodes)
|
||||
{
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var originalOrderByNodeId = nodes
|
||||
.Select((node, index) => new KeyValuePair<string, int>(node.Id, index))
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
var parentByNodeId = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
var childrenByParentId = new Dictionary<string, List<string>>(StringComparer.Ordinal)
|
||||
{
|
||||
[RootKey] = [],
|
||||
};
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var parentNodeId = string.IsNullOrWhiteSpace(node.ParentNodeId)
|
||||
? null
|
||||
: node.ParentNodeId.Trim();
|
||||
if (parentNodeId is not null && !nodesById.ContainsKey(parentNodeId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Node '{node.Id}' references unknown parent '{parentNodeId}'.");
|
||||
}
|
||||
|
||||
if (string.Equals(parentNodeId, node.Id, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Node '{node.Id}' cannot be its own parent.");
|
||||
}
|
||||
|
||||
parentByNodeId[node.Id] = parentNodeId;
|
||||
var key = parentNodeId ?? RootKey;
|
||||
if (!childrenByParentId.TryGetValue(key, out var children))
|
||||
{
|
||||
children = [];
|
||||
childrenByParentId[key] = children;
|
||||
}
|
||||
|
||||
children.Add(node.Id);
|
||||
}
|
||||
|
||||
foreach (var children in childrenByParentId.Values)
|
||||
{
|
||||
children.Sort((left, right) => originalOrderByNodeId[left].CompareTo(originalOrderByNodeId[right]));
|
||||
}
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal) { node.Id };
|
||||
var currentNodeId = node.Id;
|
||||
while (parentByNodeId[currentNodeId] is { } parentNodeId)
|
||||
{
|
||||
if (!visited.Add(parentNodeId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Containment cycle detected while resolving parent chain for '{node.Id}'.");
|
||||
}
|
||||
|
||||
currentNodeId = parentNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
var ancestorsNearestFirstByNodeId = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
||||
var depthByNodeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
ResolveAncestors(node.Id);
|
||||
}
|
||||
|
||||
return new ElkCompoundHierarchy(
|
||||
nodesById,
|
||||
parentByNodeId,
|
||||
childrenByParentId,
|
||||
ancestorsNearestFirstByNodeId,
|
||||
depthByNodeId,
|
||||
originalOrderByNodeId);
|
||||
|
||||
string[] ResolveAncestors(string nodeId)
|
||||
{
|
||||
if (ancestorsNearestFirstByNodeId.TryGetValue(nodeId, out var cachedAncestors))
|
||||
{
|
||||
return cachedAncestors;
|
||||
}
|
||||
|
||||
if (parentByNodeId[nodeId] is not { } parentNodeId)
|
||||
{
|
||||
depthByNodeId[nodeId] = 0;
|
||||
cachedAncestors = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentAncestors = ResolveAncestors(parentNodeId);
|
||||
cachedAncestors = [parentNodeId, .. parentAncestors];
|
||||
depthByNodeId[nodeId] = depthByNodeId[parentNodeId] + 1;
|
||||
}
|
||||
|
||||
ancestorsNearestFirstByNodeId[nodeId] = cachedAncestors;
|
||||
return cachedAncestors;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsCompoundNode(string nodeId) =>
|
||||
childrenByParentId.TryGetValue(nodeId, out var children) && children.Count > 0;
|
||||
|
||||
internal bool IsLeafNode(string nodeId) =>
|
||||
!IsCompoundNode(nodeId);
|
||||
|
||||
internal bool IsLayoutVisibleNode(string nodeId) =>
|
||||
IsLeafNode(nodeId);
|
||||
|
||||
internal bool ContainsNode(string nodeId) =>
|
||||
nodesById.ContainsKey(nodeId);
|
||||
|
||||
internal int GetDepth(string nodeId) =>
|
||||
depthByNodeId[nodeId];
|
||||
|
||||
internal int GetOriginalOrder(string nodeId) =>
|
||||
originalOrderByNodeId[nodeId];
|
||||
|
||||
internal string? GetParentNodeId(string nodeId) =>
|
||||
parentByNodeId[nodeId];
|
||||
|
||||
internal IReadOnlyList<string> GetChildIds(string? parentNodeId)
|
||||
{
|
||||
var key = parentNodeId ?? RootKey;
|
||||
return childrenByParentId.TryGetValue(key, out var children)
|
||||
? children
|
||||
: [];
|
||||
}
|
||||
|
||||
internal IReadOnlyList<string> GetAncestorIdsNearestFirst(string nodeId) =>
|
||||
ancestorsNearestFirstByNodeId[nodeId];
|
||||
|
||||
internal IEnumerable<string> GetNonLeafNodeIdsByDescendingDepth() =>
|
||||
NonLeafNodeIds
|
||||
.OrderByDescending(GetDepth)
|
||||
.ThenBy(GetOriginalOrder);
|
||||
|
||||
internal string? GetLowestCommonAncestor(string leftNodeId, string rightNodeId)
|
||||
{
|
||||
var leftAncestors = new HashSet<string>(GetAncestorIdsNearestFirst(leftNodeId), StringComparer.Ordinal);
|
||||
foreach (var ancestorNodeId in GetAncestorIdsNearestFirst(rightNodeId))
|
||||
{
|
||||
if (leftAncestors.Contains(ancestorNodeId))
|
||||
{
|
||||
return ancestorNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user