/*
* Copyright 2015 JBoss, by Red Hat, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.uberfire.ext.wires.core.trees.client.layout.treelayout;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.uberfire.commons.validation.PortablePreconditions;
/**
* Implements the actual tree layout algorithm.
* <p>
* The nodes with their final layout can be retrieved through
* {@link #getNodeBounds()}.
* <p>
* @param <TreeNode> <p/>
* <p>
* Adapted from https://code.google.com/p/treelayout/ to be available to GWT clients
* <p>
* @author Udo Borkowski (ub@abego.org)
*/
public class TreeLayout<TreeNode> {
/*
* Differences between this implementation and original algorithm
* --------------------------------------------------------------
*
* For easier reference the same names (or at least similar names) as in the
* paper of Buchheim, Jünger, and Leipert are used in this
* implementation. However in the external interface "first" and "last" are
* used instead of "left most" and "right most". The implementation also
* supports tree layouts with the root at the left (or right) side. In that
* case using "left most" would refer to the "top" child, i.e. using "first"
* is less confusing.
*
* Also the y coordinate is not the level but directly refers the y
* coordinate of a level, taking node's height and gapBetweenLevels into
* account. When the root is at the left or right side the y coordinate
* actually becomes an x coordinate.
*
* Instead of just using a constant "distance" to calculate the position to
* the next node we refer to the "size" (width or height) of the node and a
* "gapBetweenNodes".
*/
// ------------------------------------------------------------------------
// tree
private final TreeForTreeLayout<TreeNode> tree;
private final NodeExtentProvider<TreeNode> nodeExtentProvider;
// ------------------------------------------------------------------------
// nodeExtentProvider
private final Configuration<TreeNode> configuration;
private final List<Double> sizeOfLevel = new ArrayList<Double>();
private final boolean useIdentity;
private final Map<TreeNode, Double> mod;
private final Map<TreeNode, TreeNode> thread;
private final Map<TreeNode, Double> prelim;
private final Map<TreeNode, Double> change;
// ------------------------------------------------------------------------
// configuration
private final Map<TreeNode, Double> shift;
private final Map<TreeNode, TreeNode> ancestor;
private final Map<TreeNode, Integer> number;
private final Map<TreeNode, Point2D> positions;
// ------------------------------------------------------------------------
// bounds
private double boundsLeft = Double.MAX_VALUE;
private double boundsRight = Double.MIN_VALUE;
private double boundsTop = Double.MAX_VALUE;
private double boundsBottom = Double.MIN_VALUE;
private Map<TreeNode, Rectangle2D> nodeBounds;
/**
* Creates a TreeLayout for a given tree.
* <p>
* In addition to the tree the {@link NodeExtentProvider} and the
* {@link Configuration} must be given.
* @param useIdentity [default: false] when true, identity ("==") is used instead of
* equality ("equals(...)") when checking nodes. Within a tree
* each node must only be once (using this check).
*/
public TreeLayout(TreeForTreeLayout<TreeNode> tree,
NodeExtentProvider<TreeNode> nodeExtentProvider,
Configuration<TreeNode> configuration,
boolean useIdentity) {
this.tree = tree;
this.nodeExtentProvider = nodeExtentProvider;
this.configuration = configuration;
this.useIdentity = useIdentity;
if (this.useIdentity) {
this.mod = new IdentityHashMap<TreeNode, Double>();
this.thread = new IdentityHashMap<TreeNode, TreeNode>();
this.prelim = new IdentityHashMap<TreeNode, Double>();
this.change = new IdentityHashMap<TreeNode, Double>();
this.shift = new IdentityHashMap<TreeNode, Double>();
this.ancestor = new IdentityHashMap<TreeNode, TreeNode>();
this.number = new IdentityHashMap<TreeNode, Integer>();
this.positions = new IdentityHashMap<TreeNode, Point2D>();
} else {
this.mod = new HashMap<TreeNode, Double>();
this.thread = new HashMap<TreeNode, TreeNode>();
this.prelim = new HashMap<TreeNode, Double>();
this.change = new HashMap<TreeNode, Double>();
this.shift = new HashMap<TreeNode, Double>();
this.ancestor = new HashMap<TreeNode, TreeNode>();
this.number = new HashMap<TreeNode, Integer>();
this.positions = new HashMap<TreeNode, Point2D>();
}
// No need to explicitly set mod, thread and ancestor as their getters
// are taking care of the initial values. This avoids a full tree walk
// through and saves some memory as no entries are added for
// "initial values".
TreeNode r = tree.getRoot();
firstWalk(r,
null);
calcSizeOfLevels(r,
0);
secondWalk(r,
-getPrelim(r),
0,
0);
}
// ------------------------------------------------------------------------
// size of level
public TreeLayout(TreeForTreeLayout<TreeNode> tree,
NodeExtentProvider<TreeNode> nodeExtentProvider,
Configuration<TreeNode> configuration) {
this(tree,
nodeExtentProvider,
configuration,
false);
}
/**
* Returns the Tree the layout is created for.
*/
public TreeForTreeLayout<TreeNode> getTree() {
return tree;
}
/**
* Returns the {@link NodeExtentProvider} used by this {@link TreeLayout}.
*/
public NodeExtentProvider<TreeNode> getNodeExtentProvider() {
return nodeExtentProvider;
}
private double getNodeHeight(TreeNode node) {
return nodeExtentProvider.getHeight(node);
}
// ------------------------------------------------------------------------
// NormalizedPosition
private double getNodeWidth(TreeNode node) {
return nodeExtentProvider.getWidth(node);
}
// ------------------------------------------------------------------------
// The Algorithm
private double getWidthOrHeightOfNode(TreeNode treeNode,
boolean returnWidth) {
return returnWidth ? getNodeWidth(treeNode) : getNodeHeight(treeNode);
}
/**
* When the level changes in Y-axis (i.e. root location Top or Bottom) the
* height of a node is its thickness, otherwise the node's width is its
* thickness.
* <p>
* The thickness of a node is used when calculating the locations of the
* levels.
* @param treeNode
* @return
*/
private double getNodeThickness(TreeNode treeNode) {
return getWidthOrHeightOfNode(treeNode,
!isLevelChangeInYAxis());
}
/**
* When the level changes in Y-axis (i.e. root location Top or Bottom) the
* width of a node is its size, otherwise the node's height is its size.
* <p>
* The size of a node is used when calculating the distance between two
* nodes.
* @param treeNode
* @return
*/
private double getNodeSize(TreeNode treeNode) {
return getWidthOrHeightOfNode(treeNode,
isLevelChangeInYAxis());
}
/**
* Returns the Configuration used by this {@link TreeLayout}.
*/
public Configuration<TreeNode> getConfiguration() {
return configuration;
}
private boolean isLevelChangeInYAxis() {
Configuration.Location rootLocation = configuration.getRootLocation();
return rootLocation == Configuration.Location.Top || rootLocation == Configuration.Location.Bottom;
}
private int getLevelChangeSign() {
Configuration.Location rootLocation = configuration.getRootLocation();
return rootLocation == Configuration.Location.Bottom
|| rootLocation == Configuration.Location.Right ? -1 : 1;
}
private void updateBounds(TreeNode node,
double centerX,
double centerY) {
double width = getNodeWidth(node);
double height = getNodeHeight(node);
double left = centerX - width / 2;
double right = centerX + width / 2;
double top = centerY - height / 2;
double bottom = centerY + height / 2;
if (boundsLeft > left) {
boundsLeft = left;
}
if (boundsRight < right) {
boundsRight = right;
}
if (boundsTop > top) {
boundsTop = top;
}
if (boundsBottom < bottom) {
boundsBottom = bottom;
}
}
/**
* Returns the bounds of the tree layout.
* <p>
* The bounds of a TreeLayout is the smallest rectangle containing the
* bounds of all nodes in the layout. It always starts at (0,0).
* @return the bounds of the tree layout
*/
public Rectangle2D getBounds() {
return new Rectangle2D(0,
0,
boundsRight - boundsLeft,
boundsBottom - boundsTop);
}
private void calcSizeOfLevels(TreeNode node,
int level) {
double oldSize;
if (sizeOfLevel.size() <= level) {
sizeOfLevel.add(Double.valueOf(0));
oldSize = 0;
} else {
oldSize = sizeOfLevel.get(level);
}
double size = getNodeThickness(node);
// size = nodeExtentProvider.getHeight(node);
if (oldSize < size) {
sizeOfLevel.set(level,
size);
}
if (!tree.isLeaf(node)) {
for (TreeNode child : tree.getChildren(node)) {
calcSizeOfLevels(child,
level + 1);
}
}
}
/**
* Returns the number of levels of the tree.
* @return [level > 0]
*/
public int getLevelCount() {
return sizeOfLevel.size();
}
/**
* Returns the size of a level.
* <p>
* When the root is located at the top or bottom the size of a level is the
* maximal height of the nodes of that level. When the root is located at
* the left or right the size of a level is the maximal width of the nodes
* of that level.
* @param level
* @return the size of the level [level >= 0 && level < levelCount]
*/
public double getSizeOfLevel(int level) {
PortablePreconditions.checkCondition("level must be >= 0",
level >= 0);
PortablePreconditions.checkCondition("level must be < levelCount",
level < getLevelCount());
return sizeOfLevel.get(level);
}
private double getMod(TreeNode node) {
Double d = mod.get(node);
return d != null ? d.doubleValue() : 0;
}
private void setMod(TreeNode node,
double d) {
mod.put(node,
d);
}
private TreeNode getThread(TreeNode node) {
TreeNode n = thread.get(node);
return n != null ? n : null;
}
private void setThread(TreeNode node,
TreeNode thread) {
this.thread.put(node,
thread);
}
private TreeNode getAncestor(TreeNode node) {
TreeNode n = ancestor.get(node);
return n != null ? n : node;
}
private void setAncestor(TreeNode node,
TreeNode ancestor) {
this.ancestor.put(node,
ancestor);
}
private double getPrelim(TreeNode node) {
Double d = prelim.get(node);
return d != null ? d.doubleValue() : 0;
}
private void setPrelim(TreeNode node,
double d) {
prelim.put(node,
d);
}
private double getChange(TreeNode node) {
Double d = change.get(node);
return d != null ? d.doubleValue() : 0;
}
private void setChange(TreeNode node,
double d) {
change.put(node,
d);
}
private double getShift(TreeNode node) {
Double d = shift.get(node);
return d != null ? d.doubleValue() : 0;
}
private void setShift(TreeNode node,
double d) {
shift.put(node,
d);
}
/**
* The distance of two nodes is the distance of the centers of both noded.
* <p>
* I.e. the distance includes the gap between the nodes and half of the
* sizes of the nodes.
* @param v
* @param w
* @return the distance between node v and w
*/
private double getDistance(TreeNode v,
TreeNode w) {
double sizeOfNodes = getNodeSize(v) + getNodeSize(w);
double distance = sizeOfNodes / 2 + configuration.getGapBetweenNodes(v,
w);
return distance;
}
private TreeNode nextLeft(TreeNode v) {
return tree.isLeaf(v) ? getThread(v) : tree.getFirstChild(v);
}
private TreeNode nextRight(TreeNode v) {
return tree.isLeaf(v) ? getThread(v) : tree.getLastChild(v);
}
/**
* @param node [tree.isChildOfParent(node, parentNode)]
* @param parentNode parent of node
* @return
*/
private int getNumber(TreeNode node,
TreeNode parentNode) {
Integer n = number.get(node);
if (n == null) {
int i = 1;
for (TreeNode child : tree.getChildren(parentNode)) {
number.put(child,
i++);
}
n = number.get(node);
}
return n.intValue();
}
/**
* @param vIMinus
* @param v
* @param parentOfV
* @param defaultAncestor
* @return the greatest distinct ancestor of vIMinus and its right neighbor
* v
*/
private TreeNode ancestor(TreeNode vIMinus,
TreeNode v,
TreeNode parentOfV,
TreeNode defaultAncestor) {
TreeNode ancestor = getAncestor(vIMinus);
// when the ancestor of vIMinus is a sibling of v (i.e. has the same
// parent as v) it is also the greatest distinct ancestor vIMinus and
// v. Otherwise it is the defaultAncestor
return tree.isChildOfParent(ancestor,
parentOfV) ? ancestor : defaultAncestor;
}
private void moveSubtree(TreeNode wMinus,
TreeNode wPlus,
TreeNode parent,
double shift) {
int subtrees = getNumber(wPlus,
parent) - getNumber(wMinus,
parent);
setChange(wPlus,
getChange(wPlus) - shift / subtrees);
setShift(wPlus,
getShift(wPlus) + shift);
setChange(wMinus,
getChange(wMinus) + shift / subtrees);
setPrelim(wPlus,
getPrelim(wPlus) + shift);
setMod(wPlus,
getMod(wPlus) + shift);
}
/**
* In difference to the original algorithm we also pass in the leftSibling
* and the parent of v.
* <p>
* <b>Why adding the parameter 'parent of v' (parentOfV) ?</b>
* <p>
* In this method we need access to the parent of v. Not every tree
* implementation may support efficient (i.e. constant time) access to it.
* On the other hand the (only) caller of this method can provide this
* information with only constant extra time.
* <p>
* Also we need access to the "left most sibling" of v. Not every tree
* implementation may support efficient (i.e. constant time) access to it.
* On the other hand the "left most sibling" of v is also the "first child"
* of the parent of v. The first child of a parent node we can get in
* constant time. As we got the parent of v we can so also get the
* "left most sibling" of v in constant time.
* <p>
* <b>Why adding the parameter 'leftSibling' ?</b>
* <p>
* In this method we need access to the "left sibling" of v. Not every tree
* implementation may support efficient (i.e. constant time) access to it.
* However it is easy for the caller of this method to provide this
* information with only constant extra time.
* <p>
* <p>
* <p>
* In addition these extra parameters avoid the need for
* {@link TreeForTreeLayout} to include extra methods "getParent",
* "getLeftSibling", or "getLeftMostSibling". This keeps the interface
* {@link TreeForTreeLayout} small and avoids redundant implementations.
* @param v
* @param defaultAncestor
* @param leftSibling [nullable] the left sibling v, if there is any
* @param parentOfV the parent of v
* @return the (possibly changes) defaultAncestor
*/
private TreeNode apportion(TreeNode v,
TreeNode defaultAncestor,
TreeNode leftSibling,
TreeNode parentOfV) {
TreeNode w = leftSibling;
if (w == null) {
// v has no left sibling
return defaultAncestor;
}
// v has left sibling w
// The following variables "v..." are used to traverse the contours to
// the subtrees. "Minus" refers to the left, "Plus" to the right
// subtree. "I" refers to the "inside" and "O" to the outside contour.
TreeNode vOPlus = v;
TreeNode vIPlus = v;
TreeNode vIMinus = w;
// get leftmost sibling of vIPlus, i.e. get the leftmost sibling of
// v, i.e. the leftmost child of the parent of v (which is passed
// in)
TreeNode vOMinus = tree.getFirstChild(parentOfV);
Double sIPlus = getMod(vIPlus);
Double sOPlus = getMod(vOPlus);
Double sIMinus = getMod(vIMinus);
Double sOMinus = getMod(vOMinus);
TreeNode nextRightVIMinus = nextRight(vIMinus);
TreeNode nextLeftVIPlus = nextLeft(vIPlus);
while (nextRightVIMinus != null && nextLeftVIPlus != null) {
vIMinus = nextRightVIMinus;
vIPlus = nextLeftVIPlus;
vOMinus = nextLeft(vOMinus);
vOPlus = nextRight(vOPlus);
setAncestor(vOPlus,
v);
double shift = (getPrelim(vIMinus) + sIMinus)
- (getPrelim(vIPlus) + sIPlus)
+ getDistance(vIMinus,
vIPlus);
if (shift > 0) {
moveSubtree(ancestor(vIMinus,
v,
parentOfV,
defaultAncestor),
v,
parentOfV,
shift);
sIPlus = sIPlus + shift;
sOPlus = sOPlus + shift;
}
sIMinus = sIMinus + getMod(vIMinus);
sIPlus = sIPlus + getMod(vIPlus);
sOMinus = sOMinus + getMod(vOMinus);
sOPlus = sOPlus + getMod(vOPlus);
nextRightVIMinus = nextRight(vIMinus);
nextLeftVIPlus = nextLeft(vIPlus);
}
if (nextRightVIMinus != null && nextRight(vOPlus) == null) {
setThread(vOPlus,
nextRightVIMinus);
setMod(vOPlus,
getMod(vOPlus) + sIMinus - sOPlus);
}
if (nextLeftVIPlus != null && nextLeft(vOMinus) == null) {
setThread(vOMinus,
nextLeftVIPlus);
setMod(vOMinus,
getMod(vOMinus) + sIPlus - sOMinus);
defaultAncestor = v;
}
return defaultAncestor;
}
/**
* @param v [!tree.isLeaf(v)]
*/
private void executeShifts(TreeNode v) {
double shift = 0;
double change = 0;
for (TreeNode w : tree.getChildrenReverse(v)) {
change = change + getChange(w);
setPrelim(w,
getPrelim(w) + shift);
setMod(w,
getMod(w) + shift);
shift = shift + getShift(w) + change;
}
}
// ------------------------------------------------------------------------
// nodeBounds
/**
* In difference to the original algorithm we also pass in the leftSibling
* (see {@link #apportion(Object, Object, Object, Object)} for a
* motivation).
* @param v
* @param leftSibling [nullable] the left sibling v, if there is any
*/
private void firstWalk(TreeNode v,
TreeNode leftSibling) {
if (tree.isLeaf(v)) {
// No need to set prelim(v) to 0 as the getter takes care of this.
TreeNode w = leftSibling;
if (w != null) {
// v has left sibling
setPrelim(v,
getPrelim(w) + getDistance(v,
w));
}
} else {
// v is not a leaf
TreeNode defaultAncestor = tree.getFirstChild(v);
TreeNode previousChild = null;
for (TreeNode w : tree.getChildren(v)) {
firstWalk(w,
previousChild);
defaultAncestor = apportion(w,
defaultAncestor,
previousChild,
v);
previousChild = w;
}
executeShifts(v);
double midpoint = (getPrelim(tree.getFirstChild(v)) + getPrelim(tree.getLastChild(v))) / 2.0;
TreeNode w = leftSibling;
if (w != null) {
// v has left sibling
setPrelim(v,
getPrelim(w) + getDistance(v,
w));
setMod(v,
getPrelim(v) - midpoint);
} else {
// v has no left sibling
setPrelim(v,
midpoint);
}
}
}
/**
* In difference to the original algorithm we also pass in extra level
* information.
* @param v
* @param m
* @param level
* @param levelStart
*/
private void secondWalk(TreeNode v,
double m,
int level,
double levelStart) {
// construct the position from the prelim and the level information
// The rootLocation affects the way how x and y are changed and in what
// direction.
double levelChangeSign = getLevelChangeSign();
boolean levelChangeOnYAxis = isLevelChangeInYAxis();
double levelSize = getSizeOfLevel(level);
double x = getPrelim(v) + m;
double y;
Configuration.AlignmentInLevel alignment = configuration.getAlignmentInLevel();
if (alignment == Configuration.AlignmentInLevel.Center) {
y = levelStart + levelChangeSign * (levelSize / 2);
} else if (alignment == Configuration.AlignmentInLevel.TowardsRoot) {
y = levelStart + levelChangeSign * (getNodeThickness(v) / 2);
} else {
y = levelStart + levelSize - levelChangeSign * (getNodeThickness(v) / 2);
}
if (!levelChangeOnYAxis) {
double t = x;
x = y;
y = t;
}
positions.put(v,
new NormalizedPosition(x,
y));
// update the bounds
updateBounds(v,
x,
y);
// recurse
if (!tree.isLeaf(v)) {
double nextLevelStart = levelStart
+ (levelSize + configuration.getGapBetweenLevels(level + 1))
* levelChangeSign;
for (TreeNode w : tree.getChildren(v)) {
secondWalk(w,
m + getMod(v),
level + 1,
nextLevelStart);
}
}
}
// ------------------------------------------------------------------------
// constructor
/**
* Returns the layout of the tree nodes by mapping each node of the tree to
* its bounds (position and size).
* <p>
* For each rectangle x and y will be >= 0. At least one rectangle will have
* an x == 0 and at least one rectangle will have an y == 0.
* @return maps each node of the tree to its bounds (position and size).
*/
public Map<TreeNode, Rectangle2D> getNodeBounds() {
if (nodeBounds == null) {
nodeBounds = this.useIdentity ? new IdentityHashMap<TreeNode, Rectangle2D>()
: new HashMap<TreeNode, Rectangle2D>();
for (Map.Entry<TreeNode, Point2D> entry : positions.entrySet()) {
TreeNode node = entry.getKey();
Point2D pos = entry.getValue();
double w = getNodeWidth(node);
double h = getNodeHeight(node);
double x = pos.getX() - w / 2;
double y = pos.getY() - h / 2;
nodeBounds.put(node,
new Rectangle2D(x,
y,
w,
h));
}
}
return nodeBounds;
}
private void addUniqueNodes(Map<TreeNode, TreeNode> nodes,
TreeNode newNode) {
if (nodes.put(newNode,
newNode) != null) {
throw new RuntimeException("Node used more than once in tree: " + newNode);
}
for (TreeNode n : tree.getChildren(newNode)) {
addUniqueNodes(nodes,
n);
}
}
// ------------------------------------------------------------------------
// checkTree
/**
* Check if the tree is a "valid" tree.
* <p>
* Typically you will use this method during development when you get an
* unexpected layout from your trees.
* <p>
* The following checks are performed:
* <ul>
* <li>Each node must only occur once in the tree.</li>
* </ul>
*/
public void checkTree() {
Map<TreeNode, TreeNode> nodes = this.useIdentity ? new IdentityHashMap<TreeNode, TreeNode>()
: new HashMap<TreeNode, TreeNode>();
// Traverse the tree and check if each node is only used once.
addUniqueNodes(nodes,
tree.getRoot());
}
/**
* The algorithm calculates the position starting with the root at 0. I.e.
* the left children will get negative positions. However we want the result
* to be normalized to (0,0).
* <p>
* {@link NormalizedPosition} will normalize the position (given relative to
* the root position), taking the current bounds into account. This way the
* left most node bounds will start at x = 0, the top most node bounds at y
* = 0.
*/
private class NormalizedPosition extends Point2D {
private double x_relativeToRoot;
private double y_relativeToRoot;
public NormalizedPosition(double x_relativeToRoot,
double y_relativeToRoot) {
setLocation(x_relativeToRoot,
y_relativeToRoot);
}
@Override
public double getX() {
return x_relativeToRoot - boundsLeft;
}
@Override
public double getY() {
return y_relativeToRoot - boundsTop;
}
@Override
// never called from outside
public void setLocation(double x_relativeToRoot,
double y_relativeToRoot) {
this.x_relativeToRoot = x_relativeToRoot;
this.y_relativeToRoot = y_relativeToRoot;
}
}
}