/* * Jalview - A Sequence Alignment Editor and Viewer * Copyright (C) 2007 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle * * 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 */ // NewickFile.java // Tree I/O // http://evolution.genetics.washington.edu/phylip/newick_doc.html // TODO: Implement Basic NHX tag parsing and preservation // TODO: http://evolution.genetics.wustl.edu/eddy/forester/NHX.html // TODO: Extended SequenceNodeI to hold parsed NHX strings package uk.ac.vamsas.objects.utils.trees; import java.io.*; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import uk.ac.vamsas.client.Vobject; import uk.ac.vamsas.client.VorbaId; import uk.ac.vamsas.objects.core.SequenceType; import uk.ac.vamsas.objects.core.Treenode; import uk.ac.vamsas.objects.core.Vref; /** * DOCUMENT ME! * * @author $author$ * @version $Revision: 1.12 $ */ public class NewickFile { public class BinaryNode { VorbaId element; String name; BinaryNode left; BinaryNode right; BinaryNode parent; /** DOCUMENT ME!! */ public int bootstrap; /** * Creates a new BinaryNode object. */ public BinaryNode() { left = right = parent = null; bootstrap = 0; } /** * Creates a new BinaryNode object. * * @param element * DOCUMENT ME! * @param parent * DOCUMENT ME! * @param name * DOCUMENT ME! */ public BinaryNode(VorbaId element, BinaryNode parent, String name) { this.element = element; this.parent = parent; this.name = name; left = right = null; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public VorbaId element() { return element; } /** * DOCUMENT ME! * * @param v * DOCUMENT ME! * * @return DOCUMENT ME! */ public VorbaId setElement(VorbaId v) { return element = v; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode left() { return left; } /** * DOCUMENT ME! * * @param n * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode setLeft(BinaryNode n) { return left = n; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode right() { return right; } /** * DOCUMENT ME! * * @param n * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode setRight(BinaryNode n) { return right = n; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode parent() { return parent; } /** * DOCUMENT ME! * * @param n * DOCUMENT ME! * * @return DOCUMENT ME! */ public BinaryNode setParent(BinaryNode n) { return parent = n; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public boolean isLeaf() { return (left == null) && (right == null); } /** * attaches FIRST and SECOND node arguments as the LEFT and RIGHT children * of this node (removing any old references) a null parameter DOES NOT mean * that the pointer to the corresponding child node is set to NULL - you * should use setChild(null), or detach() for this. * */ public void SetChildren(BinaryNode leftchild, BinaryNode rightchild) { if (leftchild != null) { this.setLeft(leftchild); leftchild.detach(); leftchild.setParent(this); } if (rightchild != null) { this.setRight(rightchild); rightchild.detach(); rightchild.setParent(this); } } /** * Detaches the node from the binary tree, along with all its child nodes. * * @return BinaryNode The detached node. */ public BinaryNode detach() { if (this.parent != null) { if (this.parent.left == this) { this.parent.left = null; } else { if (this.parent.right == this) { this.parent.right = null; } } } this.parent = null; return this; } /** * Traverses up through the tree until a node with a free leftchild is * discovered. * * @return BinaryNode */ public BinaryNode ascendLeft() { BinaryNode c = this; do { c = c.parent(); } while ((c != null) && (c.left() != null) && !c.left().isLeaf()); return c; } /** * Traverses up through the tree until a node with a free rightchild is * discovered. Jalview builds trees by descent on the left, so this may be * unused. * * @return BinaryNode */ public BinaryNode ascendRight() { BinaryNode c = this; do { c = c.parent(); } while ((c != null) && (c.right() != null) && !c.right().isLeaf()); return c; } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! */ public void setName(String name) { this.name = name; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public String getName() { return this.name; } /** * DOCUMENT ME! * * @param boot * DOCUMENT ME! */ public void setBootstrap(int boot) { this.bootstrap = boot; } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public int getBootstrap() { return bootstrap; } /** * * @return unquoted string of form VorbaId|v|node name string */ public String getNewickNodeName() { if (element!=null || name!=null) { return ((element!=null) ? element.getId()+"|v|" : "") + ((name == null) ? "" : name); } return ""; } public boolean parseNodeNameString(uk.ac.vamsas.client.ClientDocument binder) { if (element==null && name!=null && name.indexOf("|v|")>-1) { element = binder.getObject(null).getVorbaId(); // TODO: fix THIS ! name.substring(0, name.indexOf("|v"))); } return false; } } public class SequenceNode extends BinaryNode { /** DOCUMENT ME!! */ public float dist; /** DOCUMENT ME!! */ public boolean dummy = false; private boolean placeholder = false; /** * Creates a new SequenceNode object. */ public SequenceNode() { super(); } /** * Creates a new SequenceNode object. * * @param val * DOCUMENT ME! * @param parent * DOCUMENT ME! * @param dist * DOCUMENT ME! * @param name * DOCUMENT ME! */ public SequenceNode(VorbaId val, SequenceNode parent, float dist, String name) { super(val, parent, name); this.dist = dist; } public SequenceNode(Vobject val, SequenceNode parent, float dist, String name) { super(val.getVorbaId(), parent, name); this.dist = dist; } /** * Creates a new SequenceNode object. * * @param val * DOCUMENT ME! * @param parent * DOCUMENT ME! * @param name * DOCUMENT ME! * @param dist * DOCUMENT ME! * @param bootstrap * DOCUMENT ME! * @param dummy * DOCUMENT ME! */ public SequenceNode(Vobject val, SequenceNode parent, String name, float dist, int bootstrap, boolean dummy) { super(val.getVorbaId(), parent, name); this.dist = dist; this.bootstrap = bootstrap; this.dummy = dummy; } /** * @param dummy * true if node is created for the representation of polytomous * trees */ public boolean isDummy() { return dummy; } /* * @param placeholder is true if the sequence refered to in the element node * is not actually present in the associated alignment */ public boolean isPlaceholder() { return placeholder; } /** * DOCUMENT ME! * * @param newstate * DOCUMENT ME! * * @return DOCUMENT ME! */ public boolean setDummy(boolean newstate) { boolean oldstate = dummy; dummy = newstate; return oldstate; } /** * DOCUMENT ME! * * @param Placeholder * DOCUMENT ME! */ public void setPlaceholder(boolean Placeholder) { this.placeholder = Placeholder; } /** * ascends the tree but doesn't stop until a non-dummy node is discovered. * This will probably break if the tree is a mixture of BinaryNodes and * SequenceNodes. */ public SequenceNode AscendTree() { SequenceNode c = this; do { c = (SequenceNode) c.parent(); } while ((c != null) && c.dummy); return c; } } SequenceNode root; private boolean HasBootstrap = false; private boolean HasDistances = false; private boolean RootHasDistance = false; // File IO Flags boolean ReplaceUnderscores = false; boolean printRootInfo = false; private Pattern[] NodeSafeName = new Pattern[] { Pattern.compile("[\\[,:'()]"), // test for requiring quotes Pattern.compile("'"), // escaping quote characters Pattern.compile("/w") // unqoted whitespace transformation }; char QuoteChar = '\''; String newickFile = null; /** * Creates a new NewickFile object. * * @param inStr * DOCUMENT ME! * * @throws IOException * DOCUMENT ME! */ public NewickFile(String inStr) throws IOException { newickFile = inStr; parse(); } public NewickFile(File inFile) throws IOException { errormessage = "Problem's reading file "+inFile; dataIn = new java.io.BufferedReader(new InputStreamReader(new java.io.FileInputStream(inFile))); parse(); } /** * Creates a new NewickFile object. * * @param newtree * DOCUMENT ME! */ public NewickFile(SequenceNode newtree) { root = newtree; } /** * Creates a new NewickFile object. * * @param newtree * DOCUMENT ME! * @param bootstrap * DOCUMENT ME! */ public NewickFile(SequenceNode newtree, boolean bootstrap) { HasBootstrap = bootstrap; root = newtree; } /** * Creates a new NewickFile object. * * @param newtree * DOCUMENT ME! * @param bootstrap * DOCUMENT ME! * @param distances * DOCUMENT ME! */ public NewickFile(SequenceNode newtree, boolean bootstrap, boolean distances) { root = newtree; HasBootstrap = bootstrap; HasDistances = distances; } /** * Creates a new NewickFile object. * * @param newtree * DOCUMENT ME! * @param bootstrap * DOCUMENT ME! * @param distances * DOCUMENT ME! * @param rootdistance * DOCUMENT ME! */ public NewickFile(SequenceNode newtree, boolean bootstrap, boolean distances, boolean rootdistance) { root = newtree; HasBootstrap = bootstrap; HasDistances = distances; RootHasDistance = rootdistance; } /** * DOCUMENT ME! * * @param Error * Message prefix * @param Er * Message qualifier (ie the incorrect data) * @param r * range of characters either side to dump * @param p * character position * @param s * newick string being parsed * * @return composed error message */ private String ErrorStringrange(String Error, String Er, int r, int p, String s) { return ((Error == null) ? "" : Error) + Er + " at position " + p + " ( " + s.substring(((p - r) < 0) ? 0 : (p - r), ((p + r) > s.length()) ? s .length() : (p + r)) + " )\n"; } /** * * @return true if tree has bootstrap values */ public boolean HasBootstrap() { return HasBootstrap; } /** * * @return true if tree has distances on branches */ public boolean HasDistances() { return HasDistances; } /** * * @return true if root has a stem distance */ public boolean HasRootDistance() { return RootHasDistance; } /* * hacked out of jalview code */ boolean error; String errormessage; java.io.BufferedReader dataIn=null; public String nextLine() throws IOException { if (dataIn==null) { dataIn = new BufferedReader(new StringReader(newickFile)); error=false; } else throw new IOException("IMPLEMENTATION ERROR: NewickFile has not been initialised for reading a newick string."); if (!error) return dataIn.readLine(); throw new IOException("Invalid Source Stream:" + errormessage); } /** * call this to convert the newick string into a binary node linked tree * * @throws IOException * if the newick string cannot be parsed. */ public void parse() throws IOException { String nf; if (newickFile==null) { // fill nf with complete tree file StringBuffer file = new StringBuffer(); while ((nf = nextLine()) != null) { file.append(nf); } nf = file.toString(); } else { nf = newickFile; } root = new SequenceNode(); SequenceNode realroot = null; SequenceNode c = root; int d = -1; int cp = 0; // int flen = nf.length(); String Error = null; String nodename = null; float DefDistance = (float) 0.001; // @param Default distance for a node - // very very small int DefBootstrap = 0; // @param Default bootstrap for a node float distance = DefDistance; int bootstrap = DefBootstrap; boolean ascending = false; // flag indicating that we are leaving the // current node Pattern majorsyms = Pattern.compile( "[(\\['),;]"); Matcher mjsyms = majorsyms.matcher(nf); while (mjsyms.find(cp) && (Error == null)) { int fcp = mjsyms.start(); switch (nf.charAt(fcp)) { case '[': // Comment or structured/extended NH format info if (nf.indexOf(']',fcp)>-1) { // Skip the comment field cp = nf.indexOf(']',fcp); } else { Error = ErrorStringrange(Error, "Unterminated comment", 3, fcp, nf); } ; break; case '(': // ascending should not be set // New Internal node if (ascending) { Error = ErrorStringrange(Error, "Unexpected '('", 7, fcp, nf); continue; } ; d++; if (c.right() == null) { c.setRight(new SequenceNode(null, c, null, DefDistance, DefBootstrap, false)); c = (SequenceNode) c.right(); } else { if (c.left() != null) { // Dummy node for polytomy - keeps c.left free for new node SequenceNode tmpn = new SequenceNode(null, c, null, 0, 0, true); tmpn.SetChildren(c.left(), c.right()); c.setRight(tmpn); } c.setLeft(new SequenceNode(null, c, null, DefDistance, DefBootstrap, false)); c = (SequenceNode) c.left(); } if (realroot == null) { realroot = c; } nodename = null; distance = DefDistance; bootstrap = DefBootstrap; cp = fcp + 1; break; // Deal with quoted fields case '\'': Matcher qnodename = Pattern.compile("([^']|'')+'").matcher(nf); if (qnodename.find(fcp)) { nodename = new String(qnodename.group(1)); cp = qnodename.end(0); } else { Error = ErrorStringrange(Error, "Unterminated quotes for nodename", 7, fcp, nf); } break; case ';': if (d != -1) { Error = ErrorStringrange(Error, "Wayward semicolon (depth=" + d + ")", 7, fcp, nf); } // cp advanced at the end of default default: // Parse simpler field strings String fstring = nf.substring(cp, fcp); Matcher uqnodename = Pattern.compile("\\b([^' :;\\](),]+)").matcher(fstring); if (uqnodename.matches() && ((uqnodename.start(1) == 0) || (fstring.charAt(uqnodename .start(1) - 1) != ':'))) // JBPNote HACK! { if (nodename == null) { if (ReplaceUnderscores) { nodename = uqnodename.group(1).replace('_', ' '); } else { nodename = uqnodename.group(1); } } else { Error = ErrorStringrange(Error, "File has broken algorithm - overwritten nodename", 10, fcp, nf); } } Matcher nbootstrap = Pattern.compile("\\S+([0-9+]+)\\S*:").matcher(fstring); if (nbootstrap.matches() && (nbootstrap.start(1) > uqnodename.end(1))) { try { bootstrap = (new Integer(nbootstrap.group(1))).intValue(); HasBootstrap = true; } catch (Exception e) { Error = ErrorStringrange(Error, "Can't parse bootstrap value", 4, cp + nbootstrap.start(0), nf); } } Matcher ndist = Pattern.compile( ":([-0-9Ee.+]+)").matcher(fstring); boolean nodehasdistance = false; if (ndist.matches()) { try { distance = (new Float(ndist.group(1))).floatValue(); HasDistances = true; nodehasdistance = true; } catch (Exception e) { Error = ErrorStringrange(Error, "Can't parse node distance value", 7, cp + ndist.start(0), nf); } } if (ascending) { // Write node info here c.setName(nodename); // Trees without distances still need a render distance c.dist = (HasDistances) ? distance : DefDistance; // be consistent for internal bootstrap defaults too c.setBootstrap((HasBootstrap) ? bootstrap : DefBootstrap); if (c == realroot) { RootHasDistance = nodehasdistance; // JBPNote This is really // UGLY!!! Ensure root node gets // its given distance } } else { // Find a place to put the leaf SequenceNode newnode = new SequenceNode(null, c, nodename, (HasDistances) ? distance : DefDistance, (HasBootstrap) ? bootstrap : DefBootstrap, false); if (c.right() == null) { c.setRight(newnode); } else { if (c.left() == null) { c.setLeft(newnode); } else { // Insert a dummy node for polytomy // dummy nodes have distances SequenceNode newdummy = new SequenceNode(null, c, null, (HasDistances ? 0 : DefDistance), 0, true); newdummy.SetChildren(c.left(), newnode); c.setLeft(newdummy); } } } if (ascending) { // move back up the tree from preceding closure c = c.AscendTree(); if ((d > -1) && (c == null)) { Error = ErrorStringrange( Error, "File broke algorithm: Lost place in tree (is there an extra ')' ?)", 7, fcp, nf); } } if (nf.charAt(fcp) == ')') { d--; ascending = true; } else { if (nf.charAt(fcp) == ',') { if (ascending) { ascending = false; } else { // Just advance focus, if we need to if ((c.left() != null) && (!c.left().isLeaf())) { c = (SequenceNode) c.left(); } } } // else : We do nothing if ';' is encountered. } // Reset new node properties to obvious fakes nodename = null; distance = DefDistance; bootstrap = DefBootstrap; cp = fcp + 1; } } if (Error != null) { throw (new IOException("NewickFile: " + Error + "\n")); } root = (SequenceNode) root.right().detach(); // remove the imaginary root. if (!RootHasDistance) { root.dist = (HasDistances) ? 0 : DefDistance; } } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public SequenceNode getTree() { return root; } public uk.ac.vamsas.objects.core.Treenode[] matchTreeNodeNames(String[] names, Vobject[] boundObjects) { // todo! // also - need to reconstruct a names object id mapping (or BInaryNode) mapping for the parsed tree file return null; } /** * Generate a newick format tree according to internal flags for bootstraps, * distances and root distances. * * @return new hampshire tree in a single line */ public String print() { synchronized (this) { StringBuffer tf = new StringBuffer(); print(tf, root); return (tf.append(";").toString()); } } /** * * * Generate a newick format tree according to internal flags for distances and * root distances and user specificied writing of bootstraps. * * @param withbootstraps * controls if bootstrap values are explicitly written. * * @return new hampshire tree in a single line */ public String print(boolean withbootstraps) { synchronized (this) { boolean boots = this.HasBootstrap; this.HasBootstrap = withbootstraps; String rv = print(); this.HasBootstrap = boots; return rv; } } /** * * Generate newick format tree according to internal flags for writing root * node distances. * * @param withbootstraps * explicitly write bootstrap values * @param withdists * explicitly write distances * * @return new hampshire tree in a single line */ public String print(boolean withbootstraps, boolean withdists) { synchronized (this) { boolean dists = this.HasDistances; this.HasDistances = withdists; String rv = print(withbootstraps); this.HasDistances = dists; return rv; } } /** * Generate newick format tree according to user specified flags * * @param withbootstraps * explicitly write bootstrap values * @param withdists * explicitly write distances * @param printRootInfo * explicitly write root distance * * @return new hampshire tree in a single line */ public String print(boolean withbootstraps, boolean withdists, boolean printRootInfo) { synchronized (this) { boolean rootinfo = printRootInfo; this.printRootInfo = printRootInfo; String rv = print(withbootstraps, withdists); this.printRootInfo = rootinfo; return rv; } } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ char getQuoteChar() { return QuoteChar; } /** * DOCUMENT ME! * * @param c * DOCUMENT ME! * * @return DOCUMENT ME! */ char setQuoteChar(char c) { char old = QuoteChar; QuoteChar = c; return old; } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * * @return DOCUMENT ME! */ private String nodeName(String name) { if (NodeSafeName[0].matcher(name).find()) { return QuoteChar + NodeSafeName[1].matcher(name).replaceAll("''") + QuoteChar; // quite } else { return NodeSafeName[2].matcher(name).replaceAll("_"); // whitespace } } /** * DOCUMENT ME! * * @param c * DOCUMENT ME! * * @return DOCUMENT ME! */ private String printNodeField(SequenceNode c) { return c.getNewickNodeName() + ((HasBootstrap) ? ((c.getBootstrap() > -1) ? (" " + c.getBootstrap()) : "") : "") + ((HasDistances) ? (":" + c.dist) : ""); } /** * DOCUMENT ME! * * @param root * DOCUMENT ME! * * @return DOCUMENT ME! */ private String printRootField(SequenceNode root) { return (printRootInfo) ? (((root.getName() == null) ? "" : nodeName(root .getName())) + ((HasBootstrap) ? ((root.getBootstrap() > -1) ? (" " + root .getBootstrap()) : "") : "") + ((RootHasDistance) ? (":" + root.dist) : "")) : ""; } // Non recursive call deals with root node properties public void print(StringBuffer tf, SequenceNode root) { if (root != null) { if (root.isLeaf() && printRootInfo) { tf.append(printRootField(root)); } else { if (root.isDummy()) { _print(tf, (SequenceNode) root.right()); _print(tf, (SequenceNode) root.left()); } else { tf.append("("); _print(tf, (SequenceNode) root.right()); if (root.left() != null) { tf.append(","); } _print(tf, (SequenceNode) root.left()); tf.append(")" + printRootField(root)); } } } } // Recursive call for non-root nodes public void _print(StringBuffer tf, SequenceNode c) { if (c != null) { if (c.isLeaf()) { tf.append(printNodeField(c)); } else { if (c.isDummy()) { _print(tf, (SequenceNode) c.left()); if (c.left() != null) { tf.append(","); } _print(tf, (SequenceNode) c.right()); } else { tf.append("("); _print(tf, (SequenceNode) c.right()); if (c.left() != null) { tf.append(","); } _print(tf, (SequenceNode) c.left()); tf.append(")" + printNodeField(c)); } } } } // Test public static void main(String[] args) { try { if (args == null || args.length != 1) { System.err .println("Takes one argument - file name of a newick tree file."); System.exit(0); } File fn = new File(args[0]); StringBuffer newickfile = new StringBuffer(); BufferedReader treefile = new BufferedReader(new FileReader(fn)); String l; while ((l = treefile.readLine()) != null) { newickfile.append(l); } treefile.close(); System.out.println("Read file :\n"); NewickFile trf = new NewickFile(fn); // trf.parse(); System.out.println("Original file :\n"); System.out.println(Pattern.compile("\n+").matcher(newickfile.toString()).replaceAll("") + "\n"); System.out.println("Parsed file.\n"); System.out.println("Default output type for original input.\n"); System.out.println(trf.print()); System.out.println("Without bootstraps.\n"); System.out.println(trf.print(false)); System.out.println("Without distances.\n"); System.out.println(trf.print(true, false)); System.out.println("Without bootstraps but with distanecs.\n"); System.out.println(trf.print(false, true)); System.out.println("Without bootstraps or distanecs.\n"); System.out.println(trf.print(false, false)); System.out.println("With bootstraps and with distances.\n"); System.out.println(trf.print(true, true)); } catch (java.io.IOException e) { System.err.println("Exception\n" + e); e.printStackTrace(); } } /** * Search for leaf nodes. * * @param node root node to search from * @param leaves Vector of leaves to add leaf node objects too. * * @return Vector of leaf nodes on binary tree */ public Vector findLeaves(SequenceNode node, Vector leaves) { if (node == null) { return leaves; } if ( (node.left() == null) && (node.right() == null)) // Interior node detection { leaves.addElement(node); return leaves; } else { /* TODO: Identify internal nodes... if (node.isSequenceLabel()) { leaves.addElement(node); }*/ findLeaves( (SequenceNode) node.left(), leaves); findLeaves( (SequenceNode) node.right(), leaves); } return leaves; } /** * make tree node vector from a newick tree structure with associated vamsas objects * @param ntree * @return */ public Treenode[] makeTreeNodes() { // NJTree ntree) { Vector leaves = new Vector(); findLeaves(root, leaves); Vector tnv = new Vector(); Enumeration l = leaves.elements(); int i=0; Hashtable nodespecs = new Hashtable(); while (l.hasMoreElements()) { BinaryNode tnode = (BinaryNode) l.nextElement(); if (tnode instanceof SequenceNode) { if (!((SequenceNode) tnode).isPlaceholder()) { Object assocseq = ((SequenceNode) tnode).element(); if (assocseq instanceof Vobject) { Vobject vobj = (Vobject) assocseq; if (vobj!=null) { Treenode node = new Treenode(); node.setNodespec(makeNodeSpec(nodespecs, tnode)); node.setName(tnode.getName()); Vref vr = new Vref(); vr.addRefs(vobj); node.addVref(vr); tnv.addElement(node); } else { System.err.println("WARNING: Unassociated treeNode "+tnode.element().toString()+" " +((tnode.getName()!=null) ? " label "+tnode.getName() : "")); } } } } } if (tnv.size()>0) { Treenode[] tn = new Treenode[tnv.size()]; tnv.copyInto(tn); return tn; } return new Treenode[] {}; } private String makeNodeSpec(Hashtable nodespecs, BinaryNode tnode) { String nname = new String(tnode.getName()); Integer nindx = (Integer) nodespecs.get(nname); if (nindx==null) { nindx = new Integer(1); } nname = nindx.toString()+" "+nname; return nname; } /** * call to match up Treenode specs to NJTree parsed from document object. * * @param nodespec * @param leaves * as returned from NJTree.findLeaves( .., ..) .. * @return */ private BinaryNode findNodeSpec(String nodespec, Vector leaves) { int occurence=-1; String nspec = nodespec.substring(nodespec.indexOf(' ')+1); String oval = nodespec.substring(0, nodespec.indexOf(' ')); try { occurence = new Integer(oval).intValue(); } catch (Exception e) { System.err.println("Invalid nodespec '"+nodespec+"'"); return null; } BinaryNode bn = null; int nocc = 0; Enumeration en = leaves.elements(); while (en.hasMoreElements() && nocc