189 lines
6.6 KiB
C#
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;
|
|
}
|
|
}
|