/* * 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); } }