/*
* RectilinearTreeLayout.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.treelayouts;
import jebl.evolution.graphs.Node;
import jebl.evolution.trees.RootedTree;
import java.awt.*;
import java.awt.geom.*;
import java.util.List;
/**
* @author Andrew Rambaut
* @version $Id$
*
* $HeadURL$
*
* $LastChangedBy$
* $LastChangedDate$
* $LastChangedRevision$
*/
public class RectilinearTreeLayout extends AbstractTreeLayout {
private double curvature = 0.0;
private boolean alignTipLabels = false;
private double fishEye = 0.0;
private double pointOfInterest = 0.5;
private int tipCount = 0;
private double rootLengthProportion = 0.01;
private double yPosition;
private double yIncrement;
private double maxXPosition;
public AxisType getXAxisType() {
return AxisType.CONTINUOUS;
}
public AxisType getYAxisType() {
return AxisType.DISCRETE;
}
public boolean isShowingRootBranch() {
return true;
}
public boolean maintainAspectRatio() {
return false;
}
public double getRootLengthProportion() {
return rootLengthProportion;
}
public void setRootLengthProportion(double rootLengthProportion) {
this.rootLengthProportion = rootLengthProportion;
fireTreeLayoutChanged();
}
public double getHeightOfPoint(Point2D point) {
return point.getX();
}
public Shape getAxisLine(double height) {
double x = height;
double y1 = 0.0;
double y2 = 1.0;
return new Line2D.Double(x, y1, x, y2);
}
public Shape getHeightArea(double height1, double height2) {
double x = height1;
double y = 0.0;
double w = Math.abs(height2 - height1);
double h = 1.0;
return new Rectangle2D.Double(x, y, w, h);
}
public boolean isAlignTipLabels() {
return alignTipLabels;
}
public double getCurvature() {
return curvature;
}
public double getFishEye() {
return fishEye;
}
public double getPointOfInterest() {
return pointOfInterest;
}
public void setAlignTipLabels(boolean alignTipLabels) {
this.alignTipLabels = alignTipLabels;
fireTreeLayoutChanged();
}
public void setCurvature(double curvature) {
this.curvature = curvature;
fireTreeLayoutChanged();
}
public void setFishEye(double fishEye) {
this.fishEye = fishEye;
fireTreeLayoutChanged();
}
public void setPointOfInterest(double x, double y) {
this.pointOfInterest = y;
fireTreeLayoutChanged();
}
public boolean isShowingColouring() {
return (branchColouringAttribute != null && curvature == 0.0);
}
public void layout(RootedTree tree, TreeLayoutCache cache) {
cache.clear();
maxXPosition = 0.0;
yPosition = 0.0;
tipCount = tree.getExternalNodes().size();
yIncrement = 1.0 / (tipCount - 1);
Node root = tree.getRootNode();
setRootLength(rootLengthProportion * tree.getHeight(root));
maxXPosition = 0.0;
getMaxXPosition(tree, root, getRootLength());
Point2D rootPoint = constructNode(tree, root, 0.0, getRootLength(), cache);
constructNodeAreas(tree, root, new Area(), cache);
// construct a root branch line
double ty = transformY(rootPoint.getY());
Line2D line = new Line2D.Double(0.0, ty, rootPoint.getX(), ty);
// add the line to the map of branch paths
cache.branchPaths.put(root, line);
}
private Point2D constructNode(final RootedTree tree, final Node node, final double xParent, final double xPosition, TreeLayoutCache cache) {
Point2D nodePoint;
if (hilightAttributeName != null && node.getAttribute(hilightAttributeName) != null) {
constructHilight(tree, node, xParent, xPosition, cache);
}
if (!tree.isExternal(node)) {
if (collapsedAttributeName != null && node.getAttribute(collapsedAttributeName) != null) {
nodePoint = constructCollapsedNode(tree, node, xPosition, cache);
} else if (cartoonAttributeName != null && node.getAttribute(cartoonAttributeName) != null) {
nodePoint = constructCartoonNode(tree, node, xPosition, cache);
} else {
double yPos = 0.0;
List<Node> children = tree.getChildren(node);
boolean rotate = false;
if (node.getAttribute("!rotate") != null &&
((Boolean)node.getAttribute("!rotate"))) {
rotate = true;
}
for (int i = 0; i < children.size(); i++) {
int index = i;
if (rotate) {
index = children.size() - i - 1;
}
Node child = children.get(index);
double length = tree.getLength(child);
Point2D childPoint = constructNode(tree, child, xPosition, xPosition + length, cache);
yPos += childPoint.getY();
}
// the y-position of the node is the average of the child nodes
yPos /= children.size();
nodePoint = new Point2D.Double(xPosition, yPos);
final double ty = transformY(yPos);
// start point
final float x0 = (float) nodePoint.getX();
final float y0 = (float) ty;
for (Node child : children) {
Point2D childPoint = cache.nodePoints.get(child);
GeneralPath branchPath = new GeneralPath();
// end point
final float x1 = (float) childPoint.getX();
final float y1 = (float) transformY(childPoint.getY());
if (curvature == 0.0) {
Object[] colouring = null;
if (branchColouringAttribute != null) {
colouring = (Object[])child.getAttribute(branchColouringAttribute);
}
if (colouring != null) {
// If there is a colouring, then we break the path up into
// segments. This should allow us to iterate along the segments
// and colour them as we draw them.
float nodeHeight = (float) tree.getHeight(node);
float childHeight = (float) tree.getHeight(child);
// to help this, we are going to draw the branch backwards
branchPath.moveTo(x1, y1);
float x = x1;
for (int i = 0; i < colouring.length - 1; i+=2) {
// float height = ((Number)colouring[i+1]).floatValue();
// float p = (height - childHeight) / (nodeHeight - childHeight);
float interval = ((Number)colouring[i+1]).floatValue();
float p = interval / (nodeHeight - childHeight);
x -= ((x1 - x0) * p);
branchPath.lineTo(x, y1);
}
branchPath.lineTo(x0, y1);
branchPath.lineTo(x0, y0);
} else {
branchPath.moveTo(x1, y1);
branchPath.lineTo(x0, y1);
branchPath.lineTo(x0, y0);
}
} else if (curvature == 1.0) {
// The extreme is to use a triangular look
branchPath.moveTo(x0, y0);
branchPath.lineTo(x1, y1);
} else {
// if the curvature is on then we simply don't
// do tree colouring - I just can't be bothered to
// implement it (and it would probably be confusing anyway).
float x2 = x1 - ((x1 - x0) * (float) (1.0 - curvature));
float y2 = y0 + ((y1 - y0) * (float) (1.0 - curvature));
branchPath.moveTo(x1, y1);
branchPath.lineTo(x2, y1);
branchPath.quadTo(x0, y1, x0, y2);
branchPath.lineTo(x0, y0);
}
// add the branchPath to the map of branch paths
cache.branchPaths.put(child, branchPath);
double x3 = (nodePoint.getX() + childPoint.getX()) / 2;
Line2D branchLabelPath = new Line2D.Double(
x3 - 1.0, y1,
x3 + 1.0, y1);
cache.branchLabelPaths.put(child, branchLabelPath);
}
Line2D nodeLabelPath = new Line2D.Double(
nodePoint.getX(), ty,
nodePoint.getX() + 1.0, ty);
cache.nodeLabelPaths.put(node, nodeLabelPath);
Line2D nodeShapePath = new Line2D.Double(
nodePoint.getX(), ty,
nodePoint.getX() - 1.0, ty);
cache.nodeShapePaths.put(node, nodeShapePath);
}
} else {
nodePoint = new Point2D.Double(xPosition, yPosition);
double ty = transformY(yPosition);
Line2D tipLabelPath;
if (alignTipLabels) {
tipLabelPath = new Line2D.Double(
maxXPosition, ty,
maxXPosition + 1.0, ty);
Line2D calloutPath = new Line2D.Double(
nodePoint.getX(), ty,
maxXPosition, ty);
cache.calloutPaths.put(node, calloutPath);
} else {
tipLabelPath = new Line2D.Double(
nodePoint.getX(), ty,
nodePoint.getX() + 1.0, ty);
}
cache.tipLabelPaths.put(node, tipLabelPath);
Line2D nodeShapePath = new Line2D.Double(
nodePoint.getX(), ty,
nodePoint.getX() - 1.0, ty);
cache.nodeShapePaths.put(node, nodeShapePath);
yPosition += yIncrement;
}
// add the node point to the map of node points
cache.nodePoints.put(node, nodePoint);
return nodePoint;
}
private void constructNodeAreas(final RootedTree tree, final Node node, final Area parentNodeArea, TreeLayoutCache cache) {
if (!tree.isExternal(node) &&
(collapsedAttributeName == null || node.getAttribute(collapsedAttributeName) == null) &&
(cartoonAttributeName == null || node.getAttribute(cartoonAttributeName) == null)) {
List<Node> children = tree.getChildren(node);
boolean rotate = false;
if (node.getAttribute("!rotate") != null &&
((Boolean)node.getAttribute("!rotate"))) {
rotate = true;
}
int index = (rotate ? children.size() - 1 : 0);
Node child1 = children.get(index);
Area childArea1 = new Area();
constructNodeAreas(tree, child1, childArea1, cache);
Rectangle2D branchBounds1 = cache.getBranchPath(child1).getBounds2D();
index = (rotate ? 0 : children.size() - 1);
Node child2 = children.get(index);
Area childArea2 = new Area();
constructNodeAreas(tree, child2, childArea2, cache);
Rectangle2D branchBounds2 = cache.getBranchPath(child2).getBounds2D();
GeneralPath nodePath = new GeneralPath();
// start point
final float x0 = (float) branchBounds1.getX();
final float y0 = (float) (branchBounds1.getY() + branchBounds1.getHeight());
nodePath.moveTo(x0, y0);
if (curvature == 0.0) {
final float y1 = (float) branchBounds1.getY();
nodePath.lineTo(x0, y1);
nodePath.lineTo((float)maxXPosition, y1);
final float y2 = (float) (branchBounds2.getY() + branchBounds2.getHeight());
nodePath.lineTo((float)maxXPosition, y2);
nodePath.lineTo(x0, y2);
} else if (curvature == 1.0) {
// The extreme is to use a triangular look
final float x1 = (float) (branchBounds1.getX() + branchBounds1.getWidth());
final float y1 = (float) branchBounds1.getY();
nodePath.lineTo(x1, y1);
nodePath.lineTo((float)maxXPosition, y1);
final float y2 = (float) (branchBounds2.getY() + branchBounds2.getHeight());
nodePath.lineTo((float)maxXPosition, y2);
final float x2 = (float) (branchBounds2.getX() + branchBounds2.getWidth());
nodePath.lineTo(x2, y2);
} else {
final float x1 = (float) (branchBounds1.getX() + branchBounds1.getWidth());
final float y1 = (float) branchBounds1.getY();
float x2 = x1 - ((x1 - x0) * (float) (1.0 - curvature));
float y2 = y0 - ((y0 - y1) * (float) (1.0 - curvature));
nodePath.lineTo(x0, y2);
nodePath.quadTo(x0, y1, x2, y1);
nodePath.lineTo((float)maxXPosition, y1);
final float y3 = (float) (branchBounds2.getY() + branchBounds2.getHeight());
nodePath.lineTo((float)maxXPosition, y3);
final float x3 = (float) (branchBounds2.getX() + branchBounds2.getWidth());
final float x4 = x3 - ((x3 - x0) * (float) (1.0 - curvature));
final float y4 = y0 + ((y3 - y0) * (float) (1.0 - curvature));
nodePath.lineTo(x4, y3);
nodePath.quadTo(x0, y3, x0, y4);
}
nodePath.lineTo(x0, y0);
nodePath.closePath();
Area nodeArea = new Area(nodePath);
parentNodeArea.add(nodeArea);
parentNodeArea.add(childArea1);
parentNodeArea.add(childArea2);
nodeArea.subtract(childArea1);
nodeArea.subtract(childArea2);
cache.nodeAreas.put(node, nodeArea);
}
}
private Point2D constructCartoonNode(RootedTree tree, Node node, double xPosition, TreeLayoutCache cache) {
Point2D nodePoint;
Object[] values = (Object[])node.getAttribute(cartoonAttributeName);
int tipCount = (Integer)values[0];
double tipHeight = (Double)values[1];
double height = tree.getHeight(node);
double maxXPos = xPosition + height - tipHeight;
double minYPos = yPosition;
yPosition += yIncrement * (tipCount - 1);
double maxYPos = yPosition;
yPosition += yIncrement;
// the y-position of the node is the average of the child nodes
double yPos = (maxYPos + minYPos) / 2;
nodePoint = new Point2D.Double(xPosition, yPos);
GeneralPath collapsedShape = new GeneralPath();
// start point
float x0 = (float)nodePoint.getX();
float y0 = (float)transformY(nodePoint.getY());
// end point
float x1 = (float)maxXPos;
float y1 = (float)transformY(minYPos);
float y2 = (float)transformY(maxYPos);
collapsedShape.moveTo(x0, y0);
collapsedShape.lineTo(x1, y1);
collapsedShape.lineTo(x1, y2);
collapsedShape.closePath();
// add the collapsedShape to the map of branch paths
cache.collapsedShapes.put(node, collapsedShape);
Line2D nodeLabelPath = new Line2D.Double(
nodePoint.getX(), y0,
nodePoint.getX() + 1.0, y0);
cache.nodeLabelPaths.put(node, nodeLabelPath);
Line2D nodeBarPath = new Line2D.Double(
nodePoint.getX(), y0,
nodePoint.getX() - 1.0, y0);
cache.nodeShapePaths.put(node, nodeBarPath);
if (showingCartoonTipLabels) {
constructCartoonTipLabelPaths(tree, node, maxXPos, new double[] { minYPos }, cache);
}
return nodePoint;
}
private void constructCartoonTipLabelPaths(RootedTree tree, Node node,
double xPosition, double[] yPosition,
TreeLayoutCache cache) {
if (!tree.isExternal(node)) {
for (Node child : tree.getChildren(node)) {
constructCartoonTipLabelPaths(tree, child, xPosition, yPosition, cache);
}
} else {
Point2D nodePoint = new Point2D.Double(xPosition, yPosition[0]);
double x0 = nodePoint.getX();
double y0 = transformY(nodePoint.getY());
Line2D tipLabelPath;
if (alignTipLabels) {
tipLabelPath = new Line2D.Double(maxXPosition, y0, maxXPosition + 1.0, y0);
Line2D calloutPath = new Line2D.Double(x0, y0, maxXPosition, y0);
cache.calloutPaths.put(node, calloutPath);
} else {
tipLabelPath = new Line2D.Double(x0, y0, x0 + 1.0, y0);
}
cache.tipLabelPaths.put(node, tipLabelPath);
yPosition[0] += yIncrement;
}
}
private Point2D constructCollapsedNode(RootedTree tree, Node node, double xPosition, TreeLayoutCache cache) {
Point2D nodePoint;
Object[] values = (Object[])node.getAttribute(collapsedAttributeName);
double tipHeight = (Double)values[1];
double height = tree.getHeight(node);
double maxXPos = xPosition + height - tipHeight;
double minYPos = yPosition - (yIncrement * 0.5);
double maxYPos = minYPos + yIncrement;
yPosition += yIncrement;
// the y-position of the node is the average of the child nodes
double yPos = (maxYPos + minYPos) / 2;
nodePoint = new Point2D.Double(xPosition, yPos);
double ty = transformY(yPos);
GeneralPath collapsedShape = new GeneralPath();
// start point
float x0 = (float)nodePoint.getX();
float y0 = (float)ty;
// end point
float x1 = (float)maxXPos;
float y1 = (float)transformY(minYPos);
float y2 = (float)transformY(maxYPos);
collapsedShape.moveTo(x0, y0);
collapsedShape.lineTo(x1, y1);
collapsedShape.lineTo(x1, y2);
collapsedShape.closePath();
// add the collapsedShape to the map of branch paths
cache.collapsedShapes.put(node, collapsedShape);
Line2D nodeLabelPath = new Line2D.Double(xPosition, ty, xPosition + 1.0, ty);
cache.nodeLabelPaths.put(node, nodeLabelPath);
Line2D nodeBarPath = new Line2D.Double(xPosition, ty, xPosition - 1.0, ty);
cache.nodeShapePaths.put(node, nodeBarPath);
Line2D tipLabelPath;
if (alignTipLabels) {
tipLabelPath = new Line2D.Double(
maxXPosition, ty,
maxXPosition + 1.0, ty);
Line2D calloutPath = new Line2D.Double(
maxXPos, ty,
maxXPosition, ty);
cache.calloutPaths.put(node, calloutPath);
} else {
tipLabelPath = new Line2D.Double(maxXPos, ty, maxXPos + 1.0, ty);
}
cache.tipLabelPaths.put(node, tipLabelPath);
return nodePoint;
}
private void constructHilight(RootedTree tree, Node node, double xParent, double xPosition, TreeLayoutCache cache) {
Object[] values = (Object[])node.getAttribute(hilightAttributeName);
int tipCount = (Integer)values[0];
double tipHeight = (Double)values[1];
double height = tree.getHeight(node);
GeneralPath hilightShape = new GeneralPath();
float x0 = (float)((xPosition + xParent) / 2.0);
float x1 = (float)(xPosition + height /*- tipHeight*/);
double tmp = yPosition - (yIncrement / 2);
float y0 = (float)transformY(tmp);
float y1 = (float)transformY(tmp + (yIncrement * tipCount));
hilightShape.moveTo(x0, y0);
hilightShape.lineTo(x1, y0);
hilightShape.lineTo(x1, y1);
hilightShape.lineTo(x0, y1);
hilightShape.closePath();
// add the collapsedShape to the map of branch paths
cache.hilightNodes.add(node);
cache.hilightShapes.put(node, hilightShape);
}
private void getMaxXPosition(RootedTree tree, Node node, double xPosition) {
if (!tree.isExternal(node)) {
List<Node> children = tree.getChildren(node);
for (Node child : children) {
double length = tree.getLength(child);
getMaxXPosition(tree, child, xPosition + length);
}
} else {
if (xPosition > maxXPosition) {
maxXPosition = xPosition;
}
}
}
private double transformY(double y) {
if (fishEye == 0.0) {
return y;
}
double scale = 1.0 / (fishEye * tipCount);
double dist = pointOfInterest - y;
double min = 1.0 - (pointOfInterest / (scale + pointOfInterest));
double max = 1.0 - ((pointOfInterest - 1.0) / (scale - (pointOfInterest - 1.0)));
double c = 1.0 - (dist < 0 ? (dist / (scale - dist)) : (dist / (scale + dist)));
return (c - min) / (max - min);
}
}