d6538113ce71a3299fdea3f666181c317cb9345d
[jalview.git] / src / jalview / io / NewickFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3  * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  *  
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 // NewickFile.java
19 // Tree I/O
20 // http://evolution.genetics.washington.edu/phylip/newick_doc.html
21 // TODO: Implement Basic NHX tag parsing and preservation
22 // TODO: http://evolution.genetics.wustl.edu/eddy/forester/NHX.html
23 // TODO: Extended SequenceNodeI to hold parsed NHX strings
24 package jalview.io;
25
26 import java.io.*;
27 import java.util.StringTokenizer;
28
29 import jalview.datamodel.*;
30
31 /**
32  * Parse a new hanpshire style tree Caveats: NHX files are NOT supported and the
33  * tree distances and topology are unreliable when they are parsed. TODO: on
34  * this: NHX codes are appended in comments beginning with &&NHX. The codes are
35  * given below (from http://www.phylosoft.org/forester/NHX.html): Element Type
36  * Description Corresponding phyloXML element (parent element in parentheses) no
37  * tag string name of this node/clade (MUST BE FIRST, IF ASSIGNED)
38  * <name>(<clade>) : decimal branch length to parent node (MUST BE SECOND, IF
39  * ASSIGNED) <branch_length>(<clade>) :GN= string gene name <name>(<sequence>)
40  * :AC= string sequence accession <accession>(<sequence>) :ND= string node
41  * identifier - if this is being used, it has to be unique within each phylogeny
42  * <node_id>(<clade>) :B= decimal confidence value for parent branch
43  * <confidence>(<clade>) :D= 'T', 'F', or '?' 'T' if this node represents a
44  * duplication event - 'F' if this node represents a speciation event, '?' if
45  * this node represents an unknown event (D= tag should be replaced by Ev= tag)
46  * n/a :Ev=duplications>speciations>gene losses>event type>duplication type int
47  * int int string string event (replaces the =D tag), number of duplication,
48  * speciation, and gene loss events, type of event (transfer, fusion, root,
49  * unknown, other, speciation_duplication_loss, unassigned) <events>(<clade>)
50  * :E= string EC number at this node <annotation>(<sequence>) :Fu= string
51  * function at this node <annotation>(<sequence>)
52  * :DS=protein-length>from>to>support>name>from>... int int int double string
53  * int ... domain structure at this node <domain_architecture>(<sequence>) :S=
54  * string species name of the species/phylum at this node <taxonomy>(<clade>)
55  * :T= integer taxonomy ID of the species/phylum at this node <id>(<taxonomy>)
56  * :W= integer width of parent branch <width>(<clade>) :C=rrr.ggg.bbb
57  * integer.integer.integer color of parent branch <color>(<clade>) :Co= 'Y' or
58  * 'N' collapse this node when drawing the tree (default is not to collapse) n/a
59  * :XB= string custom data associated with a branch <property>(<clade>) :XN=
60  * string custom data associated with a node <property>(<clade>) :O= integer
61  * orthologous to this external node n/a :SN= integer subtree neighbors n/a :SO=
62  * integer super orthologous (no duplications on paths) to this external node
63  * n/a
64  * 
65  * @author Jim Procter
66  * @version $Revision$
67  */
68 public class NewickFile extends FileParse
69 {
70   SequenceNode root;
71
72   private boolean HasBootstrap = false;
73
74   private boolean HasDistances = false;
75
76   private boolean RootHasDistance = false;
77
78   // File IO Flags
79   boolean ReplaceUnderscores = false;
80
81   boolean printRootInfo = true;
82
83   private com.stevesoft.pat.Regex[] NodeSafeName = new com.stevesoft.pat.Regex[]
84   { new com.stevesoft.pat.Regex().perlCode("m/[\\[,:'()]/"), // test for
85       // requiring
86       // quotes
87       new com.stevesoft.pat.Regex().perlCode("s/'/''/"), // escaping quote
88       // characters
89       new com.stevesoft.pat.Regex().perlCode("s/\\/w/_/") // unqoted whitespace
90   // transformation
91   };
92
93   char QuoteChar = '\'';
94
95   /**
96    * Creates a new NewickFile object.
97    * 
98    * @param inStr
99    *          DOCUMENT ME!
100    * 
101    * @throws IOException
102    *           DOCUMENT ME!
103    */
104   public NewickFile(String inStr) throws IOException
105   {
106     super(inStr, "Paste");
107   }
108
109   /**
110    * Creates a new NewickFile object.
111    * 
112    * @param inFile
113    *          DOCUMENT ME!
114    * @param type
115    *          DOCUMENT ME!
116    * 
117    * @throws IOException
118    *           DOCUMENT ME!
119    */
120   public NewickFile(String inFile, String type) throws IOException
121   {
122     super(inFile, type);
123   }
124
125   public NewickFile(FileParse source) throws IOException
126   {
127     super(source);
128   }
129
130   /**
131    * Creates a new NewickFile object.
132    * 
133    * @param newtree
134    *          DOCUMENT ME!
135    */
136   public NewickFile(SequenceNode newtree)
137   {
138     root = newtree;
139   }
140
141   /**
142    * Creates a new NewickFile object.
143    * 
144    * @param newtree
145    *          DOCUMENT ME!
146    * @param bootstrap
147    *          DOCUMENT ME!
148    */
149   public NewickFile(SequenceNode newtree, boolean bootstrap)
150   {
151     HasBootstrap = bootstrap;
152     root = newtree;
153   }
154
155   /**
156    * Creates a new NewickFile object.
157    * 
158    * @param newtree
159    *          DOCUMENT ME!
160    * @param bootstrap
161    *          DOCUMENT ME!
162    * @param distances
163    *          DOCUMENT ME!
164    */
165   public NewickFile(SequenceNode newtree, boolean bootstrap,
166           boolean distances)
167   {
168     root = newtree;
169     HasBootstrap = bootstrap;
170     HasDistances = distances;
171   }
172
173   /**
174    * Creates a new NewickFile object.
175    * 
176    * @param newtree
177    *          DOCUMENT ME!
178    * @param bootstrap
179    *          DOCUMENT ME!
180    * @param distances
181    *          DOCUMENT ME!
182    * @param rootdistance
183    *          DOCUMENT ME!
184    */
185   public NewickFile(SequenceNode newtree, boolean bootstrap,
186           boolean distances, boolean rootdistance)
187   {
188     root = newtree;
189     HasBootstrap = bootstrap;
190     HasDistances = distances;
191     RootHasDistance = rootdistance;
192   }
193
194   /**
195    * DOCUMENT ME!
196    * 
197    * @param Error
198    *          DOCUMENT ME!
199    * @param Er
200    *          DOCUMENT ME!
201    * @param r
202    *          DOCUMENT ME!
203    * @param p
204    *          DOCUMENT ME!
205    * @param s
206    *          DOCUMENT ME!
207    * 
208    * @return DOCUMENT ME!
209    */
210   private String ErrorStringrange(String Error, String Er, int r, int p,
211           String s)
212   {
213     return ((Error == null) ? "" : Error)
214             + Er
215             + " at position "
216             + p
217             + " ( "
218             + s.substring(((p - r) < 0) ? 0 : (p - r),
219                     ((p + r) > s.length()) ? s.length() : (p + r)) + " )\n";
220   }
221
222   // @tree annotations
223   // These are set automatically by the reader
224   public boolean HasBootstrap()
225   {
226     return HasBootstrap;
227   }
228
229   /**
230    * DOCUMENT ME!
231    * 
232    * @return DOCUMENT ME!
233    */
234   public boolean HasDistances()
235   {
236     return HasDistances;
237   }
238
239   public boolean HasRootDistance()
240   {
241     return RootHasDistance;
242   }
243
244   /**
245    * parse the filesource as a newick file (new hampshire and/or extended)
246    * 
247    * @throws IOException
248    *           with a line number and character position for badly formatted NH
249    *           strings
250    */
251   public void parse() throws IOException
252   {
253     String nf;
254
255     { // fill nf with complete tree file
256
257       StringBuffer file = new StringBuffer();
258
259       while ((nf = nextLine()) != null)
260       {
261         file.append(nf);
262       }
263
264       nf = file.toString();
265     }
266
267     root = new SequenceNode();
268
269     SequenceNode realroot = null;
270     SequenceNode c = root;
271
272     int d = -1;
273     int cp = 0;
274     // int flen = nf.length();
275
276     String Error = null;
277     String nodename = null;
278     String commentString2 = null; // comments after simple node props
279
280     float DefDistance = (float) 0.001; // @param Default distance for a node -
281     // very very small
282     int DefBootstrap = -1; // @param Default bootstrap for a node
283
284     float distance = DefDistance;
285     int bootstrap = DefBootstrap;
286
287     boolean ascending = false; // flag indicating that we are leaving the
288     // current node
289
290     com.stevesoft.pat.Regex majorsyms = new com.stevesoft.pat.Regex(
291             "[(\\['),;]");
292
293     int nextcp = 0;
294     int ncp = cp;
295     boolean parsednodename=false;
296     while (majorsyms.searchFrom(nf, cp) && (Error == null))
297     {
298       int fcp = majorsyms.matchedFrom();
299       char schar;
300       switch (schar = nf.charAt(fcp))
301       {
302       case '(':
303
304         // ascending should not be set
305         // New Internal node
306         if (ascending)
307         {
308           Error = ErrorStringrange(Error, "Unexpected '('", 7, fcp, nf);
309
310           continue;
311         }
312
313         ;
314         d++;
315
316         if (c.right() == null)
317         {
318           c.setRight(new SequenceNode(null, c, null, DefDistance,
319                   DefBootstrap, false));
320           c = (SequenceNode) c.right();
321         }
322         else
323         {
324           if (c.left() != null)
325           {
326             // Dummy node for polytomy - keeps c.left free for new node
327             SequenceNode tmpn = new SequenceNode(null, c, null, 0, 0, true);
328             tmpn.SetChildren(c.left(), c.right());
329             c.setRight(tmpn);
330           }
331
332           c.setLeft(new SequenceNode(null, c, null, DefDistance,
333                   DefBootstrap, false));
334           c = (SequenceNode) c.left();
335         }
336
337         if (realroot == null)
338         {
339           realroot = c;
340         }
341
342         nodename = null;
343         distance = DefDistance;
344         bootstrap = DefBootstrap;
345         cp = fcp + 1;
346
347         break;
348
349       // Deal with quoted fields
350       case '\'':
351
352         com.stevesoft.pat.Regex qnodename = new com.stevesoft.pat.Regex(
353                 "'([^']|'')+'");
354
355         if (qnodename.searchFrom(nf, fcp))
356         {
357           int nl = qnodename.stringMatched().length();
358           nodename = new String(qnodename.stringMatched().substring(1,
359                   nl - 1));
360           // jump to after end of quoted nodename
361           nextcp = fcp + nl + 1;
362           parsednodename=true;
363         }
364         else
365         {
366           Error = ErrorStringrange(Error,
367                   "Unterminated quotes for nodename", 7, fcp, nf);
368         }
369
370         break;
371
372       default:
373         if (schar == ';')
374         {
375           if (d != -1)
376           {
377             Error = ErrorStringrange(Error, "Wayward semicolon (depth=" + d
378                     + ")", 7, fcp, nf);
379           }
380           // cp advanced at the end of default
381         }
382         if (schar == '[')
383         {
384           // node string contains Comment or structured/extended NH format info
385           /*
386            * if ((fcp-cp>1 && nf.substring(cp,fcp).trim().length()>1)) { // will
387            * process in remains System.err.println("skipped text:
388            * '"+nf.substring(cp,fcp)+"'"); }
389            */
390           // verify termination.
391           com.stevesoft.pat.Regex comment = new com.stevesoft.pat.Regex("]");
392           if (comment.searchFrom(nf, fcp))
393           {
394             // Skip the comment field
395             nextcp = comment.matchedFrom() + 1;
396             warningMessage = "Tree file contained comments which may confuse input algorithm.";
397             break;
398
399             // cp advanced at the end of default to nextcp, ncp is unchanged so
400             // any node info can be read.
401           }
402           else
403           {
404             Error = ErrorStringrange(Error, "Unterminated comment", 3, fcp,
405                     nf);
406           }
407
408           ;
409         }
410         // Parse simpler field strings
411         String fstring = nf.substring(ncp, fcp);
412         // remove any comments before we parse the node info
413         // TODO: test newick file with quoted square brackets in node name (is
414         // this allowed?)
415         while (fstring.indexOf(']') > -1)
416         {
417           int cstart = fstring.indexOf('[');
418           int cend = fstring.indexOf(']');
419           commentString2 = fstring.substring(cstart + 1, cend);
420           fstring = fstring.substring(0, cstart)
421                   + fstring.substring(cend + 1);
422
423         }
424         com.stevesoft.pat.Regex uqnodename = new com.stevesoft.pat.Regex(
425                 "\\b([^' :;\\](),]+)");
426         com.stevesoft.pat.Regex nbootstrap = new com.stevesoft.pat.Regex(
427                 "\\s*([0-9+]+)\\s*:");
428         com.stevesoft.pat.Regex ndist = new com.stevesoft.pat.Regex(
429                 ":([-0-9Ee.+]+)");
430
431         if (!parsednodename && uqnodename.search(fstring)
432                 && ((uqnodename.matchedFrom(1) == 0) || (fstring
433                         .charAt(uqnodename.matchedFrom(1) - 1) != ':'))) // JBPNote
434         // HACK!
435         {
436           if (nodename == null)
437           {
438             if (ReplaceUnderscores)
439             {
440               nodename = uqnodename.stringMatched(1).replace('_', ' ');
441             }
442             else
443             {
444               nodename = uqnodename.stringMatched(1);
445             }
446           }
447           else
448           {
449             Error = ErrorStringrange(Error,
450                     "File has broken algorithm - overwritten nodename", 10,
451                     fcp, nf);
452           }
453         }
454         // get comment bootstraps
455
456         if (nbootstrap.search(fstring))
457         {
458           if (nbootstrap.stringMatched(1).equals(
459                   uqnodename.stringMatched(1)))
460           {
461             nodename = null; // no nodename here.
462           }
463           if (nodename == null
464                   || nodename.length() == 0
465                   || nbootstrap.matchedFrom(1) > (uqnodename.matchedFrom(1) + uqnodename
466                           .stringMatched().length()))
467           {
468             try
469             {
470               bootstrap = (new Integer(nbootstrap.stringMatched(1)))
471                       .intValue();
472               HasBootstrap = true;
473             } catch (Exception e)
474             {
475               Error = ErrorStringrange(Error,
476                       "Can't parse bootstrap value", 4,
477                       ncp + nbootstrap.matchedFrom(), nf);
478             }
479           }
480         }
481
482         boolean nodehasdistance = false;
483
484         if (ndist.search(fstring))
485         {
486           try
487           {
488             distance = (new Float(ndist.stringMatched(1))).floatValue();
489             HasDistances = true;
490             nodehasdistance = true;
491           } catch (Exception e)
492           {
493             Error = ErrorStringrange(Error,
494                     "Can't parse node distance value", 7,
495                     ncp + ndist.matchedFrom(), nf);
496           }
497         }
498
499         if (ascending)
500         {
501           // Write node info here
502           c.setName(nodename);
503           // Trees without distances still need a render distance
504           c.dist = (HasDistances) ? distance : DefDistance;
505           // be consistent for internal bootstrap defaults too
506           c.setBootstrap((HasBootstrap) ? bootstrap : DefBootstrap);
507           if (c == realroot)
508           {
509             RootHasDistance = nodehasdistance; // JBPNote This is really
510             // UGLY!!! Ensure root node gets
511             // its given distance
512           }
513           parseNHXNodeProps(c, commentString2);
514           commentString2 = null;
515         }
516         else
517         {
518           // Find a place to put the leaf
519           SequenceNode newnode = new SequenceNode(null, c, nodename,
520                   (HasDistances) ? distance : DefDistance,
521                   (HasBootstrap) ? bootstrap : DefBootstrap, false);
522           parseNHXNodeProps(c, commentString2);
523           commentString2 = null;
524
525           if (c.right() == null)
526           {
527             c.setRight(newnode);
528           }
529           else
530           {
531             if (c.left() == null)
532             {
533               c.setLeft(newnode);
534             }
535             else
536             {
537               // Insert a dummy node for polytomy
538               // dummy nodes have distances
539               SequenceNode newdummy = new SequenceNode(null, c, null,
540                       (HasDistances ? 0 : DefDistance), 0, true);
541               newdummy.SetChildren(c.left(), newnode);
542               c.setLeft(newdummy);
543             }
544           }
545         }
546
547         if (ascending)
548         {
549           // move back up the tree from preceding closure
550           c = c.AscendTree();
551
552           if ((d > -1) && (c == null))
553           {
554             Error = ErrorStringrange(
555                     Error,
556                     "File broke algorithm: Lost place in tree (is there an extra ')' ?)",
557                     7, fcp, nf);
558           }
559         }
560
561         if (nf.charAt(fcp) == ')')
562         {
563           d--;
564           ascending = true;
565         }
566         else
567         {
568           if (nf.charAt(fcp) == ',')
569           {
570             if (ascending)
571             {
572               ascending = false;
573             }
574             else
575             {
576               // Just advance focus, if we need to
577               if ((c.left() != null) && (!c.left().isLeaf()))
578               {
579                 c = (SequenceNode) c.left();
580               }
581             }
582           }
583         }
584
585         // Reset new node properties to obvious fakes
586         nodename = null;
587         distance = DefDistance;
588         bootstrap = DefBootstrap;
589         commentString2 = null;
590         parsednodename=false;
591       }
592       if (nextcp == 0)
593       {
594         ncp = cp = fcp + 1;
595       }
596       else
597       {
598         cp = nextcp;
599         nextcp = 0;
600       }
601     }
602
603     if (Error != null)
604     {
605       throw (new IOException("NewickFile: " + Error + "\n"));
606     }
607     if (root == null)
608     {
609       throw (new IOException("NewickFile: No Tree read in\n"));
610     }
611     // THe next line is failing for topali trees - not sure why yet. if
612     // (root.right()!=null && root.isDummy())
613     root = (SequenceNode) root.right().detach(); // remove the imaginary root.
614
615     if (!RootHasDistance)
616     {
617       root.dist = (HasDistances) ? 0 : DefDistance;
618     }
619   }
620
621   /**
622    * parse NHX codes in comment strings and update NewickFile state flags for
623    * distances and bootstraps, and add any additional properties onto the node.
624    * 
625    * @param c
626    * @param commentString
627    * @param commentString2
628    */
629   private void parseNHXNodeProps(SequenceNode c, String commentString)
630   {
631     // TODO: store raw comment on the sequenceNode so it can be recovered when
632     // tree is output
633     if (commentString != null && commentString.startsWith("&&NHX"))
634     {
635       StringTokenizer st = new StringTokenizer(commentString.substring(5),
636               ":");
637       while (st.hasMoreTokens())
638       {
639         String tok = st.nextToken();
640         int colpos = tok.indexOf("=");
641
642         if (colpos > -1)
643         {
644           String code = tok.substring(0, colpos);
645           String value = tok.substring(colpos + 1);
646           try
647           {
648             // parse out code/value pairs
649             if (code.toLowerCase().equals("b"))
650             {
651               int v = -1;
652               Float iv = new Float(value);
653               v = iv.intValue(); // jalview only does integer bootstraps
654               // currently
655               c.setBootstrap(v);
656               HasBootstrap = true;
657             }
658             // more codes here.
659           } catch (Exception e)
660           {
661             System.err.println("Couldn't parse code '" + code + "' = '"
662                     + value + "'");
663             e.printStackTrace(System.err);
664           }
665         }
666       }
667     }
668
669   }
670
671   /**
672    * DOCUMENT ME!
673    * 
674    * @return DOCUMENT ME!
675    */
676   public SequenceNode getTree()
677   {
678     return root;
679   }
680
681   /**
682    * Generate a newick format tree according to internal flags for bootstraps,
683    * distances and root distances.
684    * 
685    * @return new hampshire tree in a single line
686    */
687   public String print()
688   {
689     synchronized (this)
690     {
691       StringBuffer tf = new StringBuffer();
692       print(tf, root);
693
694       return (tf.append(";").toString());
695     }
696   }
697
698   /**
699    * 
700    * 
701    * Generate a newick format tree according to internal flags for distances and
702    * root distances and user specificied writing of bootstraps.
703    * 
704    * @param withbootstraps
705    *          controls if bootstrap values are explicitly written.
706    * 
707    * @return new hampshire tree in a single line
708    */
709   public String print(boolean withbootstraps)
710   {
711     synchronized (this)
712     {
713       boolean boots = this.HasBootstrap;
714       this.HasBootstrap = withbootstraps;
715
716       String rv = print();
717       this.HasBootstrap = boots;
718
719       return rv;
720     }
721   }
722
723   /**
724    * 
725    * Generate newick format tree according to internal flags for writing root
726    * node distances.
727    * 
728    * @param withbootstraps
729    *          explicitly write bootstrap values
730    * @param withdists
731    *          explicitly write distances
732    * 
733    * @return new hampshire tree in a single line
734    */
735   public String print(boolean withbootstraps, boolean withdists)
736   {
737     synchronized (this)
738     {
739       boolean dists = this.HasDistances;
740       this.HasDistances = withdists;
741
742       String rv = print(withbootstraps);
743       this.HasDistances = dists;
744
745       return rv;
746     }
747   }
748
749   /**
750    * Generate newick format tree according to user specified flags
751    * 
752    * @param withbootstraps
753    *          explicitly write bootstrap values
754    * @param withdists
755    *          explicitly write distances
756    * @param printRootInfo
757    *          explicitly write root distance
758    * 
759    * @return new hampshire tree in a single line
760    */
761   public String print(boolean withbootstraps, boolean withdists,
762           boolean printRootInfo)
763   {
764     synchronized (this)
765     {
766       boolean rootinfo = printRootInfo;
767       this.printRootInfo = printRootInfo;
768
769       String rv = print(withbootstraps, withdists);
770       this.printRootInfo = rootinfo;
771
772       return rv;
773     }
774   }
775
776   /**
777    * DOCUMENT ME!
778    * 
779    * @return DOCUMENT ME!
780    */
781   char getQuoteChar()
782   {
783     return QuoteChar;
784   }
785
786   /**
787    * DOCUMENT ME!
788    * 
789    * @param c
790    *          DOCUMENT ME!
791    * 
792    * @return DOCUMENT ME!
793    */
794   char setQuoteChar(char c)
795   {
796     char old = QuoteChar;
797     QuoteChar = c;
798
799     return old;
800   }
801
802   /**
803    * DOCUMENT ME!
804    * 
805    * @param name
806    *          DOCUMENT ME!
807    * 
808    * @return DOCUMENT ME!
809    */
810   private String nodeName(String name)
811   {
812     if (NodeSafeName[0].search(name))
813     {
814       return QuoteChar + NodeSafeName[1].replaceAll(name) + QuoteChar;
815     }
816     else
817     {
818       return NodeSafeName[2].replaceAll(name);
819     }
820   }
821
822   /**
823    * DOCUMENT ME!
824    * 
825    * @param c
826    *          DOCUMENT ME!
827    * 
828    * @return DOCUMENT ME!
829    */
830   private String printNodeField(SequenceNode c)
831   {
832     return ((c.getName() == null) ? "" : nodeName(c.getName()))
833             + ((HasBootstrap) ? ((c.getBootstrap() > -1) ? ((c.getName() != null ? " "
834                     : "") + c.getBootstrap())
835                     : "")
836                     : "") + ((HasDistances) ? (":" + c.dist) : "");
837   }
838
839   /**
840    * DOCUMENT ME!
841    * 
842    * @param root
843    *          DOCUMENT ME!
844    * 
845    * @return DOCUMENT ME!
846    */
847   private String printRootField(SequenceNode root)
848   {
849     return (printRootInfo) ? (((root.getName() == null) ? ""
850             : nodeName(root.getName()))
851             + ((HasBootstrap) ? ((root.getBootstrap() > -1) ? ((root
852                     .getName() != null ? " " : "") + +root.getBootstrap())
853                     : "") : "") + ((RootHasDistance) ? (":" + root.dist)
854             : "")) : "";
855   }
856
857   // Non recursive call deals with root node properties
858   public void print(StringBuffer tf, SequenceNode root)
859   {
860     if (root != null)
861     {
862       if (root.isLeaf() && printRootInfo)
863       {
864         tf.append(printRootField(root));
865       }
866       else
867       {
868         if (root.isDummy())
869         {
870           _print(tf, (SequenceNode) root.right());
871           _print(tf, (SequenceNode) root.left());
872         }
873         else
874         {
875           tf.append("(");
876           _print(tf, (SequenceNode) root.right());
877
878           if (root.left() != null)
879           {
880             tf.append(",");
881           }
882
883           _print(tf, (SequenceNode) root.left());
884           tf.append(")" + printRootField(root));
885         }
886       }
887     }
888   }
889
890   // Recursive call for non-root nodes
891   public void _print(StringBuffer tf, SequenceNode c)
892   {
893     if (c != null)
894     {
895       if (c.isLeaf())
896       {
897         tf.append(printNodeField(c));
898       }
899       else
900       {
901         if (c.isDummy())
902         {
903           _print(tf, (SequenceNode) c.left());
904           if (c.left() != null)
905           {
906             tf.append(",");
907           }
908           _print(tf, (SequenceNode) c.right());
909         }
910         else
911         {
912           tf.append("(");
913           _print(tf, (SequenceNode) c.right());
914
915           if (c.left() != null)
916           {
917             tf.append(",");
918           }
919
920           _print(tf, (SequenceNode) c.left());
921           tf.append(")" + printNodeField(c));
922         }
923       }
924     }
925   }
926
927   // Test
928   public static void main(String[] args)
929   {
930     try
931     {
932       if (args == null || args.length != 1)
933       {
934         System.err
935                 .println("Takes one argument - file name of a newick tree file.");
936         System.exit(0);
937       }
938
939       File fn = new File(args[0]);
940
941       StringBuffer newickfile = new StringBuffer();
942       BufferedReader treefile = new BufferedReader(new FileReader(fn));
943       String l;
944
945       while ((l = treefile.readLine()) != null)
946       {
947         newickfile.append(l);
948       }
949
950       treefile.close();
951       System.out.println("Read file :\n");
952
953       NewickFile trf = new NewickFile(args[0], "File");
954       trf.parse();
955       System.out.println("Original file :\n");
956
957       com.stevesoft.pat.Regex nonl = new com.stevesoft.pat.Regex("\n+", "");
958       System.out.println(nonl.replaceAll(newickfile.toString()) + "\n");
959
960       System.out.println("Parsed file.\n");
961       System.out.println("Default output type for original input.\n");
962       System.out.println(trf.print());
963       System.out.println("Without bootstraps.\n");
964       System.out.println(trf.print(false));
965       System.out.println("Without distances.\n");
966       System.out.println(trf.print(true, false));
967       System.out.println("Without bootstraps but with distanecs.\n");
968       System.out.println(trf.print(false, true));
969       System.out.println("Without bootstraps or distanecs.\n");
970       System.out.println(trf.print(false, false));
971       System.out.println("With bootstraps and with distances.\n");
972       System.out.println(trf.print(true, true));
973     } catch (java.io.IOException e)
974     {
975       System.err.println("Exception\n" + e);
976       e.printStackTrace();
977     }
978   }
979 }