Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkCompoundHierarchy.cs
2026-03-28 13:36:52 +02:00

189 lines
6.6 KiB
C#

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;
}
}