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