//******************************************************************************
//
// File:    TreeDrawing.java
// Package: edu.rit.compbio.phyl
// Unit:    Class edu.rit.compbio.phyl.TreeDrawing
//
// This Java source file is copyright (C) 2008 by Alan Kaminsky. All rights
// reserved. For further information, contact the author, Alan Kaminsky, at
// ark@cs.rit.edu.
//
// This Java source file is part of the Parallel Java Library ("PJ"). PJ 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 3 of the License, or (at your option) any later version.
//
// PJ 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.
//
// A copy of the GNU General Public License is provided in the file gpl.txt. You
// may also obtain a copy of the GNU General Public License on the World Wide
// Web at http://www.gnu.org/licenses/gpl.html.
//
//******************************************************************************

package edu.rit.compbio.phyl;

import edu.rit.draw.Drawing;

import edu.rit.draw.item.Group;
import edu.rit.draw.item.Line;
import edu.rit.draw.item.Point;
import edu.rit.draw.item.Text;

import java.io.PrintStream;

import java.text.DecimalFormat;

import java.util.LinkedList;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class TreeDrawing provides an object that draws a picture of a phylogenetic
 * tree. The tree is specified either as a {@linkplain DnaSequenceTree} object
 * or as a string in a subset of Newick Standard format.
 * 

* Note: Class TreeDrawing is not multiple thread safe. *

* Newick Standard format. The subset of Newick Standard format class * TreeDrawing supports is described by this BNF syntax. Nonterminal symbols are * in italic font, terminal symbols are in typewriter font, | * designates alternatives. *

* tree ::= node ; *
node ::= ( childList ) nameLength *
childList ::= child | child , childList *
child ::= tip | node *
tip ::= nameLength *
nameLength ::= empty | name | : length | name : length *
name ::= Any sequence of non-whitespace characters except ( ) , : ; *
length ::= Floating point number, as in the Double.valueOf() method *

* This subset only supports non-quoted names with no whitespace; however, any * underscore character _ in a name is replaced with a space character. * The full Newick Standard format supports quoted names including whitespace. * For further information about Newick Standard format, see: *

*

* Here is an example tree: *

* "(Gibbon:10,(Orangutan:8,(Gorilla:6,(Chimp:4,Human:4)))apes)primates;" *

* Here is the resulting drawing: *

* * * @author Alan Kaminsky * @version 25-Jul-2008 */ public class TreeDrawing { // Hidden constants. // Vertical distance between tip strings. private static final double V = 27.0; // Length of lines from a node to its descendents. private static final double H = 72.0; // Gap from line to tip text. private static final double GAP = 6.0; // Gap from line to branch length text. private static final double VGAP = 3.0; // Regular expression for parsing tokens in Newick Standard format. private static final Pattern pattern = Pattern.compile ("\\(|\\)|,|:|;|[^(),:; \\t\\n\\x0B\\f\\r]+"); // Hidden data members. // Tree string in Newick Standard format. private String treestring; // For formatting branch lengths. private DecimalFormat format = new DecimalFormat ("0.00"); // Matcher for parsing tokens. private Matcher matcher; // Current token and its starting index in . private String token; private int index; // Number of tips. private int tipCount; // Group in which to store the tree drawing. private Group group; // Exported constructors. /** * Construct a new tree drawing object. */ public TreeDrawing() { } // Main program. /** * Specify the format with which to draw branch lengths. The format string * is used to construct a java.text.DecimalFormat object. If not specified, * the default format string is "0.00". * * @param format Format string. */ public void setBranchLengthFormat (String format) { this.format = new DecimalFormat (format); } /** * Draw a picture of the given tree. The picture is added to the default * {@linkplain edu.rit.draw.Drawing} object. * * @param tree DNA sequence tree. */ public void draw (DnaSequenceTree tree) { draw (tree, Drawing.defaultDrawing()); } /** * Draw a picture of the given tree. The picture is added to the given * {@linkplain edu.rit.draw.Drawing} object. * * @param tree DNA sequence tree. * @param dwg Drawing object. */ public void draw (DnaSequenceTree tree, Drawing dwg) { draw (tree, new Group()); dwg.add (group); } /** * Draw a picture of the given tree. The picture is added to the given * {@linkplain edu.rit.draw.item.Group} object. * * @param tree DNA sequence tree. * @param group Drawing group object. */ public void draw (DnaSequenceTree tree, Group group) { // Initialize. this.tipCount = 0; this.group = group; // Traverse the tree, building up the drawing as it goes. Point root = drawNode (tree, tree.root()); // Add final line to root. group.append (new Line().to(root).hby(-H)); } /** * Draw a picture of the given tree string. The picture is added to the * default {@linkplain edu.rit.draw.Drawing} object. * * @param tree Tree as a string in Newick Standard format. * * @exception SyntaxException * Thrown if there was a syntax error in tree. */ public void draw (String tree) throws SyntaxException { draw (tree, Drawing.defaultDrawing()); } /** * Draw a picture of the given tree string. The picture is added to the * given {@linkplain edu.rit.draw.Drawing} object. * * @param tree Tree as a string in Newick Standard format. * @param dwg Drawing object. * * @exception SyntaxException * Thrown if there was a syntax error in tree. */ public void draw (String tree, Drawing dwg) throws SyntaxException { draw (tree, new Group()); dwg.add (group); } /** * Draw a picture of the given tree string. The picture is added to the * given {@linkplain edu.rit.draw.item.Group} object. * * @param tree Tree as a string in Newick Standard format. * @param group Drawing group object. * * @exception SyntaxException * Thrown if there was a syntax error in tree. */ public void draw (String tree, Group group) throws SyntaxException { // Initialize. this.treestring = tree; this.tipCount = 0; this.group = group; // Set up matcher. matcher = pattern.matcher (treestring); nextToken(); // Parse the tree, building up the drawing as it goes. Point root = parseNode(); if (! ";".equals (token)) syntaxError ("; expected"); nextToken(); if (token != null) syntaxError ("Extra characters"); // Add final line to root. group.append (new Line().to(root).hby(-H)); } // Hidden operations. /** * Draw a node. * * @param tree DNA sequence tree. * @param index Node index. * * @return Point to which to attach line from parent. */ private Point drawNode (DnaSequenceTree tree, int index) { Point p; // Draw a tip node. boolean isTip = tree.child1 (index) == -1; if (isTip) { p = new Point (0, tipCount * V); ++ tipCount; } // Draw an interior node. else { Point p1 = drawNode (tree, tree.child1 (index)); Point p2 = drawNode (tree, tree.child2 (index)); double x = Math.min (p1.x(), p2.x()) - H; double y1 = p1.y(); double y2 = p2.y(); p = new Point (x, (y1 + y2)/2); group.append (new Line().to(p1).hto(x).vto(p2).hto(p2)); } // Draw node name. DnaSequence seq = tree.seq (index); if (seq != null) drawName (seq.name(), p); // Draw branch length. Double brlen = tree.branchLength (index); if (brlen != null) drawBranchLength (brlen, p, isTip); return p; } /** * Draw a node name. * * @param name Node name, or null if none. * @param p Node's anchor point. */ private void drawName (String name, Point p) { if (name != null) { group.append (new Text().text(name).w(p.e(GAP))); } } /** * Draw a branch length. * * @param brlen Branch length. * @param p Node's anchor point. * @param isTip True if node is a tip node, false otherwise. */ private void drawBranchLength (double brlen, Point p, boolean isTip) { if (isTip) { group.append (new Text().text(format.format(brlen)).se(p.n(VGAP).w(GAP))); } else { group.append (new Text().text(format.format(brlen)).s(p.n(VGAP).w(H/2))); } } /** * Parse a node. * * @return Point to which to attach line from parent. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private Point parseNode() throws SyntaxException { if (! "(".equals (token)) syntaxError ("( expected"); nextToken(); Point p = parseChildList(); if (! ")".equals (token)) syntaxError (") expected"); nextToken(); parseNameLength (p, false); return p; } /** * Parse a name and/or length. * * @param p Point at which to anchor name and branch length. * @param isTip True if node is a tip node, false otherwise. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private void parseNameLength (Point p, boolean isTip) throws SyntaxException { if (isName (token)) { drawName (token.replaceAll ("_", " "), p); nextToken(); } if (":".equals (token)) { nextToken(); double brlen = 0.0; try { brlen = new Double (token); } catch (NullPointerException exc) { syntaxError ("Branch length expected"); } catch (NumberFormatException exc) { syntaxError ("Branch length expected"); } nextToken(); drawBranchLength (brlen, p, isTip); } } /** * Determine if the given token is a name. * * @param token Token. * * @return True if token is a name, false otherwise. */ private boolean isName (String token) { return token != null && ! "(".equals (token) && ! ")".equals (token) && ! ",".equals (token) && ! ":".equals (token) && ! ";".equals (token); } /** * Parse a child list. * * @return Point to which to attach line from parent. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private Point parseChildList() throws SyntaxException { LinkedList childList = new LinkedList(); double x = 0.0; double ymin = 0.0; double ymax = 0.0; for (;;) { Point p = parseChild(); childList.add (p); x = Math.min (x, p.x()); if (! ",".equals (token)) break; nextToken(); } x -= H; for (Point p : childList) { group.append (new Line().to(p).hto(x)); } ymin = childList.get(0).y(); ymax = childList.get(childList.size()-1).y(); group.append (new Line().to(x,ymin).to(x,ymax)); return new Point (x, (ymin+ymax)/2); } /** * Parse a child. * * @return Point to which to attach line from parent. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private Point parseChild() throws SyntaxException { if ("(".equals (token)) { return parseNode(); } else { return parseTip(); } } /** * Parse a tip. * * @return Point to which to attach line from parent. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private Point parseTip() throws SyntaxException { Point p = new Point (0, tipCount * V); ++ tipCount; parseNameLength (p, true); return p; } /** * Get the next token being parsed. */ private void nextToken() { if (matcher.find()) { token = treestring.substring (matcher.start(), matcher.end()); index = matcher.start(); } else { token = null; index = treestring.length(); } } /** * Report a syntax error. * * @param msg Error message. * * @exception SyntaxException * Thrown if a syntax error occurred. */ private void syntaxError (String msg) throws SyntaxException { throw new SyntaxException (treestring, index, msg); } // Helper classes. /** * Class TreeDrawing.SyntaxException is an exception thrown if there was a * syntax error in a tree string. * * @author Alan Kaminsky * @version 15-Jul-2008 */ public static class SyntaxException extends Exception { private String treestring; private int index; /** * Construct a new syntax exception. */ private SyntaxException (String treestring, int index, String msg) { super (msg); this.treestring = treestring; this.index = index; } /** * Print a syntax error message on the given print stream. * * @param out Print stream. */ public void printSyntaxError (PrintStream out) { synchronized (out) { out.println ("Syntax error: " + getMessage()); out.println (treestring); for (int i = 0; i < index; ++ i) out.print (' '); out.println ('^'); } } } }

Ads help maintain this website.