/* * PolarTreeLayout.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 PolarTreeLayout extends AbstractTreeLayout { private double rootAngle = 180.0; private double rootLengthProportion = 0.01; private double angularRange = 360.0; private double fishEye = 0.0; private double pointOfInterest = 0.5; private int tipCount = 0; private double totalRootLength = 0.0; private boolean showingRootBranch = true; private TipLabelPosition tipLabelPosition = TipLabelPosition.FLUSH; private double yPosition; private double yIncrement; private double maxXPosition; public enum TipLabelPosition { FLUSH, RADIAL, HORIZONTAL } public AxisType getXAxisType() { return AxisType.CONTINUOUS; } public AxisType getYAxisType() { return AxisType.CONTINUOUS; } public boolean maintainAspectRatio() { return true; } public void setFishEye(double fishEye) { this.fishEye = fishEye; fireTreeLayoutChanged(); } public void setPointOfInterest(double x, double y) { this.pointOfInterest = getPolarAngle(x, y); fireTreeLayoutChanged(); } public double getHeightOfPoint(Point2D point) { throw new UnsupportedOperationException("Method getHeightOfPoint() is not supported in this TreeLayout"); } public Shape getAxisLine(double height) { double x = height; return new Ellipse2D.Double(-x, -x, x * 2.0, x * 2.0); } public Shape getHeightArea(double height1, double height2) { Area area1 = new Area(new Ellipse2D.Double(0.0, 0.0, height2 * 2.0, height2 * 2.0)); Area area2 = new Area(new Ellipse2D.Double(0.0, 0.0, height1 * 2.0, height1 * 2.0)); area1.subtract(area2); return area1; } public double getRootAngle() { return rootAngle; } public double getAngularRange() { return angularRange; } public boolean isShowingRootBranch() { return showingRootBranch; } public double getTotalRootLength() { return totalRootLength; } public double getRootLengthProportion() { return rootLengthProportion; } public TipLabelPosition getTipLabelPosition() { return tipLabelPosition; } public void setRootAngle(double rootAngle) { this.rootAngle = rootAngle; constant = rootAngle - ((360.0 - angularRange) * 0.5); fireTreeLayoutChanged(); } public void setAngularRange(double angularRange) { this.angularRange = angularRange; constant = rootAngle - ((360.0 - angularRange) * 0.5); fireTreeLayoutChanged(); } public void setShowingRootBranch(boolean showingRootBranch) { this.showingRootBranch = showingRootBranch; fireTreeLayoutChanged(); } public void setRootLengthProportion(double rootLengthProportion) { this.rootLengthProportion = rootLengthProportion; fireTreeLayoutChanged(); } public void setTipLabelPosition(TipLabelPosition tipLabelPosition) { this.tipLabelPosition = tipLabelPosition; fireTreeLayoutChanged(); } public boolean isShowingColouring() { return (branchColouringAttribute != null); } public void layout(RootedTree tree, TreeLayoutCache cache) { cache.clear(); Node root = tree.getRootNode(); double totalRootLength = (rootLengthProportion * tree.getHeight(root)) * 10.0; maxXPosition = 0.0; getMaxXPosition(tree, root, totalRootLength); yPosition = 0.0; tipCount = tree.getExternalNodes().size(); yIncrement = 1.0 / tipCount; final Point2D rootPoint = constructNode(tree, root, 0.0, totalRootLength, /*new Area(),*/ cache); // constructNodeAreas(tree, root, new Area(), cache); if (showingRootBranch) { // construct a root branch line final double y = rootPoint.getY(); Line2D line = new Line2D.Double(transform(0.0, y), transform(rootPoint.getX(), y)); // add the line to the map of branch paths cache.branchPaths.put(root, line); } } private Point2D constructNode(RootedTree tree, Node node, double xParent, double xPosition, /*final Area parentNodeArea,*/ 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> childList = tree.getChildren(node); Node[] children = new Node[childList.size()]; Point2D[] childPoints = new Point2D[childList.size()]; boolean rotate = false; if (node.getAttribute("!rotate") != null && ((Boolean)node.getAttribute("!rotate"))) { rotate = true; } // Area[] childAreas = new Area[childList.size()]; for (int i = 0; i < childList.size(); i++) { int index = i; if (rotate) { index = childList.size() - i - 1; } children[i] = childList.get(index); final double length = tree.getLength(children[i]); // childAreas[i] = new Area(); childPoints[i] = constructNode(tree, children[i], xPosition, xPosition + length, cache); // parentNodeArea.add(childAreas[i]); yPos += childPoints[i].getY(); } // the y-position of the node is the average of the child nodes yPos /= childList.size(); nodePoint = new Point2D.Double(xPosition, yPos); Point2D transformedNodePoint = transform(nodePoint); final double start = getAngle(yPos); // GeneralPath nodeAreaPath = new GeneralPath(); double firstChildAngle = 0; for (int i = 0; i < childList.size(); i++) { int index = i; if (rotate) { index = childList.size() - i - 1; } GeneralPath branchPath = new GeneralPath(); final Point2D transformedChildPoint = transform(childPoints[i]); final Point2D transformedShoulderPoint = transform( nodePoint.getX(), childPoints[i].getY()); // if (i == 0) { // nodeAreaPath.moveTo( // (float) transformedShoulderPoint.getX(), // (float) transformedShoulderPoint.getY()); // final Point2D transformedPoint2 = transform( // maxXPosition, childPoints[i].getY()); // nodeAreaPath.lineTo( // (float) transformedPoint2.getX(), // (float) transformedPoint2.getY()); // firstChildAngle = getAngle(childPoints[i].getY()); // } Object[] colouring = null; if (branchColouringAttribute != null) { colouring = (Object[])children[i].getAttribute(branchColouringAttribute); } if (colouring != null) { // If there is a colouring, then we break the path up into // segments. This should allow use to iterate along the segments // and colour them as we draw them. float nodeHeight = (float) tree.getHeight(node); float childHeight = (float) tree.getHeight(children[i]); double x1 = childPoints[i].getX(); double x0 = nodePoint.getX(); branchPath.moveTo( (float) transformedChildPoint.getX(), (float) transformedChildPoint.getY()); float x = (float)x1; for (int j = 0; j < colouring.length - 1; j+=2) { // double height = ((Number)colouring[j+1]).doubleValue(); // double p = (height - childHeight) / (nodeHeight - childHeight); float interval = ((Number)colouring[j+1]).floatValue(); float p = interval / (nodeHeight - childHeight); x -= ((x1 - x0) * p); final Point2D transformedPoint = transform(x, childPoints[index].getY()); branchPath.lineTo( (float) transformedPoint.getX(), (float) transformedPoint.getY()); } branchPath.lineTo( (float) transformedShoulderPoint.getX(), (float) transformedShoulderPoint.getY()); } else { branchPath.moveTo( (float) transformedChildPoint.getX(), (float) transformedChildPoint.getY()); branchPath.lineTo( (float) transformedShoulderPoint.getX(), (float) transformedShoulderPoint.getY()); } final double finish = getAngle(childPoints[index].getY()); Arc2D arc = new Arc2D.Double(); arc.setArcByCenter(0.0, 0.0, nodePoint.getX(), finish, start - finish, Arc2D.OPEN); branchPath.append(arc, true); // if (i == childList.size() - 1) { // Arc2D arc2 = new Arc2D.Double(); // arc2.setArcByCenter(0.0, 0.0, maxXPosition, firstChildAngle, finish - firstChildAngle, Arc2D.OPEN); // nodeAreaPath.append(arc2, true); // // nodeAreaPath.lineTo( // (float) transformedShoulderPoint.getX(), // (float) transformedShoulderPoint.getY()); // // Arc2D arc3 = new Arc2D.Double(); // arc3.setArcByCenter(0.0, 0.0, nodePoint.getX(), finish, firstChildAngle - finish, Arc2D.OPEN); // nodeAreaPath.append(arc3, true); // } // add the branchPath to the map of branch paths cache.branchPaths.put(children[i], branchPath); final double x3 = (nodePoint.getX() + childPoints[index].getX()) / 2; Line2D branchLabelPath = new Line2D.Double( transform(x3 - 1.0, childPoints[index].getY()), transform(x3 + 1.0, childPoints[index].getY())); cache.branchLabelPaths.put(children[i], branchLabelPath); } // nodeAreaPath.closePath(); // // Area nodeArea = new Area(nodeAreaPath); // parentNodeArea.add(nodeArea); // // for (Area childArea : childAreas) { // nodeArea.subtract(childArea); // } // // cache.nodeAreas.put(node, nodeArea); Line2D nodeLabelPath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() + 1.0, yPos)); cache.nodeLabelPaths.put(node, nodeLabelPath); Line2D nodeShapePath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() - 1.0, yPos)); cache.nodeShapePaths.put(node, nodeShapePath); // add the node point to the map of node points cache.nodePoints.put(node, transformedNodePoint); } } else { nodePoint = new Point2D.Double(xPosition, yPosition); Point2D transformedNodePoint = transform(nodePoint); Line2D tipLabelPath; if (tipLabelPosition == TipLabelPosition.FLUSH) { tipLabelPath = new Line2D.Double(transformedNodePoint, transform(xPosition + 1.0, yPosition)); } else if (tipLabelPosition == TipLabelPosition.RADIAL) { tipLabelPath = new Line2D.Double(transform(maxXPosition, yPosition), transform(maxXPosition + 1.0, yPosition)); Line2D calloutPath = new Line2D.Double(transformedNodePoint, transform(maxXPosition, yPosition)); cache.calloutPaths.put(node, calloutPath); } else if (tipLabelPosition == TipLabelPosition.HORIZONTAL) { // this option disabled in getControls (JH) throw new UnsupportedOperationException("Not implemented yet"); } else { // this is a bug throw new IllegalArgumentException("Unrecognized enum value"); } cache.tipLabelPaths.put(node, tipLabelPath); Line2D nodeShapePath = new Line2D.Double( transform(nodePoint.getX(), yPosition), transform(nodePoint.getX() - 1.0, yPosition)); cache.nodeShapePaths.put(node, nodeShapePath); yPosition += yIncrement; // add the node point to the map of node points cache.nodePoints.put(node, transformedNodePoint); } return nodePoint; } private void constructNodeAreas(final RootedTree tree, final Node node, final Area parentNodeArea, TreeLayoutCache cache) { if (!tree.isExternal(node)) { 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); index = (rotate ? 0 : children.size() - 1); Node child2 = children.get(index); Area childArea2 = new Area(); constructNodeAreas(tree, child2, childArea2, cache); GeneralPath nodePath = new GeneralPath(); PathIterator pi1 = cache.getBranchPath(child1).getPathIterator(null); nodePath.append(pi1, false); // start point // final float x0 = (float) branchBounds1.getX(); // final float y0 = (float) (branchBounds1.getY() + branchBounds1.getHeight()); // nodePath.moveTo(x0, y0); // // final float y1 = (float) branchBounds1.getY(); // nodePath.lineTo(x0, y1); // // nodePath.lineTo(maxXPosition, y1); // // final float y2 = (float) (branchBounds2.getY() + branchBounds2.getHeight()); // nodePath.lineTo(maxXPosition, y2); // // nodePath.lineTo(x0, y2); // // 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); Point2D transformedNodePoint0 = transform(nodePoint); Point2D transformedNodePoint1 = transform(new Point2D.Double(maxXPos, minYPos)); Point2D transformedNodePoint2 = transform(new Point2D.Double(maxXPos, maxYPos)); GeneralPath collapsedShape = new GeneralPath(); collapsedShape.moveTo((float)transformedNodePoint0.getX(), (float)transformedNodePoint0.getY()); collapsedShape.lineTo((float)transformedNodePoint1.getX(), (float)transformedNodePoint1.getY()); final double start = getAngle(maxYPos); final double finish = getAngle(minYPos); Arc2D arc = new Arc2D.Double(); arc.setArcByCenter(0.0, 0.0, maxXPos, finish, start - finish, Arc2D.OPEN); collapsedShape.append(arc, true); collapsedShape.closePath(); // add the collapsedShape to the map of branch paths cache.collapsedShapes.put(node, collapsedShape); Line2D nodeLabelPath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() + 1.0, yPos)); cache.nodeLabelPaths.put(node, nodeLabelPath); Line2D nodeBarPath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() - 1.0, yPos)); cache.nodeShapePaths.put(node, nodeBarPath); if (showingCartoonTipLabels) { constructCartoonTipLabelPaths(tree, node, maxXPos, new double[] { minYPos }, cache); } // add the node point to the map of node points cache.nodePoints.put(node, transformedNodePoint0); 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]); Point2D transformedNodePoint = transform(nodePoint); Line2D tipLabelPath; if (tipLabelPosition == TipLabelPosition.FLUSH) { tipLabelPath = new Line2D.Double(transformedNodePoint, transform(xPosition + 1.0, yPosition[0])); } else if (tipLabelPosition == TipLabelPosition.RADIAL) { tipLabelPath = new Line2D.Double(transform(maxXPosition, yPosition[0]), transform(maxXPosition + 1.0, yPosition[0])); Line2D calloutPath = new Line2D.Double(transformedNodePoint, transform(maxXPosition, yPosition[0])); cache.calloutPaths.put(node, calloutPath); } else if (tipLabelPosition == TipLabelPosition.HORIZONTAL) { // this option disabled in getControls (JH) throw new UnsupportedOperationException("Not implemented yet"); } else { // this is a bug throw new IllegalArgumentException("Unrecognized enum value"); } 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); //String tipName = (String)values[0]; 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); Point2D transformedNodePoint0 = transform(nodePoint); Point2D transformedNodePoint1 = transform(new Point2D.Double(maxXPos, minYPos)); GeneralPath collapsedShape = new GeneralPath(); collapsedShape.moveTo((float)transformedNodePoint0.getX(), (float)transformedNodePoint0.getY()); collapsedShape.lineTo((float)transformedNodePoint1.getX(), (float)transformedNodePoint1.getY()); final double start = getAngle(maxYPos); final double finish = getAngle(minYPos); Arc2D arc = new Arc2D.Double(); arc.setArcByCenter(0.0, 0.0, maxXPos, finish, start - finish, Arc2D.OPEN); collapsedShape.append(arc, true); collapsedShape.closePath(); // add the collapsedShape to the map of branch paths cache.collapsedShapes.put(node, collapsedShape); Line2D nodeLabelPath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() + 1.0, yPos)); cache.nodeLabelPaths.put(node, nodeLabelPath); Line2D nodeBarPath = new Line2D.Double( transform(nodePoint.getX(), yPos), transform(nodePoint.getX() - 1.0, yPos)); cache.nodeShapePaths.put(node, nodeBarPath); Point2D transformedNodePoint = transform(maxXPos, yPos); Line2D tipLabelPath; if (tipLabelPosition == TipLabelPosition.FLUSH) { tipLabelPath = new Line2D.Double(transformedNodePoint, transform(maxXPos + 1.0, yPos)); } else if (tipLabelPosition == TipLabelPosition.RADIAL) { tipLabelPath = new Line2D.Double(transform(maxXPosition, yPos), transform(maxXPosition + 1.0, yPos)); Line2D calloutPath = new Line2D.Double(transformedNodePoint, transform(maxXPosition, yPos)); cache.calloutPaths.put(node, calloutPath); } else if (tipLabelPosition == TipLabelPosition.HORIZONTAL) { // this option disabled in getControls (JH) throw new UnsupportedOperationException("Not implemented yet"); } else { // this is a bug throw new IllegalArgumentException("Unrecognized enum value"); } cache.tipLabelPaths.put(node, tipLabelPath); // add the node point to the map of node points cache.nodePoints.put(node, transformedNodePoint0); 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(); double x0 = ((xPosition + xParent) / 2.0); double x1 = (xPosition + height /*- tipHeight*/); double y0 = yPosition - (yIncrement / 2); double y1 = yPosition + (yIncrement * tipCount) - (yIncrement / 2); Point2D p1 = transform(new Point2D.Double(x0, y0)); Point2D p2 = transform(new Point2D.Double(x1, y0)); Point2D p3 = transform(new Point2D.Double(x1, y1)); Point2D p4 = transform(new Point2D.Double(x0, y1)); final double start = getAngle(y0); final double finish = getAngle(y1); hilightShape.moveTo((float)p1.getX(), (float)p1.getY()); hilightShape.lineTo((float)p2.getX(), (float)p2.getY()); Arc2D arc = new Arc2D.Double(); arc.setArcByCenter(0.0, 0.0, x1, start, finish - start, Arc2D.OPEN); hilightShape.append(arc, true); hilightShape.lineTo((float)p3.getX(), (float)p3.getY()); arc = new Arc2D.Double(); arc.setArcByCenter(0.0, 0.0, x0, finish, start - finish, Arc2D.OPEN); hilightShape.append(arc, true); 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) { final double length = tree.getLength(child); getMaxXPosition(tree, child, xPosition + length); } } else { if (xPosition > maxXPosition) { maxXPosition = xPosition; } } } /** * Polar transform * * @param point * @return the point in polar space */ private Point2D transform(Point2D point) { return transform(point.getX(), point.getY()); } /** * Polar transform * * @param h the hypotenuse * @param a the angle * @return the point in euclidean space */ private Point2D transform(double h, double a) { double r = - Math.toRadians(getAngle(a)); return new Point2D.Double(h * Math.cos(r), h * Math.sin(r)); } private double constant; /** * Polar angle for an x, y coordinate * * @param x * @param y * @return the angle */ private double getPolarAngle(double x, double y) { double r = Math.toDegrees(Math.atan(y/x)); return (constant - r) / angularRange; } /** * The angle in degrees given by a 0, 1 proportion of the circle * @param y * @return */ private double getAngle(double y) { if (fishEye == 0.0) { return constant - (y * angularRange); } 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))); double tY = (c - min) / (max - min); return rootAngle - ((360.0 - angularRange) * 0.5) - (tY * angularRange); } }