/// <summary> /// Distribute nodes evenly between two sibling nodes. /// It will do nothing if there are no nodes between the two nodes. /// </summary> /// <param name="leftNode">Node on the left.</param> /// <param name="rightNode">Node on the right.</param> private void DistributeEvenlyNodesBetween(TreeLayoutNode leftNode, TreeLayoutNode rightNode) { var parentChildren = leftNode.Parent.Children; var leftIndex = parentChildren.IndexOf(leftNode); var rightIndex = parentChildren.IndexOf(rightNode); var countNodesBetween = rightIndex - leftIndex - 1; if (countNodesBetween < 1) { return; } var distanceBetweenNodes = (leftNode.X - rightNode.X) / (countNodesBetween + 1); var count = 1; for (var i = leftIndex + 1; i < rightIndex; i++, count++) { var middleNode = parentChildren[i]; var desiredX = rightNode.X + distanceBetweenNodes * count; var offset = desiredX - middleNode.X; middleNode.X += offset; middleNode.ChildrenShiftX += offset; this.FixConflicts(middleNode); } }
/// <summary> /// Calculate tree side contour (recursive). /// </summary> private void CalculateTreeContour( TreeLayoutNode node, List <double> contour, bool isLeftContour, int maxArrayIndex = int.MaxValue, int arrayLevel = 0, double shiftX = 0) { var currentNodeX = node.X + shiftX; if (arrayLevel < contour.Count) { var currentValue = contour[arrayLevel]; contour[arrayLevel] = isLeftContour ? Math.Min(currentValue, currentNodeX) : Math.Max(currentValue, currentNodeX); } else { contour.Add(currentNodeX); } if (arrayLevel == maxArrayIndex || node.Children.Count == 0) { // no need (or cannot) traverse deeper return; } shiftX += node.ChildrenShiftX; foreach (var child in node.Children) { this.CalculateTreeContour(child, contour, isLeftContour, maxArrayIndex, arrayLevel + 1, shiftX); } }
/// <summary> /// Layout multiple root nodes. /// </summary> /// <param name="rootNodes">List of root nodes.</param> public void DoLayout(IReadOnlyCollection <T> rootNodes) { var layoutRootNode = new TreeLayoutNode(null, rootNodes.Count); foreach (var node in rootNodes) { var layoutNode = this.CreateLayoutNode(node); layoutRootNode.Children.Add(layoutNode); layoutNode.Parent = layoutRootNode; } this.DoLayout(layoutRootNode); }
/// <summary> /// Traverse the tree and ensure that every node is in the screen bounds (minimal X>=0). /// </summary> private void EnsureAllNodesInScreenBounds(TreeLayoutNode rootNode) { // calculate left contour var contour = new List <double>(capacity: this.estimatedTreeMaxDepth); this.CalculateTreeContour(rootNode, contour, isLeftContour: true); // calculate min X position in contour var minX = contour.Min(); // shift root node to keep it in bounds rootNode.X -= minX; rootNode.ChildrenShiftX -= minX; }
/// <summary> /// Calculate final positions - apply parent X shift values to their children. /// </summary> private void CalculateFinalX(TreeLayoutNode node, double shiftX) { // offset this node on the shift value node.X += shiftX; // write result X position node.SetLayoutPositionX(node.X); if (node.Children.Count == 0) { return; } // process children // all the next children will be offset on the combined shift value shiftX += node.ChildrenShiftX; foreach (var child in node.Children) { this.CalculateFinalX(child, shiftX); } }
private TreeLayoutNode CreateLayoutNode(T rootNode) { var childrenCount = rootNode.Children.Count; var layoutNode = new TreeLayoutNode(rootNode, childrenCount); if (childrenCount == 0) { return(layoutNode); } var layoutNodeChildren = layoutNode.Children; foreach (var childNode in rootNode.Children) { var childLayoutNode = this.CreateLayoutNode(childNode); childLayoutNode.Parent = layoutNode; layoutNodeChildren.Add(childLayoutNode); } return(layoutNode); }
/// <summary> /// Calculate initial X and children shift values. /// </summary> /// <param name="node">Current node.</param> /// <param name="previousNode">Previous (left sibling to current) node.</param> private void CalculateInitialX(TreeLayoutNode node, TreeLayoutNode previousNode) { var childrenCount = node.Children.Count; if (childrenCount > 0) { TreeLayoutNode previousChildNode = null; foreach (var childNode in node.Children) { this.CalculateInitialX(childNode, previousChildNode); previousChildNode = childNode; } } if (childrenCount == 0) { // no children if (previousNode == null) { // first node in the row node.X = 0; } else { // take previous node X and add the padding distance node.X = previousNode.X + this.siblingNodePadding; } return; } // has children double middleX; var leftChild = node.Children[0]; if (childrenCount == 1) { // single children - middleX equals to the (only) child X middleX = leftChild.X; } else { // many children - calculate middle X var rightChild = node.Children[node.Children.Count - 1]; middleX = (leftChild.X + rightChild.X) / 2; } if (previousNode == null) { // no left sibling node.X = middleX; } else { // has left sibling - offset accordingly node.X = previousNode.X + this.siblingNodePadding; node.ChildrenShiftX = node.X - middleX; // since subtrees can overlap, check for conflicts and shift tree right wherever needed this.FixConflicts(node); } }
/// <summary> /// Check and resolve conflicts with all the left-side sibling trees. /// This method will check collisions of current node tree with all the sibling node trees on the left side starting from /// the most left node. /// The implementation will calculate left contour of the provided node and compare it with the right contours of the /// sibling nodes. /// </summary> /// <param name="node">Current node (it will be shifted when needed).</param> private void FixConflicts(TreeLayoutNode node) { var siblingNode = node.GetLeftMostSibling(); if (siblingNode == node) { return; } var minDistance = this.siblingNodePadding; var nodeLeftContour = new List <double>(capacity: this.estimatedTreeMaxDepth); var nodeLeftContourMaxLevel = 0; var isNeedRecalculateNodeLeftContour = true; var siblingNodeRightContour = new List <double>(capacity: this.estimatedTreeMaxDepth); do { if (isNeedRecalculateNodeLeftContour) { // calculate current node left contour isNeedRecalculateNodeLeftContour = false; nodeLeftContour.Clear(); this.CalculateTreeContour(node, nodeLeftContour, isLeftContour: true); nodeLeftContourMaxLevel = nodeLeftContour.Count - 1; } if (siblingNodeRightContour.Count > 0) { siblingNodeRightContour.Clear(); } // calculate right contour of the sibling tree this.CalculateTreeContour( siblingNode, siblingNodeRightContour, isLeftContour: false, maxArrayIndex: nodeLeftContourMaxLevel); var siblingNodeRightContourMaxLevel = siblingNodeRightContour.Count - 1; var maxLevel = Math.Min(nodeLeftContourMaxLevel, siblingNodeRightContourMaxLevel); // compare contours to find shift value // (separation distance between the adjacent nodes of two trees) var maxMeltaDistance = 0.0; for (var level = 1; level <= maxLevel; level++) { var deltaDistance = minDistance + siblingNodeRightContour[level] - nodeLeftContour[level]; if (deltaDistance > maxMeltaDistance) { maxMeltaDistance = deltaDistance; } } if (maxMeltaDistance > 0) { // need to shift node right to resolve collision with the tree on the left side node.X += maxMeltaDistance; node.ChildrenShiftX += maxMeltaDistance; isNeedRecalculateNodeLeftContour = true; // need to distribute (shift) all the nodes between current node and sibling node this.DistributeEvenlyNodesBetween(leftNode: siblingNode, rightNode: node); } siblingNode = siblingNode.GetNextSibling(); }while (siblingNode != node); }
private void DoLayout(TreeLayoutNode layoutRootNode) { this.CalculateInitialX(layoutRootNode, previousNode: null); this.EnsureAllNodesInScreenBounds(layoutRootNode); this.CalculateFinalX(layoutRootNode, shiftX: 0); }