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