/*******************************************************************************
* Copyright (c) 2005, 2016 The Chisel Group and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors: Ian Bull (The Chisel Group) - initial API and implementation
* Mateusz Matela - "Tree Views for Zest" contribution, Google Summer of Code 2009
* Matthias Wienand (itemis AG) - refactorings
* Alexaner Nyßen (itemis AG) - refactorings (bug #469472)
******************************************************************************/
package org.eclipse.gef.layout.algorithms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Rectangle;
import org.eclipse.gef.graph.Node;
import org.eclipse.gef.layout.ILayoutAlgorithm;
import org.eclipse.gef.layout.LayoutContext;
import org.eclipse.gef.layout.LayoutProperties;
import org.eclipse.gef.layout.algorithms.TreeLayoutHelper.TreeNode;
/**
* Layout algorithm implementing SpaceTree. It assumes that nodes in the layout
* context make a tree structure.
*
* It expands and collapses nodes to optimize use of available space. In order
* to keep the tree structure clearly visible, it also keeps track of the nodes'
* positions to makes sure they stay in their current layer and don't overlap
* with each other.
*
* @author Ian Bull
* @author Mateusz Matela
* @author mwienand
*/
public class SpaceTreeLayoutAlgorithm implements ILayoutAlgorithm {
/**
* Tree direction constant for which root is placed at the top and branches
* spread downwards
*/
public final static int TOP_DOWN = 1;
/**
* Tree direction constant for which root is placed at the bottom and
* branches spread upwards
*/
public final static int BOTTOM_UP = 2;
/**
* Tree direction constant for which root is placed at the left and branches
* spread to the right
*/
public final static int LEFT_RIGHT = 3;
/**
* Tree direction constant for which root is placed at the right and
* branches spread to the left
*/
public final static int RIGHT_LEFT = 4;
private class SpaceTreeNode extends TreeLayoutHelper.TreeNode {
public boolean expanded = true;
public double positionInLayer;
public SpaceTreeNode(Node node, TreeLayoutHelper owner) {
super(node, owner);
}
protected void addChild(TreeLayoutHelper.TreeNode child) {
super.addChild(child);
SpaceTreeNode child2 = (SpaceTreeNode) child;
child2.expanded = false;
if (child.depth >= 0)
spaceTreeLayers.get(child.depth).removeNode(child2);
if (expanded) {
child.depth = this.depth + 1;
SpaceTreeLayer childLayer;
if (child.depth < spaceTreeLayers.size())
childLayer = spaceTreeLayers.get(child.depth);
else
spaceTreeLayers
.add(childLayer = new SpaceTreeLayer(child.depth));
if (childLayer.nodes.isEmpty())
child.order = 0;
else
child.order = childLayer.nodes
.get(childLayer.nodes.size() - 1).order + 1;
childLayer.addNodes(Arrays.asList(child));
}
}
public void precomputeTree() {
super.precomputeTree();
if (this == owner.getSuperRoot()) {
expanded = true;
while (spaceTreeLayers.size() <= this.height)
spaceTreeLayers
.add(new SpaceTreeLayer(spaceTreeLayers.size()));
}
}
/**
* Moves the node back to its layer, as close as possible to given
* preferred location.
*
* @param preferredLocation
* The location to which the node should be moved.
*/
public void adjustPosition(Point preferredLocation) { // !
protectedNode = (SpaceTreeNode) owner.getSuperRoot();
double newPositionInLayer = (direction == BOTTOM_UP
|| direction == TOP_DOWN) ? preferredLocation.x
: preferredLocation.y;
if (((SpaceTreeNode) parent).expanded) {
spaceTreeLayers.get(depth).moveNode(this, newPositionInLayer);
centerParentsTopDown();
}
}
public double spaceRequiredForNode() {
if (node == null)
return 0;
switch (direction) {
case TOP_DOWN:
case BOTTOM_UP:
return LayoutProperties.getSize(node).width;
case LEFT_RIGHT:
case RIGHT_LEFT:
return LayoutProperties.getSize(node).height;
}
throw new RuntimeException("invalid direction");
}
public double spaceRequiredForChildren() {
if (children.isEmpty())
return 0;
double result = 0;
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
SpaceTreeNode child = (SpaceTreeNode) iterator.next();
result += child.spaceRequiredForNode();
}
result += leafGap * (children.size() - 1);
return result;
}
/**
* Checks if nodes in given list have proper positions according to
* their children (a parent's position cannot be smaller than its first
* child's position nor bigger than its last child's position). If not,
* it tries to fix them.
*
* @param nodesToCheck
* An {@link ArrayList} of the {@link TreeNode}s that are
* checked for proper positions.
* @return true if all locations are correct or could be corrected while
* checking.
*/
public boolean childrenPositionsOK(ArrayList<TreeNode> nodesToCheck) {
for (Iterator<TreeNode> iterator = nodesToCheck.iterator(); iterator
.hasNext();) {
SpaceTreeNode node = (SpaceTreeNode) iterator.next();
if (node.depth < 0 || node.children.isEmpty())
continue;
SpaceTreeNode child = ((SpaceTreeNode) node.children.get(0));
if (child.positionInLayer > node.positionInLayer) {
spaceTreeLayers.get(node.depth).moveNode(node,
child.positionInLayer);
if (child.positionInLayer > node.positionInLayer) {
spaceTreeLayers.get(child.depth).moveNode(child,
node.positionInLayer);
if (child.positionInLayer > node.positionInLayer) {
return false;
}
}
}
child = ((SpaceTreeNode) node.children
.get(node.children.size() - 1));
if (child.positionInLayer < node.positionInLayer) {
spaceTreeLayers.get(node.depth).moveNode(node,
child.positionInLayer);
if (child.positionInLayer < node.positionInLayer) {
spaceTreeLayers.get(child.depth).moveNode(child,
node.positionInLayer);
if (child.positionInLayer < node.positionInLayer) {
return false;
}
}
}
}
return true;
}
public void centerParentsBottomUp() {
if (!children.isEmpty() && expanded) {
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
((SpaceTreeNode) iterator.next()).centerParentsBottomUp();
}
if (depth >= 0) {
SpaceTreeNode firstChild = (SpaceTreeNode) children.get(0);
SpaceTreeNode lastChild = (SpaceTreeNode) children
.get(children.size() - 1);
SpaceTreeLayer layer = spaceTreeLayers.get(depth);
layer.moveNode(this, (firstChild.positionInLayer
+ lastChild.positionInLayer) / 2);
}
}
}
public void centerParentsTopDown() {
if (this == owner.getSuperRoot()) {
this.positionInLayer = getAvailableSpace() / 2;
}
if (!children.isEmpty() && expanded) {
SpaceTreeNode firstChild = (SpaceTreeNode) children.get(0);
SpaceTreeNode lastChild = (SpaceTreeNode) children
.get(children.size() - 1);
double offset = this.positionInLayer
- (firstChild.positionInLayer
+ lastChild.positionInLayer) / 2;
if (firstChild.positionInLayer
- firstChild.spaceRequiredForNode() / 2 + offset < 0)
offset = -firstChild.positionInLayer
+ firstChild.spaceRequiredForNode() / 2;
double availableSpace = getAvailableSpace();
if (lastChild.positionInLayer
+ lastChild.spaceRequiredForNode() / 2
+ offset > availableSpace) {
offset = availableSpace - lastChild.positionInLayer
- lastChild.spaceRequiredForNode() / 2;
}
SpaceTreeLayer layer = spaceTreeLayers.get(depth + 1);
layer.fitNodesWithinBounds(children,
firstChild.positionInLayer + offset,
lastChild.positionInLayer + offset);
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
((SpaceTreeNode) iterator.next()).centerParentsTopDown();
}
}
}
public void flushExpansionChanges() {
if (this.expanded) {
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
((SpaceTreeNode) iterator.next()).flushExpansionChanges();
}
}
}
/**
* Sets locations of nodes in the graph depending on their current layer
* and position in layer.
*
* @param thicknessSoFar
* sum of thicknesses and gaps for all layers 'above' this
* node (should be 0 if called on superRoot)
* @return true if location of at least one node has changed
*/
public boolean flushLocationChanges(double thicknessSoFar) {
boolean madeChanges = false;
if (node != null) {
Dimension nodeSize = LayoutProperties.getSize(node);
double x = 0, y = 0;
switch (direction) {
case TOP_DOWN:
x = bounds.getX() + positionInLayer;
y = thicknessSoFar + nodeSize.height / 2;
break;
case BOTTOM_UP:
x = bounds.getX() + positionInLayer;
y = bounds.getY() + bounds.getHeight() - thicknessSoFar
- nodeSize.height / 2;
break;
case LEFT_RIGHT:
x = thicknessSoFar + nodeSize.height / 2;
y = bounds.getY() + positionInLayer;
break;
case RIGHT_LEFT:
x = bounds.getX() + bounds.getWidth() - thicknessSoFar
- nodeSize.height / 2;
y = bounds.getY() + positionInLayer;
break;
}
Point currentLocation = LayoutProperties.getLocation(node);
if (currentLocation.x != x || currentLocation.y != y) {
LayoutProperties.setLocation(node, new Point(x, y));
SpaceTreeNode spaceTreeNode = (SpaceTreeNode) treeObserver
.getTreeNode(node);
spaceTreeNode
.adjustPosition(LayoutProperties.getLocation(node));
spaceTreeLayers.get(depth).refreshThickness();
madeChanges = true;
}
}
if (expanded) {
thicknessSoFar += (depth >= 0
? spaceTreeLayers.get(depth).thickness : 0) + layerGap;
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
SpaceTreeNode child = (SpaceTreeNode) iterator.next();
madeChanges = child.flushLocationChanges(thicknessSoFar)
|| madeChanges;
}
}
return madeChanges;
}
public String toString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < depth; i++)
sb.append(" ");
if (node != null)
sb.append(node.toString());
sb.append("|" + this.order);
sb.append('\n');
for (Iterator<TreeNode> iterator = children.iterator(); iterator
.hasNext();) {
SpaceTreeNode child = (SpaceTreeNode) iterator.next();
sb.append(child.toString());
}
return sb.toString();
}
}
private TreeLayoutHelper.TreeNodeFactory spaceTreeNodeFactory = new TreeLayoutHelper.TreeNodeFactory() {
public TreeLayoutHelper.TreeNode createTreeNode(Node nodeLayout,
TreeLayoutHelper observer) {
return new SpaceTreeNode(nodeLayout, observer);
};
};
private class SpaceTreeLayer {
public ArrayList<SpaceTreeNode> nodes = new ArrayList<>();
private final int depth;
public double thickness = 0;
public SpaceTreeLayer(int depth) {
this.depth = depth;
}
public void addNodes(List<TreeNode> nodesToAdd) {
ListIterator<SpaceTreeNode> layerIterator = nodes.listIterator();
SpaceTreeNode previousNode = null;
for (Iterator<TreeNode> iterator = nodesToAdd.iterator(); iterator
.hasNext();) {
SpaceTreeNode nodeToAdd = (SpaceTreeNode) iterator.next();
SpaceTreeNode nodeInLayer = null;
while (layerIterator.hasNext()) {
nodeInLayer = layerIterator.next();
if (nodeInLayer.order >= nodeToAdd.order)
break;
double expectedPostion = (previousNode == null) ? 0
: previousNode.positionInLayer + expectedDistance(
previousNode, nodeInLayer);
nodeInLayer.positionInLayer = Math
.max(nodeInLayer.positionInLayer, expectedPostion);
previousNode = nodeInLayer;
}
if (nodeInLayer == null) {
layerIterator.add(nodeToAdd);
} else if (nodeInLayer.order == nodeToAdd.order) {
layerIterator.set(nodeToAdd);
} else {
if (nodeInLayer.order > nodeToAdd.order)
layerIterator.previous();
layerIterator.add(nodeToAdd);
}
layerIterator.previous();
}
// move the rest of nodes so that they don't overlap
while (layerIterator.hasNext()) {
SpaceTreeNode nodeInLayer = layerIterator.next();
double expectedPostion = (previousNode == null) ? 0
: previousNode.positionInLayer
+ expectedDistance(previousNode, nodeInLayer);
nodeInLayer.positionInLayer = Math
.max(nodeInLayer.positionInLayer, expectedPostion);
previousNode = nodeInLayer;
}
refreshThickness();
}
public void removeNode(SpaceTreeNode node) {
if (nodes.remove(node)) {
spaceTreeLayers.get(depth + 1).removeNodes(node.children);
refreshThickness();
}
}
public void removeNodes(List<TreeNode> nodesToRemove) {
if (this.nodes.removeAll(nodesToRemove)) {
SpaceTreeLayer nextLayer = spaceTreeLayers.get(depth + 1);
for (Iterator<TreeNode> iterator = nodesToRemove
.iterator(); iterator.hasNext();) {
SpaceTreeNode nodeToRemove = (SpaceTreeNode) iterator
.next();
nextLayer.removeNodes(nodeToRemove.children);
}
refreshThickness();
}
}
public void checkThickness(SpaceTreeNode node) {
double nodeThickness = 0;
Dimension size = LayoutProperties.getSize(node.node);
nodeThickness = (direction == TOP_DOWN || direction == BOTTOM_UP)
? size.height : size.width;
this.thickness = Math.max(this.thickness, nodeThickness);
}
public void refreshThickness() {
this.thickness = 0;
for (Iterator<SpaceTreeNode> iterator = nodes.iterator(); iterator
.hasNext();) {
checkThickness(iterator.next());
}
}
public void fitNodesWithinBounds(List<TreeNode> nodeList,
double startPosition, double endPosition) {
NodeSnapshot[][] snapShot = takeSnapShot();
SpaceTreeNode[] nodes = nodeList
.toArray(new SpaceTreeNode[nodeList.size()]);
double initialStartPosition = nodes[0].positionInLayer;
double initialNodesBredth = nodes[nodes.length - 1].positionInLayer
- initialStartPosition;
double[] desiredPositions = new double[nodes.length];
// calculate desired positions for every node, regarding their
// initial initial proportions
for (int i = 0; i < nodes.length; i++) {
double initialPositionAsPercent = (initialNodesBredth > 0)
? (nodes[i].positionInLayer - initialStartPosition)
/ initialNodesBredth
: 0;
desiredPositions[i] = initialPositionAsPercent
* (endPosition - startPosition);
}
// make sure there's proper distance between each pair of
// consecutive nodes
for (int i = 1; i < nodes.length; i++) {
SpaceTreeNode node = nodes[i];
SpaceTreeNode previousNode = nodes[i - 1];
double expectedDistance = expectedDistance(previousNode, node);
if (desiredPositions[i]
- desiredPositions[i - 1] < expectedDistance) {
desiredPositions[i] = desiredPositions[i - 1]
+ expectedDistance;
}
}
// if the above operation caused some nodes to fall out of requested
// bounds, push them back
if (desiredPositions[nodes.length - 1] > (endPosition
- startPosition)) {
desiredPositions[nodes.length - 1] = (endPosition
- startPosition);
for (int i = nodes.length - 1; i > 0; i--) {
SpaceTreeNode node = nodes[i];
SpaceTreeNode previousNode = nodes[i - 1];
double expectedDistance = expectedDistance(previousNode,
node);
if (desiredPositions[i]
- desiredPositions[i - 1] < expectedDistance) {
desiredPositions[i - 1] = desiredPositions[i]
- expectedDistance;
} else
break;
}
}
int maxCount = nodeList.size() * 100;
int totalCount = 0;
for (int i = 0; i < nodeList.size(); i++) {
// Stop this cycle if no result can be found
// Possible cause: lack of space to lay out nodes without overlapping
totalCount++;
if (totalCount > maxCount) {
break;
}
SpaceTreeNode node = (SpaceTreeNode) nodeList.get(i);
double desiredPosition = startPosition + desiredPositions[i];
moveNode(node, desiredPosition);
if (Math.abs(node.positionInLayer - desiredPosition) > 0.5) {
startPosition += (node.positionInLayer - desiredPosition);
i = -1;
revertToSnapshot(snapShot);
}
}
}
public void moveNode(SpaceTreeNode node, double newPosition) {
Collections.sort(nodes, new Comparator<SpaceTreeNode>() {
public int compare(SpaceTreeNode arg0, SpaceTreeNode arg1) {
return arg0.order - arg1.order;
}
});
double positionInLayerAtStart = node.positionInLayer;
if (newPosition >= positionInLayerAtStart)
moveNodeForward(node, newPosition);
if (newPosition <= positionInLayerAtStart)
moveNodeBackward(node, newPosition);
}
/**
* Tries to increase node's position in layer. It can move a node only
* if it doesn't cause nodes to fall out of available space (see
* {@link SpaceTreeLayoutAlgorithm#getAvailableSpace()}. If there's not
* enough space available, some nodes may be collapsed to increase it as
* long as it doesn't cause
* {@link SpaceTreeLayoutAlgorithm#protectedNode} or any of its
* descendants to be collapsed.
*
* @param nodeToMove
* @param newPosition
*/
private void moveNodeForward(SpaceTreeNode nodeToMove,
double newPosition) {
int nodeIndex = nodes.indexOf(nodeToMove);
if (nodeIndex == -1)
throw new IllegalArgumentException("node not on this layer");
// move forward -> check space to the 'right'
NodeSnapshot[][] snapShot = takeSnapShot();
boolean firstRun = true;
mainLoop: while (firstRun
|| nodeToMove.positionInLayer < newPosition) {
firstRun = false;
double requiredSpace = 0;
SpaceTreeNode previousNode = nodeToMove;
for (int i = nodeIndex + 1; i < nodes.size(); i++) {
SpaceTreeNode nextNode = nodes.get(i);
requiredSpace += expectedDistance(previousNode, nextNode);
previousNode = nextNode;
}
requiredSpace += previousNode.spaceRequiredForNode() / 2;
if (requiredSpace > getAvailableSpace() - newPosition) {
// find nodes to remove
boolean removed = false;
for (int i = nodeIndex; i < nodes.size(); i++) {
SpaceTreeNode nextNode = nodes.get(i);
if (protectedNode == null
|| (!protectedNode.isAncestorOf(nextNode)
&& !nextNode.parent
.isAncestorOf(protectedNode))) {
collapseNode((SpaceTreeNode) nextNode.parent);
if (nextNode.parent == nodeToMove.parent)
break mainLoop;
removed = true;
break;
}
}
if (!removed) {
// not enough space, but we can't collapse anything...
newPosition = getAvailableSpace() - requiredSpace;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
// move the node and all its neighbors to the 'right'
SpaceTreeNode currentNodeToMove = nodeToMove;
double newPositionForCurrent = newPosition;
for (int i = nodeIndex; i < nodes.size(); i++) {
currentNodeToMove.positionInLayer = newPositionForCurrent;
// move parent if moved node is its first child
if (currentNodeToMove.firstChild) {
SpaceTreeNode parent = (SpaceTreeNode) currentNodeToMove.parent;
if (depth > 0
&& parent.positionInLayer <= newPositionForCurrent) {
SpaceTreeLayer parentLayer = spaceTreeLayers
.get(depth - 1);
parentLayer.moveNodeForward(parent,
newPositionForCurrent);
if (parent.positionInLayer < newPositionForCurrent) {
double delta = newPositionForCurrent
- parent.positionInLayer;
newPosition -= delta;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
}
// move children if necessary
if (currentNodeToMove.expanded
&& !currentNodeToMove.children.isEmpty()) {
SpaceTreeNode lastChild = (SpaceTreeNode) currentNodeToMove.children
.get(currentNodeToMove.children.size() - 1);
if (lastChild.positionInLayer < newPositionForCurrent) {
// try to move all the children, that is move the
// first child and the rest will be pushed
SpaceTreeNode firstChild = (SpaceTreeNode) currentNodeToMove.children
.get(0);
SpaceTreeLayer childLayer = spaceTreeLayers
.get(depth + 1);
double expectedDistanceBetweenChildren = currentNodeToMove
.spaceRequiredForChildren()
- firstChild.spaceRequiredForNode() / 2
- lastChild.spaceRequiredForNode() / 2;
childLayer.moveNodeForward(firstChild,
newPositionForCurrent
- expectedDistanceBetweenChildren);
if (currentNodeToMove.expanded
&& lastChild.positionInLayer < newPositionForCurrent) {
// the previous attempt failed -> try to move
// only the last child
childLayer.moveNodeForward(lastChild,
newPositionForCurrent);
if (lastChild.positionInLayer < newPositionForCurrent) {
// child couldn't be moved as far as needed
// -> move current node back to the position
// over the child
double delta = newPositionForCurrent
- lastChild.positionInLayer;
newPosition -= delta;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
}
}
if (i < nodes.size() - 1) {
SpaceTreeNode nextNode = nodes.get(i + 1);
newPositionForCurrent += expectedDistance(
currentNodeToMove, nextNode);
currentNodeToMove = nextNode;
if (currentNodeToMove.positionInLayer > newPositionForCurrent)
break;
}
}
}
}
/**
* Method complementary to
* {@link #moveNodeForward(SpaceTreeNode, double)}. Decreases node's
* position in layer.
*
* @param nodeToMove
* @param newPosition
*/
private void moveNodeBackward(SpaceTreeNode nodeToMove,
double newPosition) {
int nodeIndex = nodes.indexOf(nodeToMove);
if (nodeIndex == -1)
throw new IllegalArgumentException("node not on this layer");
// move backward -> check space to the 'left'
// move and collapse until there's enough space
NodeSnapshot[][] snapShot = takeSnapShot();
boolean firstRun = true;
mainLoop: while (firstRun
|| nodeToMove.positionInLayer > newPosition) {
firstRun = false;
double requiredSpace = 0;
SpaceTreeNode previousNode = nodeToMove;
for (int i = nodeIndex - 1; i >= 0; i--) {
SpaceTreeNode nextNode = nodes.get(i);
requiredSpace += expectedDistance(previousNode, nextNode);
previousNode = nextNode;
}
requiredSpace += previousNode.spaceRequiredForNode() / 2;
if (requiredSpace > newPosition) {
// find nodes to remove
boolean removed = false;
for (int i = nodeIndex; i >= 0; i--) {
SpaceTreeNode nextNode = nodes.get(i);
if (protectedNode == null
|| (!protectedNode.isAncestorOf(nextNode)
&& !nextNode.parent
.isAncestorOf(protectedNode))) {
collapseNode((SpaceTreeNode) nextNode.parent);
if (nextNode.parent == nodeToMove.parent)
break mainLoop;
nodeIndex -= nextNode.parent.children.size();
removed = true;
break;
}
}
if (!removed) {
// not enough space, but we can't collapse anything...
newPosition = requiredSpace;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
// move the node and all its neighbors to the 'left'
SpaceTreeNode currentNodeToMove = nodeToMove;
double newPositionForCurrent = newPosition;
for (int i = nodeIndex; i >= 0; i--) {
currentNodeToMove.positionInLayer = newPositionForCurrent;
// move parent if moved node is its last child
if (currentNodeToMove.lastChild) {
SpaceTreeNode parent = (SpaceTreeNode) currentNodeToMove.parent;
if (depth > 0
&& parent.positionInLayer >= newPositionForCurrent) {
SpaceTreeLayer parentLayer = spaceTreeLayers
.get(depth - 1);
parentLayer.moveNodeBackward(parent,
newPositionForCurrent);
if (parent.positionInLayer > newPositionForCurrent) {
double delta = parent.positionInLayer
- newPositionForCurrent;
newPosition += delta;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
}
// move children if necessary
if (currentNodeToMove.expanded
&& !currentNodeToMove.children.isEmpty()) {
SpaceTreeNode firstChild = (SpaceTreeNode) currentNodeToMove.children
.get(0);
if (firstChild.positionInLayer > newPositionForCurrent) {
// try to move all the children, that is move the
// last child and the rest will be pushed
SpaceTreeNode lastChild = (SpaceTreeNode) currentNodeToMove.children
.get(currentNodeToMove.children.size() - 1);
SpaceTreeLayer childLayer = spaceTreeLayers
.get(depth + 1);
double expectedDistanceBetweenChildren = currentNodeToMove
.spaceRequiredForChildren()
- firstChild.spaceRequiredForNode() / 2
- lastChild.spaceRequiredForNode() / 2;
childLayer.moveNodeBackward(lastChild,
newPositionForCurrent
+ expectedDistanceBetweenChildren);
if (currentNodeToMove.expanded
&& firstChild.positionInLayer > newPositionForCurrent) {
// the previous attempt failed -> try to move
// only the first child
childLayer.moveNodeBackward(firstChild,
newPositionForCurrent);
if (firstChild.positionInLayer > newPositionForCurrent) {
// child couldn't be moved as far as needed
// -> move current node back to the position
// over the child
double delta = firstChild.positionInLayer
- newPositionForCurrent;
newPosition += delta;
revertToSnapshot(snapShot);
continue mainLoop;
}
}
}
}
if (i > 0) {
SpaceTreeNode nextNode = nodes.get(i - 1);
newPositionForCurrent -= expectedDistance(
currentNodeToMove, nextNode);
currentNodeToMove = nextNode;
if (currentNodeToMove.positionInLayer < newPositionForCurrent)
break;
}
}
}
}
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("Layer ").append(depth).append(": ");
for (Iterator<SpaceTreeNode> iterator = nodes.iterator(); iterator
.hasNext();) {
SpaceTreeNode node = iterator.next();
buffer.append(node.node).append(", ");
}
return buffer.toString();
}
private void collapseNode(SpaceTreeNode node) {
node.expanded = false;
SpaceTreeLayer layer = spaceTreeLayers.get(node.depth + 1);
layer.removeNodes(node.children);
for (Iterator<TreeNode> iterator = node.children
.iterator(); iterator.hasNext();) {
SpaceTreeNode child = (SpaceTreeNode) iterator.next();
if (child.expanded)
collapseNode(child);
}
}
}
private int direction = TOP_DOWN;
private double leafGap = 15;
private double branchGap = leafGap + 5;
private double layerGap = 20;
/**
* Sets the distance between leaf nodes to the given value. Default value is
* 15.
*
* @param value
* The new distance between leaf nodes.
*
*/
public void setLeafGap(double value) {
this.leafGap = value;
}
/**
* Sets the distance between branches to the given value. Default value is
* 20.
*
* @param value
* The new distance between branches.
*
*/
public void setBranchGap(double value) {
this.branchGap = value;
}
/**
* Sets the distance between layers to the given value. Default value is 20.
*
* @param value
* The new distance between layers.
*
*/
public void setLayerGap(double value) {
this.layerGap = value;
}
/**
* Returns the distance between leaf nodes. Default value is 15.
*
* @return The distance between leaf nodes.
*/
public double getLeafGap() {
return this.leafGap;
}
/**
* Returns the distance between branches. Default value is 20.
*
* @return The distance between branches.
*/
public double getBranchGap() {
return this.branchGap;
}
/**
* Returns the distance between layers. Default value is 20.
*
* @return The distance between layers.
*/
public double getLayerGap() {
return this.layerGap;
}
private TreeLayoutHelper treeObserver;
private double availableSpace;
private ArrayList<SpaceTreeLayer> spaceTreeLayers = new ArrayList<>();
/**
* If not null, this node and all of its children shall not be collapsed
* during node movements.
*/
private SpaceTreeNode protectedNode = null;
private Rectangle bounds;
/**
* Constructs an instance of <code>SpaceTreeLayoutAlgorithm</code> that
* places the root of a tree at the top of the graph.
*/
public SpaceTreeLayoutAlgorithm() {
}
/**
* Constructs an instance of <code>SpaceTreeLayoutAlgorithm</code> that
* places the root of a tree according to given direction
*
* @param direction
* direction of the tree, sould be one of the following:
* {@link #TOP_DOWN}, {@link #BOTTOM_UP}, {@link #LEFT_RIGHT},
* {@link #RIGHT_LEFT}.
*/
public SpaceTreeLayoutAlgorithm(int direction) {
setDirection(direction);
}
/**
*
* @return current direction (placement) of the tree
*/
public int getDirection() {
return direction;
}
/**
* Sets direction (placement) of the tree
*
* @param direction
* direction of the tree, sould be one of the following:
* {@link #TOP_DOWN}, {@link #BOTTOM_UP}, {@link #LEFT_RIGHT},
* {@link #RIGHT_LEFT}.
*/
public void setDirection(int direction) {
if (direction == this.direction)
return;
if (direction == TOP_DOWN || direction == BOTTOM_UP
|| direction == LEFT_RIGHT || direction == RIGHT_LEFT) {
this.direction = direction;
} else
throw new IllegalArgumentException(
"Invalid direction: " + direction);
}
public void applyLayout(LayoutContext layoutContext, boolean clean) {
bounds = LayoutProperties.getBounds(layoutContext.getGraph());
if (bounds.isEmpty()) {
return;
}
treeObserver = new TreeLayoutHelper(spaceTreeNodeFactory);
treeObserver.computeTree(layoutContext.getNodes());
if (clean) {
maximizeExpansion((SpaceTreeNode) treeObserver.getSuperRoot());
}
SpaceTreeNode superRoot = ((SpaceTreeNode) treeObserver.getSuperRoot());
superRoot.flushExpansionChanges();
superRoot.flushLocationChanges(0);
}
private void maximizeExpansion(SpaceTreeNode nodeToExpand) {
protectedNode = nodeToExpand;
double availableSpace = getAvailableSpace();
double requiredSpace = 0;
spaceTreeLayers.get(nodeToExpand.depth + 1)
.removeNodes(nodeToExpand.children);
ArrayList<TreeNode> nodesInThisLayer = null;
ArrayList<TreeNode> nodesInNextLayer = new ArrayList<>();
nodesInNextLayer.add(nodeToExpand);
double spaceRequiredInNextLayer = nodeToExpand.spaceRequiredForNode();
for (int layer = 0; !nodesInNextLayer.isEmpty(); layer++) {
NodeSnapshot[][] snapShot = takeSnapShot();
requiredSpace = Math.max(requiredSpace, spaceRequiredInNextLayer);
spaceRequiredInNextLayer = 0;
nodesInThisLayer = nodesInNextLayer;
nodesInNextLayer = new ArrayList<>();
int numOfNodesWithChildren = 0;
for (Iterator<TreeNode> iterator = nodesInThisLayer
.iterator(); iterator.hasNext();) {
SpaceTreeNode node = (SpaceTreeNode) iterator.next();
if (!node.children.isEmpty()) {
node.expanded = true;
spaceRequiredInNextLayer += node.spaceRequiredForChildren();
nodesInNextLayer.addAll(node.children);
numOfNodesWithChildren++;
}
}
for (Iterator<TreeNode> iterator = nodesInNextLayer
.iterator(); iterator.hasNext();) {
SpaceTreeNode node = (SpaceTreeNode) iterator.next();
node.expanded = false;
}
if (numOfNodesWithChildren == 0)
break;
spaceRequiredInNextLayer += branchGap
* (numOfNodesWithChildren - 1);
boolean addedNewLayer = false;
if ((spaceRequiredInNextLayer <= requiredSpace
|| spaceRequiredInNextLayer <= availableSpace
|| (layer < 1 && nodeToExpand.depth + layer < 1))
&& !nodesInNextLayer.isEmpty()) {
// add next layer and center its nodes
SpaceTreeLayer childLayer = spaceTreeLayers
.get(nodeToExpand.depth + layer + 1);
childLayer.addNodes(nodesInNextLayer);
SpaceTreeNode firstChild = ((SpaceTreeNode) nodesInNextLayer
.get(0));
SpaceTreeNode lastChild = ((SpaceTreeNode) nodesInNextLayer
.get(nodesInNextLayer.size() - 1));
double boundsWidth = spaceRequiredInNextLayer
- firstChild.spaceRequiredForNode() / 2
- lastChild.spaceRequiredForNode() / 2;
double startPosition = Math.max(
(availableSpace - boundsWidth) / 2,
firstChild.spaceRequiredForNode() / 2);
setAvailableSpace(spaceRequiredInNextLayer);
childLayer.fitNodesWithinBounds(nodesInNextLayer, startPosition,
startPosition + boundsWidth);
setAvailableSpace(0);
if (nodeToExpand.childrenPositionsOK(nodesInThisLayer)
|| layer == 0 || nodeToExpand.depth + layer < 1)
addedNewLayer = true;
}
if (!addedNewLayer) {
revertToSnapshot(snapShot);
break;
}
}
nodeToExpand.centerParentsBottomUp();
nodeToExpand.centerParentsTopDown();
}
/**
* Available space is the biggest of the following values:
* <ul>
* <li>Space provided by current context bounds</li>
* <li>Space already taken by the widest layer</li>
* <li>Value set with {@link #setAvailableSpace(double)}</li>
* </ul>
*
* @return
*/
private double getAvailableSpace() {
double result = (direction == TOP_DOWN || direction == BOTTOM_UP)
? bounds.getWidth() : bounds.getHeight();
result = Math.max(result, this.availableSpace);
for (Iterator<SpaceTreeLayer> iterator = spaceTreeLayers
.iterator(); iterator.hasNext();) {
SpaceTreeLayer layer = iterator.next();
if (!layer.nodes.isEmpty()) {
SpaceTreeNode first = layer.nodes.get(0);
SpaceTreeNode last = layer.nodes.get(layer.nodes.size() - 1);
result = Math
.max(result,
last.positionInLayer - first.positionInLayer
+ (first.spaceRequiredForNode()
+ last.spaceRequiredForNode())
/ 2);
} else
break;
}
return result;
}
/**
* This method allows to reserve more space than actual layout bounds
* provide or nodes currently occupy.
*
* @param availableSpace
*/
private void setAvailableSpace(double availableSpace) {
this.availableSpace = availableSpace;
}
private double expectedDistance(SpaceTreeNode node,
SpaceTreeNode neighbor) {
double expectedDistance = (node.spaceRequiredForNode()
+ neighbor.spaceRequiredForNode()) / 2;
expectedDistance += (node.parent == neighbor.parent) ? leafGap
: branchGap;
return expectedDistance;
}
private class NodeSnapshot {
SpaceTreeNode node;
double position;
boolean expanded;
}
/**
* Stores current expansion state of tree nodes and their position in layers
*
* @return array containing state of all unpruned nodes
*/
private NodeSnapshot[][] takeSnapShot() {
NodeSnapshot[][] result = new NodeSnapshot[spaceTreeLayers.size()][];
for (int i = 0; i < result.length; i++) {
SpaceTreeLayer layer = spaceTreeLayers.get(i);
result[i] = new NodeSnapshot[layer.nodes.size()];
for (int j = 0; j < result[i].length; j++) {
result[i][j] = new NodeSnapshot();
result[i][j].node = layer.nodes.get(j);
result[i][j].position = result[i][j].node.positionInLayer;
result[i][j].expanded = result[i][j].node.expanded;
}
}
return result;
}
/**
* Restores tree nodes' expansion state and position in layers
*
* @param snapShot
* state obtained with {@link #takeSnapShot()}
*/
private void revertToSnapshot(NodeSnapshot[][] snapShot) {
for (int i = 0; i < snapShot.length; i++) {
SpaceTreeLayer layer = spaceTreeLayers.get(i);
layer.nodes.clear();
for (int j = 0; j < snapShot[i].length; j++) {
snapShot[i][j].node.positionInLayer = snapShot[i][j].position;
snapShot[i][j].node.expanded = snapShot[i][j].expanded;
layer.nodes.add(snapShot[i][j].node);
}
}
}
}