/*
* TreePane.java
*
* Copyright (C) 2006-2014 Andrew Rambaut
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package figtree.treeviewer;
import figtree.treeviewer.painters.Painter;
import jebl.evolution.graphs.Node;
import jebl.evolution.graphs.Graph;
import jebl.evolution.taxa.Taxon;
import jebl.evolution.trees.*;
import figtree.treeviewer.decorators.*;
import figtree.treeviewer.painters.*;
import figtree.treeviewer.treelayouts.*;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.print.*;
import java.util.*;
import java.util.List;
/**
* @author Andrew Rambaut
* @version $Id$
*
* $HeadURL$
*
* $LastChangedBy$
* $LastChangedDate$
* $LastChangedRevision$
*/
public class TreePane extends JComponent implements PainterListener, Printable {
public final static boolean DEBUG_OUTLINE = false;
public enum RootingType {
USER_ROOTING("User Selection"),
MID_POINT("Midpoint");
// LEAST_SQUARES("least squares");
RootingType(String name) {
this.name = name;
}
public String toString() { return name; }
private String name;
}
public final String CARTOON_ATTRIBUTE_NAME = "!cartoon";
public final String COLLAPSE_ATTRIBUTE_NAME = "!collapse";
public final String HILIGHT_ATTRIBUTE_NAME = "!hilight";
public TreePane() {
}
public RootedTree getTree() {
return tree;
}
public void setTree(RootedTree tree) {
if (tree != null) {
this.originalTree = tree;
if (!originalTree.hasLengths()) {
transformBranchesOn = true;
}
setupTree();
} else {
originalTree = null;
this.tree = null;
invalidate();
repaint();
}
}
private void recalibrate() {
calibrated = false;
}
private void setupTree() {
tree = constructTransformedTree(originalTree);
recalculateCollapsedNodes();
recalibrate();
invalidate();
repaint();
}
public RootedTree constructTransformedTree(RootedTree sourceTree) {
RootedTree newTree = sourceTree;
if (isRootingOn) {
if (rootingType == RootingType.MID_POINT) {
newTree = ReRootedTree.rootTreeAtCenter(newTree);
} else if (rootingType == RootingType.USER_ROOTING && rootingNode != null) {
Node left = newTree.getParent(rootingNode);
if (left != null) {
// rooting length should be [0, 1]
double length = newTree.hasLengths() ? newTree.getLength(rootingNode) * rootingLength : 1.0;
try {
newTree = new ReRootedTree(newTree, left, rootingNode, length);
} catch (Graph.NoEdgeException e) {
e.printStackTrace();
}
}
}
}
if (orderBranchesOn) {
newTree = new SortedRootedTree(newTree, branchOrdering);
}
if (transformBranchesOn || !sourceTree.hasLengths()) {
newTree = new TransformedRootedTree(newTree, branchTransform);
}
return newTree;
}
public TreeLayout getTreeLayout() {
return treeLayout;
}
public TreeLayoutCache getTreeLayoutCache() {
return treeLayoutCache;
}
public void setTreeLayout(TreeLayout treeLayout) {
this.treeLayout = treeLayout;
treeLayout.setCartoonAttributeName(CARTOON_ATTRIBUTE_NAME);
treeLayout.setCollapsedAttributeName(COLLAPSE_ATTRIBUTE_NAME);
treeLayout.setHilightAttributeName(HILIGHT_ATTRIBUTE_NAME);
treeLayout.setBranchColouringAttributeName(branchColouringAttribute);
treeLayout.addTreeLayoutListener(new TreeLayoutListener() {
public void treeLayoutChanged() {
recalibrate();
repaint();
}
});
recalibrate();
invalidate();
repaint();
}
public TimeScale getTimeScale() {
return timeScale;
}
public void setTimeScale(TimeScale timeScale) {
this.timeScale = timeScale;
this.timeScale.setReversed(isAxisReversed());
recalibrate();
repaint();
}
public boolean isCrosshairShown() {
return isCrosshairShown;
}
public void setCrosshairShown(boolean crosshairShown) {
isCrosshairShown = crosshairShown;
}
public void setCursorPosition(Point point) {
cursorPosition = point;
if (cursorPosition != null) {
double xPos = (point.getX() - treeBounds.getX()) / treeBounds.getWidth();
xPos = (xPos < 0.0 ? 0.0 : xPos > 1.0 ? 1.0 : xPos);
double yPos = (point.getY() - treeBounds.getY()) / treeBounds.getHeight();
yPos = (yPos < 0.0 ? 0.0 : yPos > 1.0 ? 1.0 : yPos);
treeLayout.setPointOfInterest(xPos, yPos);
}
}
public void midpointRoot() {
isRootingOn = true;
rootingType = RootingType.MID_POINT;
setupTree();
fireSettingsChanged();
}
public void setRootLocation(Node node, double length) {
RootedTree source = tree;
if (tree instanceof FilteredRootedTree) {
source = ((FilteredRootedTree) tree).getSource();
}
if (source instanceof ReRootedTree) {
rootingNode = ((ReRootedTree)source).getSourceNode(node);
} else {
rootingNode = node;
}
rootingLength = length;
isRootingOn = true;
rootingType = RootingType.USER_ROOTING;
setupTree();
fireSettingsChanged();
}
public void rotateNode(Node node) {
if (node != null) {
Boolean rotate = (Boolean)node.getAttribute("!rotate");
if (rotate != null) {
rotate = !rotate;
} else {
rotate = true;
}
node.setAttribute("!rotate", rotate);
recalibrate();
invalidate();
repaint();
}
}
public void clearRotation(Node node) {
if (node != null) {
Boolean rotate = (Boolean)node.getAttribute("!rotate");
if (rotate != null) {
node.removeAttribute("!rotate");
}
recalibrate();
invalidate();
repaint();
}
}
public void setBranchDecorator(Decorator branchDecorator, boolean isGradient) {
this.branchDecorator = branchDecorator;
this.branchDecoratorGradient = isGradient;
repaint();
}
public void setBranchColouringDecorator(String branchColouringAttribute, Decorator branchColouringDecorator) {
this.branchColouringAttribute = branchColouringAttribute;
treeLayout.setBranchColouringAttributeName(branchColouringAttribute);
this.branchColouringDecorator = branchColouringDecorator;
repaint();
}
public boolean isHilightingGradient() {
return hilightingGradient;
}
public void setHilightingGradient(boolean hilightingGradient) {
this.hilightingGradient = hilightingGradient;
repaint();
}
public void setNodeBackgroundDecorator(Decorator nodeBackgroundDecorator) {
this.nodeBackgroundDecorator = nodeBackgroundDecorator;
repaint();
}
public Rectangle2D getTreeBounds() {
return treeBounds;
}
/**
* This returns the scaling factor between the graphical image and the branch
* lengths of the tree
*
* @return the tree scale
*/
public double getTreeScale() {
return treeScale / timeScale.getScaleFactor(tree);
}
/**
* Transform a chart co-ordinates into a drawing co-ordinates
*/
public double scaleOnAxis(double value) {
double height = timeScale.getHeight(value, tree);
if (isAxisReversed()) {
return (treeBounds.getX() + treeBounds.getWidth()) - (height * treeScale);
} else {
return treeBounds.getX() + (height * treeScale);
}
}
public Shape getAxisLine(double value) {
if (isAxisReversed()) {
value = maxTreeHeight - value;
} else {
value -= rootHeightOffset;
}
double height = timeScale.getHeight(value, tree);
Shape line = treeLayout.getAxisLine(height);
if (line != null) {
return transform.createTransformedShape(line);
}
return null;
}
public ScaleAxis getScaleAxis() {
return scaleAxis;
}
public double getAxisOrigin() {
return axisOrigin;
}
public void setAxisOrigin(double axisOrigin) {
this.axisOrigin = axisOrigin;
recalibrate();
repaint();
}
public void setAxisReversed(final boolean isAxisReversed) {
this.isAxisReversed = isAxisReversed;
this.timeScale.setReversed(isAxisReversed());
recalibrate();
repaint();
}
public boolean isAxisReversed() {
return isAxisReversed;
}
private void setupScaleAxis() {
double minValue = timeScale.getAge(0.0, tree);
double maxValue = timeScale.getAge(maxTreeHeight, tree);
if (minValue < maxValue) {
if (axisOrigin < minValue) {
minValue = axisOrigin;
}
scaleAxis.setRange(minValue, maxValue);
} else {
if (axisOrigin > minValue) {
minValue = axisOrigin;
}
scaleAxis.setRange(maxValue, minValue);
}
}
public void setRootAge(double rootAge) {
double rootLength = timeScale.getHeight(rootAge, tree) - tree.getHeight(tree.getRootNode());
treeLayout.setRootLength(rootLength);
recalibrate();
repaint();
}
public double getRootAge() {
double treeHeight = tree.getHeight(tree.getRootNode()) + treeLayout.getRootLength();
return timeScale.getAge(treeHeight, tree);
}
public double getMajorTickSpacing() {
return scaleAxis.getMajorTickSpacing();
}
public double getMinorTickSpacing() {
return scaleAxis.getMinorTickSpacing();
}
public void setTickSpacing(double userMajorTickSpacing, double userMinorTickSpacing) {
scaleAxis.setManualAxis(userMajorTickSpacing, userMinorTickSpacing);
recalibrate();
repaint();
}
public void setAutomaticScale() {
scaleAxis.setAutomatic();
recalibrate();
repaint();
}
public void painterChanged() {
recalibrate();
repaint();
}
public void painterSettingsChanged() {
recalibrate();
repaint();
}
public void attributesChanged() {
recalibrate();
repaint();
}
public BasicStroke getBranchStroke() {
return branchLineStroke;
}
public void setBranchStroke(BasicStroke stroke) {
branchLineStroke = stroke;
float weight = stroke.getLineWidth();
selectionStroke = new BasicStroke(Math.max(weight + 4.0F, weight * 1.5F), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
repaint();
}
public BasicStroke getCalloutStroke() {
return calloutStroke;
}
public void setCalloutStroke(BasicStroke calloutStroke) {
this.calloutStroke = calloutStroke;
}
public Paint getSelectionPaint() {
return selectionPaint;
}
public void setSelectionColor(Color selectionColor) {
this.selectionPaint = new Color(
selectionColor.getRed(),
selectionColor.getGreen(),
selectionColor.getBlue(),
128);
}
public boolean isTransformBranchesOn() {
return transformBranchesOn;
}
public void setTransformBranchesOn(boolean transformBranchesOn) {
this.transformBranchesOn = transformBranchesOn;
setupTree();
}
public TransformedRootedTree.Transform getBranchTransform() {
return branchTransform;
}
public void setBranchTransform(TransformedRootedTree.Transform branchTransform) {
this.branchTransform = branchTransform;
setupTree();
}
public boolean isOrderBranchesOn() {
return orderBranchesOn;
}
public void setOrderBranchesOn(boolean orderBranchesOn) {
this.orderBranchesOn = orderBranchesOn;
setupTree();
}
public SortedRootedTree.BranchOrdering getBranchOrdering() {
return branchOrdering;
}
public void setBranchOrdering(SortedRootedTree.BranchOrdering branchOrdering) {
this.branchOrdering = branchOrdering;
setupTree();
}
public boolean isRootingOn() {
return isRootingOn;
}
public RootingType getRootingType() {
return rootingType;
}
public void setRootingOn(boolean rootingOn) {
this.isRootingOn = rootingOn;
setupTree();
}
public void setRootingType(RootingType rootingType) {
this.rootingType = rootingType;
setupTree();
}
public RootedTree getOriginalTree() {
return originalTree;
}
public boolean isShowingTipCallouts() {
return showingTipCallouts;
}
public void setShowingTipCallouts(boolean showingTipCallouts) {
this.showingTipCallouts = showingTipCallouts;
recalibrate();
repaint();
}
public void setSelectedNode(Node selectedNode) {
selectedNodes.clear();
selectedTips.clear();
addSelectedNode(selectedNode, false, false);
}
public void setSelectedTip(Node selectedTip) {
selectedNodes.clear();
selectedTips.clear();
addSelectedTip(selectedTip, false, false);
}
public void setSelectedClade(Node selectedNode) {
selectedNodes.clear();
selectedTips.clear();
addSelectedClade(selectedNode, false, false);
}
public void setSelectedTips(Node selectedNode) {
selectedNodes.clear();
selectedTips.clear();
addSelectedTips(selectedNode, false);
}
private boolean canSelectNode(Node selectedNode) {
return selectedNode != null;
}
public void addSelectedNode(Node selectedNode) {
addSelectedNode(selectedNode, false, false);
}
public void addSelectedNode(Node selectedNode, boolean toggle, boolean extend) {
amendNodeSelection(selectedNode, toggle, extend);
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void addSelectedTip(Node selectedTip) {
addSelectedTip(selectedTip, false, false);
}
public void addSelectedTip(Node selectedTip, boolean toggle, boolean extend) {
amendNodeSelection(selectedTip, toggle, extend);
selectTipsFromSelectedNodes();
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void addSelectedClade(Node selectedNode) {
addSelectedClade(selectedNode, false, false);
}
public void addSelectedClade(Node selectedNode, boolean toggle, boolean extend) {
if (canSelectNode(selectedNode)) {
amendCladeSelection(selectedNode, toggle, extend);
}
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
private void amendNodeSelection(Node selectedNode, boolean toggle, boolean extend) {
if ( !canSelectNode(selectedNode) ) {
return;
}
if (extend) {
Set<Node> nodeSet = new HashSet<Node>(selectedNodes);
nodeSet.add(selectedNode);
Node mrca = RootedTreeUtils.getCommonAncestorNode(tree, nodeSet);
for (Node node : nodeSet) {
while (node != null && node != mrca) {
amendNodeSelection(node, false, false);
node = tree.getParent(node);
}
}
} else {
if (toggle && selectedNodes.contains(selectedNode)) {
selectedNodes.remove(selectedNode);
} else {
selectedNodes.add(selectedNode);
}
}
}
private void amendCladeSelection(Node selectedNode, boolean toggle, boolean extend) {
if ( !canSelectNode(selectedNode) ) {
return;
}
if (extend) {
Set<Node> nodeSet = new HashSet<Node>(selectedNodes);
nodeSet.add(selectedNode);
Node mrca = RootedTreeUtils.getCommonAncestorNode(tree, nodeSet);
for (Node node : nodeSet) {
while (node != null && node != mrca) {
amendCladeSelection(node, false, false);
node = tree.getParent(node);
}
}
} else {
if (toggle && selectedNodes.contains(selectedNode)) {
selectedNodes.remove(selectedNode);
} else {
selectedNodes.add(selectedNode);
}
}
for (Node child : tree.getChildren(selectedNode)) {
amendCladeSelection(child, toggle, false);
}
}
public void addSelectedTips(Node selectedNode) {
addSelectedTips(selectedNode, false);
}
public void addSelectedTips(Node selectedNode, boolean toggle) {
if (selectedNode != null) {
addSelectedChildTips(selectedNode, toggle);
}
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
private void addSelectedChildTips(Node selectedNode, boolean toggle) {
if (tree.isExternal(selectedNode)) {
if (toggle && selectedTips.contains(selectedNode)) {
selectedTips.remove(selectedNode);
} else {
selectedTips.add(selectedNode);
}
} else {
for (Node child : tree.getChildren(selectedNode)) {
addSelectedChildTips(child, toggle);
}
}
}
public void selectCladesFromSelectedNodes() {
Set<Node> nodes = new HashSet<Node>(selectedNodes);
selectedNodes.clear();
for (Node node : nodes) {
addSelectedClade(node, false, false);
}
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void selectTipsFromSelectedNodes() {
for (Node node : selectedNodes) {
addSelectedChildTips(node, false);
}
selectedNodes.clear();
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void selectNodesFromSelectedTips() {
if (selectedTips.size() > 0) {
Node node = RootedTreeUtils.getCommonAncestorNode(tree, selectedTips);
addSelectedClade(node, false, false);
}
selectedTips.clear();
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void selectAllTaxa() {
selectedTips.addAll(tree.getExternalNodes());
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void selectAllNodes() {
selectedNodes.addAll(tree.getNodes());
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public void clearSelection() {
selectedNodes.clear();
selectedTips.clear();
fireSelectionChanged();
clearSelectionPaths();
repaint();
}
public boolean hasSelection() {
return selectedNodes.size() > 0 || selectedTips.size() > 0;
}
public void cartoonSelectedNodes() {
cartoonSelectedNodes(tree.getRootNode());
}
private void cartoonSelectedNodes(Node node) {
if (!tree.isExternal(node)) {
if (selectedNodes.contains(node)) {
if (node.getAttribute(CARTOON_ATTRIBUTE_NAME) != null) {
node.removeAttribute(CARTOON_ATTRIBUTE_NAME);
} else {
int tipCount = RootedTreeUtils.getTipCount(tree, node);
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipCount, height };
node.setAttribute(CARTOON_ATTRIBUTE_NAME, values);
}
recalibrate();
repaint();
} else {
for (Node child : tree.getChildren(node)) {
cartoonSelectedNodes(child);
}
}
}
}
public void collapseSelectedNodes() {
collapseSelectedNodes(tree.getRootNode());
}
private void collapseSelectedNodes(Node node) {
if (!tree.isExternal(node)) {
if (selectedNodes.contains(node)) {
if (node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
node.removeAttribute(COLLAPSE_ATTRIBUTE_NAME);
} else {
String tipName = "collapsed";
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipName, height };
node.setAttribute(COLLAPSE_ATTRIBUTE_NAME, values);
}
recalibrate();
repaint();
} else {
for (Node child : tree.getChildren(node)) {
collapseSelectedNodes(child);
}
}
}
}
public void hilightSelectedNodes(Color color) {
hilightSelectedNodes(tree.getRootNode(), color);
}
private void hilightSelectedNodes(Node node, Color color) {
if (!tree.isExternal(node)) {
if (selectedNodes.contains(node)) {
int tipCount = RootedTreeUtils.getTipCount(tree, node);
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipCount, height, color };
node.setAttribute(HILIGHT_ATTRIBUTE_NAME, values);
recalibrate();
repaint();
} else {
for (Node child : tree.getChildren(node)) {
hilightSelectedNodes(child, color);
}
}
}
}
public void recalculateCollapsedNodes() {
recalculateCollapsedNodes(tree.getRootNode());
}
private void recalculateCollapsedNodes(Node node) {
if (!tree.isExternal(node)) {
if (selectedNodes.contains(node)) {
if (node.getAttribute(CARTOON_ATTRIBUTE_NAME) != null) {
int tipCount = RootedTreeUtils.getTipCount(tree, node);
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipCount, height };
node.setAttribute(CARTOON_ATTRIBUTE_NAME, values);
}
if (node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
String tipName = "collapsed";
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipName, height };
node.setAttribute(COLLAPSE_ATTRIBUTE_NAME, values);
}
Object[] oldValues = (Object[])node.getAttribute(HILIGHT_ATTRIBUTE_NAME);
if (oldValues != null) {
int tipCount = RootedTreeUtils.getTipCount(tree, node);
double height = RootedTreeUtils.getMinTipHeight(tree, node);
Object[] values = new Object[] { tipCount, height, oldValues[2] };
node.setAttribute(HILIGHT_ATTRIBUTE_NAME, values);
}
recalibrate();
repaint();
} else {
for (Node child : tree.getChildren(node)) {
recalculateCollapsedNodes(child);
}
}
}
}
public void clearCollapsedNodes() {
if (selectedNodes.size() > 0) {
clearSelectedCollapsedNodes(tree.getRootNode());
} else {
for (Node node : tree.getInternalNodes()){
if (node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
node.removeAttribute(COLLAPSE_ATTRIBUTE_NAME);
}
if (node.getAttribute(CARTOON_ATTRIBUTE_NAME) != null) {
node.removeAttribute(CARTOON_ATTRIBUTE_NAME);
}
}
recalibrate();
repaint();
}
}
private void clearSelectedCollapsedNodes(Node node) {
if (!tree.isExternal(node)) {
// Although collapsed nodes could be nested, we don't go
// deeper. So one 'clear collapsed' will reveal any nested
// collapsed nodes.
if (selectedNodes.contains(node)) {
if (node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
node.removeAttribute(COLLAPSE_ATTRIBUTE_NAME);
}
if (node.getAttribute(CARTOON_ATTRIBUTE_NAME) != null) {
node.removeAttribute(CARTOON_ATTRIBUTE_NAME);
}
recalibrate();
repaint();
} else {
for (Node child : tree.getChildren(node)) {
clearSelectedCollapsedNodes(child);
}
}
}
}
public void clearHilightedNodes() {
if (selectedNodes.size() > 0) {
clearSelectedHilightedNodes(tree.getRootNode());
} else {
for (Node node : tree.getInternalNodes()){
if (node.getAttribute(HILIGHT_ATTRIBUTE_NAME) != null) {
node.removeAttribute(HILIGHT_ATTRIBUTE_NAME);
}
}
recalibrate();
repaint();
}
}
private void clearSelectedHilightedNodes(Node node) {
if (!tree.isExternal(node)) {
if (selectedNodes.size() == 0 || selectedNodes.contains(node)) {
if (node.getAttribute(HILIGHT_ATTRIBUTE_NAME) != null) {
node.removeAttribute(HILIGHT_ATTRIBUTE_NAME);
recalibrate();
repaint();
}
}
for (Node child : tree.getChildren(node)) {
clearSelectedHilightedNodes(child);
}
}
}
public void rerootOnSelectedBranch() {
for (Node selectedNode : selectedNodes) {
setRootLocation(selectedNode, 0.5);
// root on the first selected branch...
// Check for multiple selected branch elsewhere
return;
}
repaint();
}
public void clearRooting() {
rootingNode = null;
setupTree();
fireSettingsChanged();
}
public void rotateSelectedNode() {
for (Node selectedNode : selectedNodes) {
rotateNode(selectedNode);
}
repaint();
}
public void clearSelectedNodeRotations() {
if (selectedNodes.size() > 0) {
for (Node node : selectedNodes) {
clearRotation(node);
}
} else {
for (Node node : tree.getInternalNodes()) {
clearRotation(node);
}
}
repaint();
}
public void annotateSelectedNodes(String name, Object value) {
for (Node selectedNode : selectedNodes) {
selectedNode.setAttribute(name, value);
}
repaint();
}
public void annotateSelectedTips(String name, Object value) {
for (Node selectedTip : selectedTips) {
Taxon selectedTaxon = tree.getTaxon(selectedTip);
// if (selectedTaxon == null) {
// throw new IllegalArgumentException("missing taxon?");
// }
selectedTaxon.setAttribute(name, value);
}
repaint();
}
public void clearSelectedNodeAnnotation(String name) {
for (Node selectedNode : selectedNodes) {
selectedNode.removeAttribute(name);
}
repaint();
}
public void clearSelectedTipAnnotation(String name) {
for (Node selectedTip : selectedTips) {
Taxon selectedTaxon = tree.getTaxon(selectedTip);
selectedTaxon.removeAttribute(name);
}
repaint();
}
/**
* Return whether the two axis scales should be maintained
* relative to each other
*
* @return a boolean
*/
public boolean maintainAspectRatio() {
return treeLayout.maintainAspectRatio();
}
public void setTipLabelPainter(LabelPainter<Node> tipLabelPainter) {
tipLabelPainter.setTreePane(this);
if (this.tipLabelPainter != null) {
this.tipLabelPainter.removePainterListener(this);
}
this.tipLabelPainter = tipLabelPainter;
if (this.tipLabelPainter != null) {
this.tipLabelPainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public LabelPainter<Node> getTipLabelPainter() {
return tipLabelPainter;
}
public void setNodeLabelPainter(LabelPainter<Node> nodeLabelPainter) {
nodeLabelPainter.setTreePane(this);
if (this.nodeLabelPainter != null) {
this.nodeLabelPainter.removePainterListener(this);
}
this.nodeLabelPainter = nodeLabelPainter;
if (this.nodeLabelPainter != null) {
this.nodeLabelPainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public LabelPainter<Node> getNodeLabelPainter() {
return nodeLabelPainter;
}
public void setBranchLabelPainter(LabelPainter<Node> branchLabelPainter) {
branchLabelPainter.setTreePane(this);
if (this.branchLabelPainter != null) {
this.branchLabelPainter.removePainterListener(this);
}
this.branchLabelPainter = branchLabelPainter;
if (this.branchLabelPainter != null) {
this.branchLabelPainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public LabelPainter<Node> getBranchLabelPainter() {
return branchLabelPainter;
}
public void setNodeBarPainter(NodeBarPainter nodeBarPainter) {
nodeBarPainter.setTreePane(this);
if (this.nodeBarPainter != null) {
this.nodeBarPainter.removePainterListener(this);
}
this.nodeBarPainter = nodeBarPainter;
if (this.nodeBarPainter != null) {
this.nodeBarPainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public NodeBarPainter getNodeBarPainter() {
return nodeBarPainter;
}
public void setTipShapePainter(NodeShapePainter tipShapePainter) {
tipShapePainter.setTreePane(this);
if (this.tipShapePainter != null) {
this.tipShapePainter.removePainterListener(this);
}
this.tipShapePainter = tipShapePainter;
if (this.tipShapePainter != null) {
this.tipShapePainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public NodeShapePainter getTipShapePainter() {
return tipShapePainter;
}
public void setNodeShapePainter(NodeShapePainter nodeShapePainter) {
nodeShapePainter.setTreePane(this);
if (this.nodeShapePainter != null) {
this.nodeShapePainter.removePainterListener(this);
}
this.nodeShapePainter = nodeShapePainter;
if (this.nodeShapePainter != null) {
this.nodeShapePainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public NodeShapePainter getNodeShapePainter() {
return nodeShapePainter;
}
public void addScalePainter(ScalePainter scalePainter) {
assert scalePainter != null;
scalePainter.setTreePane(this);
scalePainter.addPainterListener(this);
scalePainters.add(scalePainter);
recalibrate();
repaint();
}
public void removeScalePainter(ScalePainter scalePainter) {
assert scalePainter != null;
scalePainter.removePainterListener(this);
scalePainters.remove(scalePainter);
recalibrate();
repaint();
}
public void setScaleGridPainter(ScaleGridPainter scaleGridPainter) {
scaleGridPainter.setTreePane(this);
if (this.scaleGridPainter != null) {
this.scaleGridPainter.removePainterListener(this);
}
this.scaleGridPainter = scaleGridPainter;
if (this.scaleGridPainter != null) {
this.scaleGridPainter.addPainterListener(this);
}
recalibrate();
repaint();
}
public void setLegendPainter(LegendPainter legendPainter) {
legendPainter.setTreePane(this);
legendPainter.addPainterListener(this);
this.legendPainter = legendPainter;
recalibrate();
repaint();
}
public LegendPainter getLegendPainter() {
return legendPainter;
}
public float getLabelSpacing() {
return labelXOffset;
}
public void setLabelSpacing(float labelSpacing) {
this.labelXOffset = labelSpacing;
recalibrate();
repaint();
}
public void setPreferredSize(Dimension dimension) {
if (treeLayout.maintainAspectRatio()) {
super.setPreferredSize(new Dimension(dimension.width, dimension.height));
} else {
super.setPreferredSize(dimension);
}
recalibrate();
}
public double getHeightAt(Graphics2D graphics2D, Point2D point) {
try {
point = transform.inverseTransform(point, null);
} catch (NoninvertibleTransformException e) {
e.printStackTrace();
}
return treeLayout.getHeightOfPoint(point);
}
public Node getNodeAt(Graphics2D g2, Point point) {
Rectangle rect = new Rectangle(point.x - 1, point.y - 1, 3, 3);
rect.translate(-insets.left, -insets.top);
for (Node node : tree.getExternalNodes()) {
Shape taxonLabelBound = tipLabelBounds.get(node);
if (taxonLabelBound != null && g2.hit(rect, taxonLabelBound, false)) {
return node;
}
}
if (transform == null) return null;
for (Node node : tree.getNodes()) {
Shape branchPath = transform.createTransformedShape(treeLayoutCache.getBranchPath(node));
if (branchPath != null && g2.hit(rect, branchPath, true)) {
return node;
}
Shape collapsedShape = transform.createTransformedShape(treeLayoutCache.getCollapsedShape(node));
if (collapsedShape != null && g2.hit(rect, collapsedShape, false)) {
return node;
}
}
return null;
}
public Set<Node> getNodesAt(Graphics2D g2, Rectangle rect) {
Set<Node> nodes = new HashSet<Node>();
for (Node node : tree.getExternalNodes()) {
Shape taxonLabelBound = tipLabelBounds.get(node);
if (taxonLabelBound != null && g2.hit(rect, taxonLabelBound, false)) {
nodes.add(node);
}
}
for (Node node : tree.getNodes()) {
Shape branchPath = transform.createTransformedShape(treeLayoutCache.getBranchPath(node));
if (branchPath != null && g2.hit(rect, branchPath, true)) {
nodes.add(node);
}
Shape collapsedShape = transform.createTransformedShape(treeLayoutCache.getCollapsedShape(node));
if (collapsedShape != null && g2.hit(rect, collapsedShape, false)) {
nodes.add(node);
}
}
return nodes;
}
public Set<Node> getSelectedNodes() {
return selectedNodes;
}
public Set<Node> getSelectedTips() {
return selectedTips;
}
public Set<Taxon> getSelectedTaxa() {
Set<Taxon> selectedTaxa = new LinkedHashSet<Taxon>();
for (Node node : getSelectedTips()) {
selectedTaxa.add(tree.getTaxon(node));
}
return selectedTaxa;
}
public RootedTree getSelectedSubtree() {
if (selectedNodes.size() == 0 && selectedTips.size() == 0) {
// nothing selected so return the whole tree
return tree;
}
SimpleRootedTree newTree = new SimpleRootedTree();
getSelectedSubtree(newTree, this.tree.getRootNode(), false);
if (newTree.getRootNode() == null) {
// no tree was constructed, most likely because only one tip was selected
return null;
}
return newTree;
}
/**
* Returns a new node structure within newTree that contains the subtree subtended by selected
* nodes of the current tree.
* @param newTree
* @param node
* @param isSelected
* @return
*/
private Node getSelectedSubtree(SimpleRootedTree newTree, Node node, boolean isSelected) {
Node newNode;
if (tree.isExternal(node)) {
if (isSelected || selectedNodes.contains(node) || selectedTips.contains(node)) {
newNode = newTree.createExternalNode(tree.getTaxon(node));
newTree.setHeight(newNode, tree.getHeight(node));
for (String key : node.getAttributeNames()) {
newNode.setAttribute(key, node.getAttribute(key));
}
} else {
newNode = null;
}
} else {
List<Node> children = new ArrayList<Node>();
for (Node child : tree.getChildren(node)) {
Node subtree = getSelectedSubtree(newTree, child, isSelected);
if (subtree != null) {
children.add(subtree);
}
}
if (children.size() == 0) {
if (selectedNodes.contains(node)) {
// if this node was selected but none of its children then include the entire
// descendent clade...
newNode = getSelectedSubtree(newTree, node, true);
} else {
newNode = null;
}
} else if (children.size() == 1) {
// just one child so pass it up...
newNode = children.get(0);
} else {
newNode = newTree.createInternalNode(children);
newTree.setHeight(newNode, tree.getHeight(node));
for (String key : node.getAttributeNames()) {
newNode.setAttribute(key, node.getAttribute(key));
}
}
}
return newNode;
}
public Rectangle2D getDragRectangle() {
return dragRectangle;
}
public void setDragRectangle(Rectangle2D dragRectangle) {
this.dragRectangle = dragRectangle;
repaint();
}
public void setRuler(double rulerHeight) {
this.rulerHeight = rulerHeight;
}
public Point getLocationOfTip(Node tip) {
if (tip == null) {
return new Point(0,0);
}
Shape path = transform.createTransformedShape(treeLayoutCache.getTipLabelPath(tip));
return path.getBounds().getLocation();
}
public void scrollPointToVisible(Point point) {
scrollRectToVisible(new Rectangle(point.x, point.y, 0, 0));
}
private final Set<TreeSelectionListener> treeSelectionListeners = new HashSet<TreeSelectionListener>();
public void addTreeSelectionListener(TreeSelectionListener treeSelectionListener) {
treeSelectionListeners.add(treeSelectionListener);
}
public void removeTreeSelectionListener(TreeSelectionListener treeSelectionListener) {
treeSelectionListeners.remove(treeSelectionListener);
}
private void fireSelectionChanged() {
for (TreeSelectionListener treeSelectionListener : treeSelectionListeners) {
treeSelectionListener.selectionChanged();
}
}
private final Set<TreePaneListener> treePaneListeners = new HashSet<TreePaneListener>();
public void addTreePaneListener(TreePaneListener treePaneListener) {
treePaneListeners.add(treePaneListener);
}
public void removeTreePaneListener(TreePaneListener treePaneListener) {
treePaneListeners.remove(treePaneListener);
}
private void fireSettingsChanged() {
for (TreePaneListener treePaneListener : treePaneListeners) {
treePaneListener.treePaneSettingsChanged();
}
}
public void paint(Graphics graphics) {
if (tree == null) return;
// graphics.setColor(Color.white);
// Rectangle r = graphics.getClipBounds();
// if (r != null) {
// graphics.fillRect(r.x, r.y, r.width, r.height);
// }
//
final Graphics2D g2 = (Graphics2D) graphics;
g2.translate(insets.left, insets.top);
if (!calibrated) {
calibrate(g2, getWidth(), getHeight());
}
drawTree(g2, getWidth(), getHeight());
Paint oldPaint = g2.getPaint();
Stroke oldStroke = g2.getStroke();
// if (isCrosshairShown && cursorPosition != null && dragRectangle == null) {
// g2.setPaint(cursorPaint);
// g2.setStroke(cursorStroke);
// double x = Math.max(treeBounds.getX(),
// Math.min(cursorPosition.getX(), treeBounds.getX() + treeBounds.getWidth()));
// double y = Math.max(treeBounds.getY(),
// Math.min(cursorPosition.getY(), treeBounds.getY() + treeBounds.getHeight()));
//
// g2.draw(new Line2D.Double(0.0, y, getWidth(), y));
// g2.draw(new Line2D.Double(x, 0.0, x, getHeight()));
//
// }
if (branchSelection == null) {
branchSelection = new GeneralPath();
for (Node selectedNode : selectedNodes) {
Shape branchPath = treeLayoutCache.getBranchPath(selectedNode);
if (branchPath != null) {
Shape transPath = transform.createTransformedShape(branchPath);
branchSelection.append(transPath, false);
}
Shape collapsedShape = treeLayoutCache.getCollapsedShape(selectedNode);
if (collapsedShape != null) {
Shape transPath = transform.createTransformedShape(collapsedShape);
branchSelection.append(transPath, false);
}
}
}
if (labelSelection == null) {
labelSelection = new GeneralPath();
for (Node selectedTip : selectedTips) {
Shape labelBounds = tipLabelBounds.get(selectedTip);
if (labelBounds != null) {
labelSelection.append(labelBounds, false);
}
}
}
g2.setPaint(selectionPaint);
g2.setStroke(selectionStroke);
g2.draw(branchSelection);
g2.fill(labelSelection);
g2.setPaint(oldPaint);
g2.setStroke(oldStroke);
if (dragRectangle != null) {
g2.setPaint(new Color(128, 128, 128, 128));
g2.fill(dragRectangle);
}
}
private void clearSelectionPaths() {
branchSelection = null;
labelSelection = null;
}
private GeneralPath branchSelection = null;
private GeneralPath labelSelection = null;
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
if (tree == null || pageIndex > 0) return NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
recalibrate();
setDoubleBuffered(false);
drawTree(g2, pageFormat.getImageableWidth(), pageFormat.getImageableHeight());
setDoubleBuffered(true);
recalibrate();
return PAGE_EXISTS;
}
public void drawTree(Graphics2D g2, double width, double height) {
final RenderingHints rhints = g2.getRenderingHints();
final boolean antialiasOn = rhints.containsValue(RenderingHints.VALUE_ANTIALIAS_ON);
if( ! antialiasOn ) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
if (!calibrated) {
calibrate(g2, width, height);
}
// save graphics state which draw changes so that upon exit it can be restored
final AffineTransform oldTransform = g2.getTransform();
final Paint oldPaint = g2.getPaint();
final Stroke oldStroke = g2.getStroke();
final Font oldFont = g2.getFont();
if (legendPainter != null && legendPainter.isVisible()) {
legendPainter.paint(g2, this, Painter.Justification.CENTER, legendBounds);
}
// Paint scales
for (ScalePainter scalePainter : scalePainters) {
if (scalePainter.isVisible()) {
Rectangle2D scaleBounds = this.scaleBounds.get(scalePainter);
scalePainter.paint(g2, this, Painter.Justification.CENTER, scaleBounds);
}
}
if (scaleGridPainter != null && scaleGridPainter.isVisible()) {
Rectangle2D gridBounds = new Rectangle2D.Double(
treeBounds.getX(), 0.0,
treeBounds.getWidth(), treeBounds.getHeight());
scaleGridPainter.paint(g2, this, null, gridBounds);
}
// Paint backgrounds
if (nodeBackgroundDecorator != null) {
for (Node node : treeLayoutCache.getNodeAreaMap().keySet() ) {
Shape nodeArea = treeLayoutCache.getNodeArea(node);
if (nodeArea != null) {
nodeBackgroundDecorator.setItem(node);
Shape transNodePath = transform.createTransformedShape(nodeArea);
Paint background = new Color(0,0,0,0);
background = nodeBackgroundDecorator.getPaint(background);
g2.setPaint(background);
g2.fill(transNodePath);
// Experimental outlining - requires order of drawing to be pre-order
// g2.setStroke(new BasicStroke(8, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
// g2.draw(transNodePath);
}
}
}
// Paint hilighted nodes
for (Node node : treeLayoutCache.getHilightNodesList() ) {
Object[] values = (Object[])node.getAttribute(HILIGHT_ATTRIBUTE_NAME);
Shape hilightShape = treeLayoutCache.getHilightShape(node);
Shape transShape = transform.createTransformedShape(hilightShape);
Paint paint = ((Color)values[2]).darker();
Paint fillPaint = (Color)values[2];
Stroke stroke = new BasicStroke(0.5F);
if (hilightingGradient && fillPaint != null) {
fillPaint = new GradientPaint(
(float)transShape.getBounds2D().getMinX(), 0.0F, Color.WHITE,
(float)transShape.getBounds2D().getMaxX(), 0.0F, (Color)values[2], false);
g2.setPaint(fillPaint);
g2.fill(transShape);
} else {
if (fillPaint != null) {
g2.setPaint(fillPaint);
g2.fill(transShape);
}
if (paint != null) {
g2.setPaint(paint);
g2.setStroke(stroke);
g2.draw(transShape);
}
}
}
if (DEBUG_OUTLINE) {
g2.setPaint(Color.blue);
g2.draw(treeBounds);
}
// Paint collapsed nodes
for (Node node : treeLayoutCache.getCollapsedShapeMap().keySet() ) {
Shape collapsedShape = treeLayoutCache.getCollapsedShape(node);
Shape transShape = transform.createTransformedShape(collapsedShape);
Paint paint = Color.BLACK;
Paint fillPaint = null;
Stroke stroke = branchLineStroke;
if (branchDecorator != null) {
branchDecorator.setItem(node);
paint = branchDecorator.getPaint(paint);
fillPaint = branchDecorator.getFillPaint(fillPaint);
stroke = branchDecorator.getStroke(stroke);
}
if (fillPaint != null) {
g2.setPaint(fillPaint);
g2.fill(transShape);
}
g2.setPaint(paint);
g2.setStroke(stroke);
g2.draw(transShape);
}
// Paint branches
for (Node node : treeLayoutCache.getBranchPathMap().keySet() ) {
Stroke stroke = branchLineStroke;
if (branchDecorator != null) {
branchDecorator.setItem(node);
stroke = branchDecorator.getStroke(stroke);
}
g2.setStroke(stroke);
Object[] branchColouring = null;
if (treeLayout.isShowingColouring() && branchColouringAttribute != null) {
branchColouring = (Object[])node.getAttribute(branchColouringAttribute);
}
Shape branchPath = treeLayoutCache.getBranchPath(node);
if (branchColouring != null) {
PathIterator iter = branchPath.getPathIterator(transform);
float[] coords1 = new float[2];
iter.currentSegment(coords1);
for (int i = 0; i < branchColouring.length - 1; i+=2) {
iter.next();
float[] coords2 = new float[2];
iter.currentSegment(coords2);
int colour = ((Number)branchColouring[i]).intValue();
branchColouringDecorator.setItem(colour);
g2.setPaint(branchColouringDecorator.getPaint(Color.BLACK));
g2.draw(new Line2D.Float(coords1[0], coords1[1], coords2[0], coords2[1]));
coords1 = coords2;
}
// Draw the remaining branch as a path so it has proper line joins...
int colour = ((Number)branchColouring[branchColouring.length - 1]).intValue();
branchColouringDecorator.setItem(colour);
g2.setPaint(branchColouringDecorator.getPaint(Color.BLACK));
// Append the rest of the PathIterator to this new path...
GeneralPath path = new GeneralPath();
path.moveTo(coords1[0], coords1[1]);
path.append(iter, true);
// and draw it...
g2.draw(path);
} else {
Shape transPath = transform.createTransformedShape(branchPath);
Paint paint = Color.BLACK;
if (branchDecorator != null) {
if (branchDecoratorGradient && branchDecorator.allowsGradient()) {
branchDecorator.setItems(node, tree.getParent(node));
PathIterator iter = transPath.getPathIterator(null);
double[] coords = new double[6];
iter.currentSegment(coords);
Point2D point1 = new Point2D.Double(coords[0], coords[1]);
do {
iter.currentSegment(coords);
iter.next();
} while (!iter.isDone());
Point2D point2 = new Point2D.Double(coords[0], coords[1]);
paint = branchDecorator.getPaint(paint, point1, point2);
} else {
branchDecorator.setItem(node);
paint = branchDecorator.getPaint(paint);
}
}
g2.setPaint(paint);
g2.draw(transPath);
}
}
// Paint node bars
if (!isTransformBranchesOn() && nodeBarPainter != null && nodeBarPainter.isVisible()) {
for (Node node : nodeBars.keySet() ) {
Shape nodeBar = nodeBars.get(node);
nodeBar = transform.createTransformedShape(nodeBar);
nodeBarPainter.paint(g2, node, NodePainter.Justification.CENTER, nodeBar);
}
}
// Paint node shapes
if (nodeShapePainter != null && nodeShapePainter.isVisible()) {
for (Node node : nodePoints.keySet()) {
Point2D point = nodePoints.get(node);
point = transform.transform(point, null);
nodeShapePainter.paint(g2, node, point, nodeShapeTransforms.get(node));
}
}
if (tipShapePainter != null && tipShapePainter.isVisible()) {
for (Node node : tipPoints.keySet()) {
Point2D point = tipPoints.get(node);
point = transform.transform(point, null);
tipShapePainter.paint(g2, node, point, nodeShapeTransforms.get(node));
}
}
// Paint tip labels
if (tipLabelPainter != null && tipLabelPainter.isVisible()) {
for (Node node : tipLabelTransforms.keySet()) {
AffineTransform tipLabelTransform = tipLabelTransforms.get(node);
Painter.Justification tipLabelJustification = tipLabelJustifications.get(node);
g2.transform(tipLabelTransform);
double labelWidth = tipLabelWidths.get(node);
tipLabelPainter.paint(g2, node, tipLabelJustification,
new Rectangle2D.Double(0.0, 0.0, labelWidth, tipLabelPainter.getPreferredHeight()));
g2.setTransform(oldTransform);
if (showingTipCallouts) {
Shape calloutPath = transform.createTransformedShape(treeLayoutCache.getCalloutPath(node));
if (calloutPath != null) {
g2.setStroke(calloutStroke);
g2.draw(calloutPath);
}
}
}
}
// Paint node labels
if (nodeLabelPainter != null && nodeLabelPainter.isVisible()) {
for (Node node : nodeLabelTransforms.keySet() ) {
AffineTransform nodeTransform = nodeLabelTransforms.get(node);
Painter.Justification nodeLabelJustification = nodeLabelJustifications.get(node);
g2.transform(nodeTransform);
nodeLabelPainter.paint(g2, node, nodeLabelJustification,
new Rectangle2D.Double(0.0, 0.0, nodeLabelPainter.getPreferredWidth(), nodeLabelPainter.getPreferredHeight()));
g2.setTransform(oldTransform);
}
}
// Paint branch labels
if (branchLabelPainter != null && branchLabelPainter.isVisible()) {
for (Node node : branchLabelTransforms.keySet() ) {
AffineTransform branchTransform = branchLabelTransforms.get(node);
g2.transform(branchTransform);
branchLabelPainter.calibrate(g2, node);
final double preferredWidth = branchLabelPainter.getPreferredWidth();
final double preferredHeight = branchLabelPainter.getPreferredHeight();
branchLabelPainter.paint(g2, node, Painter.Justification.CENTER,
new Rectangle2D.Double(0, 0, preferredWidth, preferredHeight));
g2.setTransform(oldTransform);
}
}
g2.setStroke(oldStroke);
g2.setPaint(oldPaint);
g2.setFont(oldFont);
}
private void calibrate(Graphics2D g2, double width, double height) {
// First layout the tree
treeLayout.layout(tree, treeLayoutCache);
maxTreeHeight = tree.getHeight(tree.getRootNode()) + treeLayout.getRootLength();
rootHeightOffset = 0.0;
// First of all get the bounds for the unscaled tree
treeBounds = null;
// There are two sets of bounds here. The treeBounds are the bounds of the elements
// that make up the actual tree. These are scaled from branch length space
// The bounds are then the extra stuff that doesn't get scaled with the tree such
// as labels and the like.
// bounds on branches
for (Shape branchPath : treeLayoutCache.getBranchPathMap().values()) {
// Add the bounds of the branch path to the overall bounds
final Rectangle2D branchBounds = branchPath.getBounds2D();
if (treeBounds == null) {
treeBounds = branchBounds;
} else {
treeBounds.add(branchBounds);
}
}
// Iterate though the callout paths
for (Shape calloutPath : treeLayoutCache.getCalloutPathMap().values()) {
// Get the line that represents the path for the taxon label
// and add the translated bounds to the overall bounds
final Rectangle2D calloutBounds = calloutPath.getBounds2D();
treeBounds.add(calloutBounds);
}
for (Shape collapsedShape : treeLayoutCache.getCollapsedShapeMap().values()) {
// Add the bounds of the branch path to the overall bounds
final Rectangle2D branchBounds = collapsedShape.getBounds2D();
if (treeBounds == null) {
treeBounds = branchBounds;
} else {
treeBounds.add(branchBounds);
}
}
for (Shape hilightShape : treeLayoutCache.getHilightShapeMap().values()) {
// Add the bounds of the branch path to the overall bounds
final Rectangle2D branchBounds = hilightShape.getBounds2D();
if (treeBounds == null) {
treeBounds = branchBounds;
} else {
treeBounds.add(branchBounds);
}
}
// bounds on node bars
if (!isTransformBranchesOn() && nodeBarPainter != null && nodeBarPainter.isVisible()) {
nodeBars.clear();
// Iterate though the nodes
for (Node node : tree.getInternalNodes()) {
Rectangle2D shapeBounds = nodeBarPainter.calibrate(g2, node);
if (shapeBounds != null) {
treeBounds.add(shapeBounds);
nodeBars.put(node, nodeBarPainter.getNodeBar());
}
}
if (nodeBarPainter.getMaxHeight() > maxTreeHeight) {
rootHeightOffset = Math.max(nodeBarPainter.getMaxHeight() - maxTreeHeight, 0.0);
maxTreeHeight = nodeBarPainter.getMaxHeight();
}
}
// totalTreeBounds includes all the stuff which is not in a tree scale (like labels and shapes) but in
// screen pixel scale. This is added to the treeBounds to make space round the edge.
// add the tree bounds
final Rectangle2D totalTreeBounds = treeBounds.getBounds2D();
// final Rectangle2D totalTreeBounds = new Rectangle2D.Double(0.0, 0.0,treeBounds.getWidth(),treeBounds.getHeight());
tipLabelWidths.clear();
if (tipLabelPainter != null && tipLabelPainter.isVisible()) {
// calculateMaxTipLabelWidth(g2, tree.getRootNode());
// put this in a recursive function to allow for collapsed node labels
calibrateTipLabels(g2, tree.getRootNode(), totalTreeBounds);
}
if (nodeLabelPainter != null && nodeLabelPainter.isVisible()) {
// Iterate though the nodes with node labels
for (Node node : treeLayoutCache.getNodeLabelPathMap().keySet()) {
// Get the line that represents the path for the taxon label
final Line2D labelPath = treeLayoutCache.getNodeLabelPath(node);
nodeLabelPainter.calibrate(g2, node);
final double labelHeight = nodeLabelPainter.getPreferredHeight();
final double labelWidth = nodeLabelPainter.getPreferredWidth();
Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
// Work out how it is rotated and create a transform that matches that
AffineTransform labelTransform = calculateTransform(null, labelPath, labelWidth, labelHeight, true);
// and add the translated bounds to the overall bounds
totalTreeBounds.add(labelTransform.createTransformedShape(labelBounds).getBounds2D());
}
}
if (branchLabelPainter != null && branchLabelPainter.isVisible()) {
// Iterate though the nodes with branch labels
for (Node node : treeLayoutCache.getBranchLabelPathMap().keySet()) {
// Get the line that represents the path for the branch label
final Line2D labelPath = treeLayoutCache.getBranchLabelPath(node);
branchLabelPainter.calibrate(g2, node);
final double labelHeight = branchLabelPainter.getHeightBound();
final double labelWidth = branchLabelPainter.getPreferredWidth();
Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
// Work out how it is rotated and create a transform that matches that
AffineTransform labelTransform = calculateTransform(null, labelPath, labelWidth, labelHeight, false);
// and add the translated bounds to the overall bounds
totalTreeBounds.add(labelTransform.createTransformedShape(labelBounds).getBounds2D());
}
}
// bounds on nodeShapes
if (tipShapePainter != null && tipShapePainter.isVisible()) {
tipPoints.clear();
// Iterate though the external nodes
for (Node node : tree.getExternalNodes()) {
Rectangle2D shapeBounds = tipShapePainter.calibrate(g2, node);
if (shapeBounds != null) {
totalTreeBounds.add(shapeBounds);
// just at the centroid in here as the actual shape will be reconstructed when drawing
tipPoints.put(node, new Point2D.Double(shapeBounds.getCenterX(), shapeBounds.getCenterY()));
}
}
}
if (nodeShapePainter != null && nodeShapePainter.isVisible()) {
nodePoints.clear();
// Iterate though the internal nodes
for (Node node : tree.getInternalNodes()) {
Rectangle2D shapeBounds = nodeShapePainter.calibrate(g2, node);
if (shapeBounds != null) {
totalTreeBounds.add(shapeBounds);
// just at the centroid in here as the actual shape will be reconstructed when drawing
nodePoints.put(node, new Point2D.Double(shapeBounds.getCenterX(), shapeBounds.getCenterY()));
}
}
}
// Now rescale the scale axis
setupScaleAxis();
bottomPanelBounds = new Rectangle2D.Double();
double y = totalTreeBounds.getHeight();
for (ScalePainter scalePainter : scalePainters) {
if (scalePainter.isVisible()) {
scalePainter.calibrate(g2, this);
Rectangle2D sb = new Rectangle2D.Double(
treeBounds.getX(), y,
treeBounds.getWidth(), scalePainter.getPreferredHeight());
y += sb.getHeight();
bottomPanelBounds.add(sb);
scaleBounds.put(scalePainter, sb);
}
}
leftPanelBounds = new Rectangle2D.Double();
if (legendPainter != null && legendPainter.isVisible()) {
legendPainter.calibrate(g2, this);
final double w2 = legendPainter.getPreferredWidth();
legendBounds = new Rectangle2D.Double(0.0, 0.0, w2, height);
leftPanelBounds.add(legendBounds);
}
final double availableW = width - insets.left - insets.right;
final double availableH = height - insets.top - insets.bottom;
// get the difference between the tree's bounds and the overall bounds
boolean maintainAspectRatio = treeLayout.maintainAspectRatio();
double xDiff;
double yDiff;
if (maintainAspectRatio) {
double topDiff = treeBounds.getY() - totalTreeBounds.getY();
double leftDiff = treeBounds.getX() - totalTreeBounds.getX();
double bottomDiff = (totalTreeBounds.getHeight() + totalTreeBounds.getY()) -
(treeBounds.getHeight() + treeBounds.getY());
double rightDiff = (totalTreeBounds.getWidth() + totalTreeBounds.getX()) -
(treeBounds.getWidth() + treeBounds.getX());
assert topDiff >= 0 && leftDiff >= 0 && bottomDiff >= 0 && rightDiff >= 0;
xDiff = 2.0 * (leftDiff > rightDiff ? leftDiff : rightDiff);
yDiff = 2.0 * (topDiff > bottomDiff ? topDiff : bottomDiff);
} else {
xDiff = totalTreeBounds.getWidth() - treeBounds.getWidth();
yDiff = totalTreeBounds.getHeight() - treeBounds.getHeight();
assert xDiff >= 0 && yDiff >= 0;
}
// small tree, long labels, label bounds may get larger that window, protect against that
if( xDiff >= availableW ) {
xDiff = Math.min(availableW, totalTreeBounds.getWidth()) - treeBounds.getWidth();
}
if( yDiff >= availableH ) {
yDiff = Math.min(availableH, totalTreeBounds.getHeight()) - treeBounds.getHeight();
}
// Get the amount of canvas that is going to be taken up by the tree -
// The rest is taken up by taxon labels which don't scale
final double w = availableW - xDiff - leftPanelBounds.getWidth() - rightPanelBounds.getWidth();
final double h = availableH - yDiff - topPanelBounds.getHeight() - bottomPanelBounds.getHeight();
double xScale;
double yScale;
double xOffset = 0.0;
double yOffset = 0.0;
if (maintainAspectRatio) {
// If the tree is laid out in both dimensions then we
// need to find out which axis has the least space and scale
// the tree to that (to keep the aspect ratio).
if ((w / treeBounds.getWidth()) < (h / treeBounds.getHeight())) {
xScale = w / treeBounds.getWidth();
yScale = xScale;
} else {
yScale = h / treeBounds.getHeight();
xScale = yScale;
}
treeScale = xScale; assert treeScale > 0;
// and set the origin so that the center of the tree is in
// the center of the canvas
xOffset = ((width - (treeBounds.getWidth() * xScale)) / 2) - (treeBounds.getX() * xScale);
yOffset = ((height - (treeBounds.getHeight() * yScale)) / 2) - (treeBounds.getY() * yScale);
} else {
// Otherwise just scale both dimensions
xScale = w / treeBounds.getWidth();
yScale = h / treeBounds.getHeight();
// and set the origin in the top left corner
xOffset = -treeBounds.getX() * xScale + (treeBounds.getX() - totalTreeBounds.getX());
yOffset = -treeBounds.getY() * yScale + (treeBounds.getY() - totalTreeBounds.getY());
treeScale = xScale;
}
assert treeScale > 0;
// Create the overall transform
transform = new AffineTransform();
transform.translate(xOffset + leftPanelBounds.getWidth(), yOffset + topPanelBounds.getHeight());
transform.scale(xScale, yScale);
// Get the bounds for the newly scaled tree
treeBounds = null;
// bounds on branches
for (Shape branchPath : treeLayoutCache.getBranchPathMap().values()) {
// Add the bounds of the branch path to the overall bounds
final Rectangle2D branchBounds = transform.createTransformedShape(branchPath).getBounds2D();
if (treeBounds == null) {
treeBounds = branchBounds;
} else {
treeBounds.add(branchBounds);
}
}
for (Shape collapsedShape : treeLayoutCache.getCollapsedShapeMap().values()) {
// Add the bounds of the branch path to the overall bounds
final Rectangle2D branchBounds = transform.createTransformedShape(collapsedShape).getBounds2D();
if (treeBounds == null) {
treeBounds = branchBounds;
} else {
treeBounds.add(branchBounds);
}
}
// bounds on node bars
if (!isTransformBranchesOn() && nodeBarPainter != null && nodeBarPainter.isVisible()) {
nodeBars.clear();
// Iterate though the nodes
for (Node node : tree.getInternalNodes()) {
Rectangle2D shapeBounds = nodeBarPainter.calibrate(g2, node);
if (shapeBounds != null) {
shapeBounds = transform.createTransformedShape(shapeBounds).getBounds2D();
treeBounds.add(shapeBounds);
nodeBars.put(node, nodeBarPainter.getNodeBar());
}
}
}
// Clear the map of individual taxon label bounds and transforms
tipLabelBounds.clear();
tipLabelTransforms.clear();
tipLabelJustifications.clear();
if (tipLabelPainter != null && tipLabelPainter.isVisible()) {
final double labelHeight = tipLabelPainter.getPreferredHeight();
// Iterate though the external nodes with tip labels
for (Node node : treeLayoutCache.getTipLabelPathMap().keySet()) {
// Get the line that represents the path for the tip label
Line2D tipPath = treeLayoutCache.getTipLabelPath(node);
final double labelWidth = tipLabelWidths.get(node);
Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
// Work out how it is rotated and create a transform that matches that
AffineTransform taxonTransform = calculateTransform(transform, tipPath, labelWidth, labelHeight, true);
// Store the transformed bounds in the map for use when selecting
tipLabelBounds.put(node, taxonTransform.createTransformedShape(labelBounds));
// Store the transform in the map for use when drawing
tipLabelTransforms.put(node, taxonTransform);
// Store the alignment in the map for use when drawing
final Painter.Justification just = (tipPath.getX1() < tipPath.getX2()) ?
Painter.Justification.LEFT : Painter.Justification.RIGHT;
tipLabelJustifications.put(node, just);
}
}
// Clear the map of individual node label bounds and transforms
nodeLabelBounds.clear();
nodeLabelTransforms.clear();
nodeLabelJustifications.clear();
if (nodeLabelPainter != null && nodeLabelPainter.isVisible()) {
final double labelHeight = nodeLabelPainter.getPreferredHeight();
final double labelWidth = nodeLabelPainter.getPreferredWidth();
final Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
// Iterate though the external nodes with node labels
for (Node node : treeLayoutCache.getNodeLabelPathMap().keySet()) {
// Get the line that represents the path for the node label
final Line2D labelPath = treeLayoutCache.getNodeLabelPath(node);
// Work out how it is rotated and create a transform that matches that
AffineTransform labelTransform = calculateTransform(transform, labelPath, labelWidth, labelHeight, true);
// Store the transformed bounds in the map for use when selecting
nodeLabelBounds.put(node, labelTransform.createTransformedShape(labelBounds));
// Store the transform in the map for use when drawing
nodeLabelTransforms.put(node, labelTransform);
// Store the alignment in the map for use when drawing
if (labelPath.getX1() < labelPath.getX2()) {
nodeLabelJustifications.put(node, Painter.Justification.LEFT);
} else {
nodeLabelJustifications.put(node, Painter.Justification.RIGHT);
}
}
}
branchLabelBounds.clear();
branchLabelTransforms.clear();
branchLabelJustifications.clear();
if (branchLabelPainter != null && branchLabelPainter.isVisible()) {
// Iterate though the external nodes with branch labels
for (Node node : treeLayoutCache.getBranchLabelPathMap().keySet()) {
// Get the line that represents the path for the branch label
Line2D labelPath = treeLayoutCache.getBranchLabelPath(node);
final double labelHeight = branchLabelPainter.getPreferredHeight();
final double labelWidth = branchLabelPainter.getPreferredWidth();
final Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
final double dx = labelPath.getP2().getX() - labelPath.getP1().getX();
final double dy = labelPath.getP2().getY() - labelPath.getP1().getY();
final double branchLength = Math.sqrt(dx*dx + dy*dy);
final Painter.Justification just = labelPath.getX1() < labelPath.getX2() ? Painter.Justification.LEFT :
Painter.Justification.RIGHT;
// Work out how it is rotated and create a transform that matches that
AffineTransform labelTransform = calculateTransform(transform, labelPath, labelWidth, labelHeight, false);
// move to middle of branch - since the move is before the rotation
final double direction = just == Painter.Justification.RIGHT ? 1 : -1;
labelTransform.translate(-direction * xScale * branchLength /2, 0);
// Store the transformed bounds in the map for use when selecting
branchLabelBounds.put(node, labelTransform.createTransformedShape(labelBounds));
// Store the transform in the map for use when drawing
branchLabelTransforms.put(node, labelTransform);
// Store the alignment in the map for use when drawing
branchLabelJustifications.put(node, just);
}
}
nodeShapeTransforms.clear();
if (nodeShapePainter != null && nodeShapePainter.isVisible()) {
// Iterate though the nodes
for (Node node : nodePoints.keySet()) {
Line2D shapePath = getTreeLayoutCache().getNodeShapePath(node);
if (shapePath != null) {
nodeShapeTransforms.put(node, calculateTransform(transform, shapePath));
}
}
}
if (tipShapePainter != null && tipShapePainter.isVisible()) {
// Iterate though the nodes
for (Node node : tipPoints.keySet()) {
Line2D shapePath = getTreeLayoutCache().getNodeShapePath(node);
if (shapePath != null) {
nodeShapeTransforms.put(node, calculateTransform(transform, shapePath));
}
}
}
y = availableH;
for (ScalePainter scalePainter : scalePainters) {
if (scalePainter.isVisible()) {
scalePainter.calibrate(g2, this);
y -= scalePainter.getPreferredHeight();
}
}
bottomPanelBounds = new Rectangle2D.Double(0, y, treeBounds.getWidth(), 0.0);
for (ScalePainter scalePainter : scalePainters) {
if (scalePainter.isVisible()) {
scalePainter.calibrate(g2, this);
final double h1 = scalePainter.getPreferredHeight();
Rectangle2D sb = new Rectangle2D.Double(treeBounds.getX(), y, treeBounds.getWidth(), h1);
y += h1;
bottomPanelBounds.add(sb);
scaleBounds.put(scalePainter, sb);
}
}
leftPanelBounds = new Rectangle2D.Double(0, 0, 0.0, 0.0);
if (legendPainter != null && legendPainter.isVisible()) {
legendPainter.calibrate(g2, this);
final double w2 = legendPainter.getPreferredWidth();
legendBounds = new Rectangle2D.Double(0.0, 0.0, w2, availableH);
leftPanelBounds.add(legendBounds);
}
calloutPaths.clear();
clearSelectionPaths();
calibrated = true;
}
// private void calculateMaxTipLabelWidth(final Graphics2D g2, final Node node) {
//
// if (tree.isExternal(node) || node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
// tipLabelPainter.calibrate(g2, node);
// double labelWidth = tipLabelPainter.getPreferredWidth();
// tipLabelWidths.put(node, labelWidth);
// maxTipLabelWidth = Math.max(maxTipLabelWidth, labelWidth);
// } else {
// for (Node child : tree.getChildren(node)) {
// calculateMaxTipLabelWidth(g2, child);
// }
// }
// }
private void calibrateTipLabels(final Graphics2D g2, final Node node, final Rectangle2D totalTreeBounds) {
if (tree.isExternal(node) || node.getAttribute(COLLAPSE_ATTRIBUTE_NAME) != null) {
tipLabelPainter.calibrate(g2, node);
double labelWidth = tipLabelPainter.getPreferredWidth();
double labelHeight = tipLabelPainter.getPreferredHeight();
tipLabelWidths.put(node, labelWidth);
Rectangle2D labelBounds = new Rectangle2D.Double(0.0, 0.0, labelWidth, labelHeight);
// Get the line that represents the path for the taxon label
Line2D taxonPath = treeLayoutCache.getTipLabelPath(node);
if (taxonPath != null) {
// Work out how it is rotated and create a transform that matches that
AffineTransform taxonTransform = calculateTransform(null, taxonPath, labelWidth, labelHeight, true);
// and add the translated bounds to the overall bounds
totalTreeBounds.add(taxonTransform.createTransformedShape(labelBounds).getBounds2D());
}
} else {
for (Node child : tree.getChildren(node)) {
calibrateTipLabels(g2, child, totalTreeBounds);
}
}
}
private AffineTransform calculateTransform(AffineTransform globalTransform, Line2D line,
double width, double height, boolean justify) {
final Point2D origin = line.getP1();
if (globalTransform != null) {
globalTransform.transform(origin, origin);
}
// Work out how it is rotated and create a transform that matches that
AffineTransform lineTransform = new AffineTransform();
final double dy = line.getY2() - line.getY1();
// efficency
if( dy != 0.0 ) {
final double dx = line.getX2() - line.getX1();
final double angle = dx != 0.0 ? Math.atan(dy / dx) : 0.0;
lineTransform.rotate(angle, origin.getX(), origin.getY());
}
// Now add a translate to the transform - if it is on the left then we need
// to shift it by the entire width of the string.
final double ty = origin.getY() - (height / 2.0);
double tx = origin.getX();
if (justify) {
if (line.getX2() > line.getX1()) {
tx += labelXOffset;
} else {
tx -= (labelXOffset + width);
}
}
lineTransform.translate(tx, ty);
return lineTransform;
}
private AffineTransform calculateTransform(AffineTransform globalTransform, Line2D line) {
final Point2D origin = line.getP1();
if (globalTransform != null) {
globalTransform.transform(origin, origin);
}
// Work out how it is rotated and create a transform that matches that
AffineTransform lineTransform = new AffineTransform();
final double dy = line.getY2() - line.getY1();
// efficency
if( dy != 0.0 ) {
final double dx = line.getX2() - line.getX1();
final double angle = dx != 0.0 ? Math.atan(dy / dx) : 0.0;
lineTransform.rotate(angle, origin.getX(), origin.getY());
}
return lineTransform;
}
// Overridden methods to recalibrate tree when bounds change
public void setBounds(int x, int y, int width, int height) {
recalibrate();
super.setBounds(x, y, width, height);
}
public void setBounds(Rectangle rectangle) {
recalibrate();
super.setBounds(rectangle);
}
public void setSize(Dimension dimension) {
recalibrate();
super.setSize(dimension);
}
public void setSize(int width, int height) {
recalibrate();
super.setSize(width, height);
}
private RootedTree originalTree = null;
private RootedTree tree = null;
private TreeLayout treeLayout = null;
private TreeLayoutCache treeLayoutCache = new TreeLayoutCache();
private boolean orderBranchesOn = false;
private SortedRootedTree.BranchOrdering branchOrdering = SortedRootedTree.BranchOrdering.INCREASING_NODE_DENSITY;
private boolean transformBranchesOn = false;
private TransformedRootedTree.Transform branchTransform = TransformedRootedTree.Transform.CLADOGRAM;
private boolean isRootingOn = false;
private RootingType rootingType = RootingType.USER_ROOTING;
private Node rootingNode = null;
private double rootingLength = 0.01;
private Rectangle2D treeBounds = new Rectangle2D.Double();
private double treeScale;
private double maxTreeHeight;
private double rootHeightOffset;
private ScaleAxis scaleAxis = new ScaleAxis(ScaleAxis.AT_DATA, ScaleAxis.AT_DATA);
private double axisOrigin = 0.0;
private TimeScale timeScale = new TimeScale(1.0, 0.0);
private boolean isAxisReversed = false;
//private Insets insets = new Insets(0, 0, 0, 0);
private Insets insets = new Insets(6, 6, 6, 6);
private Set<Node> selectedNodes = new HashSet<Node>();
private Set<Node> selectedTips = new LinkedHashSet<Node>();
private double rulerHeight = -1.0;
private Rectangle2D dragRectangle = null;
private Point2D cursorPosition = null;
private boolean isCrosshairShown = true;
private Decorator branchDecorator = null;
private Decorator branchColouringDecorator = null;
private boolean branchDecoratorGradient = false;
private String branchColouringAttribute = null;
private boolean hilightingGradient = false;
private Decorator nodeBackgroundDecorator = null;
private float labelXOffset = 10.0F;
private LabelPainter<Node> tipLabelPainter = null;
//private double maxTipLabelWidth;
private LabelPainter<Node> nodeLabelPainter = null;
private LabelPainter<Node> branchLabelPainter = null;
private NodeBarPainter nodeBarPainter = null;
private NodeShapePainter nodeShapePainter = null;
private NodeShapePainter tipShapePainter = null;
private List<ScalePainter> scalePainters = new ArrayList<ScalePainter>();
private Map<ScalePainter, Rectangle2D> scaleBounds = new HashMap<ScalePainter, Rectangle2D>();
private ScaleGridPainter scaleGridPainter = null;
private LegendPainter legendPainter = null;
private Rectangle2D legendBounds = new Rectangle2D.Double();
private Rectangle2D topPanelBounds = new Rectangle2D.Double();
private Rectangle2D leftPanelBounds = new Rectangle2D.Double();
private Rectangle2D bottomPanelBounds = new Rectangle2D.Double();
private Rectangle2D rightPanelBounds = new Rectangle2D.Double();
private BasicStroke branchLineStroke = new BasicStroke(1.0F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
private BasicStroke calloutStroke = new BasicStroke(0.5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, new float[]{0.5f, 2.0f}, 0.0f);
private Stroke selectionStroke = new BasicStroke(6.0F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
private Paint selectionPaint;
private Stroke cursorStroke = new BasicStroke(0.5F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
private Paint cursorPaint = Color.DARK_GRAY;
private boolean calibrated = false;
private AffineTransform transform = null;
private boolean showingTipCallouts = true;
private Map<Node, AffineTransform> tipLabelTransforms = new HashMap<Node, AffineTransform>();
private Map<Node, Shape> tipLabelBounds = new HashMap<Node, Shape>();
private Map<Node, Double> tipLabelWidths = new HashMap<Node, Double>();
private Map<Node, Painter.Justification> tipLabelJustifications = new HashMap<Node, Painter.Justification>();
private Map<Node, AffineTransform> nodeLabelTransforms = new HashMap<Node, AffineTransform>();
private Map<Node, Shape> nodeLabelBounds = new HashMap<Node, Shape>();
private Map<Node, Painter.Justification> nodeLabelJustifications = new HashMap<Node, Painter.Justification>();
private Map<Node, AffineTransform> branchLabelTransforms = new HashMap<Node, AffineTransform>();
private Map<Node, Shape> branchLabelBounds = new HashMap<Node, Shape>();
private Map<Node, Painter.Justification> branchLabelJustifications = new HashMap<Node, Painter.Justification>();
private Map<Node, Shape> nodeBars = new HashMap<Node, Shape>();
private Map<Node, Point2D> tipPoints = new HashMap<Node, Point2D>();
private Map<Node, Point2D> nodePoints = new HashMap<Node, Point2D>();
private Map<Node, AffineTransform> nodeShapeTransforms = new HashMap<Node, AffineTransform>();
private Map<Node, Shape> calloutPaths = new HashMap<Node, Shape>();
}