aa9a129fa54246d567278eb56708d6a0e4fc41d5
[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
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 = -1; // @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=null; // 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           }
503
504           // Reset new node properties to obvious fakes
505           nodename = null;
506           distance = DefDistance;
507           bootstrap = DefBootstrap;
508       }
509       if (nextcp==0)
510       {
511         ncp = cp = fcp + 1;
512       }
513       else {
514         cp=nextcp;
515         nextcp=0;
516       }
517     }
518
519     if (Error != null)
520     {
521       throw (new IOException("NewickFile: " + Error + "\n"));
522     }
523     if (root==null)
524     {
525       throw (new IOException("NewickFile: No Tree read in\n"));
526     }
527     // THe next line is failing for topali trees - not sure why yet. if (root.right()!=null && root.isDummy())
528     root = (SequenceNode) root.right().detach(); // remove the imaginary root.
529
530     if (!RootHasDistance)
531     {
532       root.dist = (HasDistances) ? 0 : DefDistance;
533     }
534   }
535
536   /**
537    * DOCUMENT ME!
538    *
539    * @return DOCUMENT ME!
540    */
541   public SequenceNode getTree()
542   {
543     return root;
544   }
545
546   /**
547    * Generate a newick format tree according to internal flags
548    * for bootstraps, distances and root distances.
549    *
550    * @return new hampshire tree in a single line
551    */
552   public String print()
553   {
554     synchronized (this)
555     {
556       StringBuffer tf = new StringBuffer();
557       print(tf, root);
558
559       return (tf.append(";").toString());
560     }
561   }
562
563   /**
564    *
565    *
566    * Generate a newick format tree according to internal flags
567    * for distances and root distances and user specificied writing of
568    * bootstraps.
569    * @param withbootstraps controls if bootstrap values are explicitly written.
570    *
571    * @return new hampshire tree in a single line
572    */
573   public String print(boolean withbootstraps)
574   {
575     synchronized (this)
576     {
577       boolean boots = this.HasBootstrap;
578       this.HasBootstrap = withbootstraps;
579
580       String rv = print();
581       this.HasBootstrap = boots;
582
583       return rv;
584     }
585   }
586
587   /**
588    *
589    * Generate newick format tree according to internal flags
590    * for writing root node distances.
591    *
592    * @param withbootstraps explicitly write bootstrap values
593    * @param withdists explicitly write distances
594    *
595    * @return new hampshire tree in a single line
596    */
597   public String print(boolean withbootstraps, boolean withdists)
598   {
599     synchronized (this)
600     {
601       boolean dists = this.HasDistances;
602       this.HasDistances = withdists;
603
604       String rv = print(withbootstraps);
605       this.HasDistances = dists;
606
607       return rv;
608     }
609   }
610
611   /**
612    * Generate newick format tree according to user specified flags
613    *
614    * @param withbootstraps explicitly write bootstrap values
615    * @param withdists explicitly write distances
616    * @param printRootInfo explicitly write root distance
617    *
618    * @return new hampshire tree in a single line
619    */
620   public String print(boolean withbootstraps, boolean withdists,
621                       boolean printRootInfo)
622   {
623     synchronized (this)
624     {
625       boolean rootinfo = printRootInfo;
626       this.printRootInfo = printRootInfo;
627
628       String rv = print(withbootstraps, withdists);
629       this.printRootInfo = rootinfo;
630
631       return rv;
632     }
633   }
634
635   /**
636    * DOCUMENT ME!
637    *
638    * @return DOCUMENT ME!
639    */
640   char getQuoteChar()
641   {
642     return QuoteChar;
643   }
644
645   /**
646    * DOCUMENT ME!
647    *
648    * @param c DOCUMENT ME!
649    *
650    * @return DOCUMENT ME!
651    */
652   char setQuoteChar(char c)
653   {
654     char old = QuoteChar;
655     QuoteChar = c;
656
657     return old;
658   }
659
660   /**
661    * DOCUMENT ME!
662    *
663    * @param name DOCUMENT ME!
664    *
665    * @return DOCUMENT ME!
666    */
667   private String nodeName(String name)
668   {
669     if (NodeSafeName[0].search(name))
670     {
671       return QuoteChar + NodeSafeName[1].replaceAll(name) + QuoteChar;
672     }
673     else
674     {
675       return NodeSafeName[2].replaceAll(name);
676     }
677   }
678
679   /**
680    * DOCUMENT ME!
681    *
682    * @param c DOCUMENT ME!
683    *
684    * @return DOCUMENT ME!
685    */
686   private String printNodeField(SequenceNode c)
687   {
688     return ( (c.getName() == null) ? "" : nodeName(c.getName())) +
689         ( (HasBootstrap)
690          ? ( (c.getBootstrap() > -1) ? ((c.getName()!=null ? " " : "")+ c.getBootstrap()) : "") : "") +
691         ( (HasDistances) ? (":" + c.dist) : "");
692   }
693
694   /**
695    * DOCUMENT ME!
696    *
697    * @param root DOCUMENT ME!
698    *
699    * @return DOCUMENT ME!
700    */
701   private String printRootField(SequenceNode root)
702   {
703     return (printRootInfo)
704         ? ( ( (root.getName() == null) ? "" : nodeName(root.getName())) +
705            ( (HasBootstrap)
706             ? ( (root.getBootstrap() > -1) ? ((root.getName()!=null ? " " : "")+
707                     + root.getBootstrap()) : "") :
708             "") +
709            ( (RootHasDistance) ? (":" + root.dist) : "")) : "";
710   }
711
712   // Non recursive call deals with root node properties
713   public void print(StringBuffer tf, SequenceNode root)
714   {
715     if (root != null)
716     {
717       if (root.isLeaf() && printRootInfo)
718       {
719         tf.append(printRootField(root));
720       }
721       else
722       {
723         if (root.isDummy())
724         {
725           _print(tf, (SequenceNode) root.right());
726           _print(tf, (SequenceNode) root.left());
727         }
728         else
729         {
730           tf.append("(");
731           _print(tf, (SequenceNode) root.right());
732
733           if (root.left() != null)
734           {
735             tf.append(",");
736           }
737
738           _print(tf, (SequenceNode) root.left());
739           tf.append(")" + printRootField(root));
740         }
741       }
742     }
743   }
744
745   // Recursive call for non-root nodes
746   public void _print(StringBuffer tf, SequenceNode c)
747   {
748     if (c != null)
749     {
750       if (c.isLeaf())
751       {
752         tf.append(printNodeField(c));
753       }
754       else
755       {
756         if (c.isDummy())
757         {
758           _print(tf, (SequenceNode) c.left());
759           if (c.left() != null)
760           {
761             tf.append(",");
762           }
763           _print(tf, (SequenceNode) c.right());
764         }
765         else
766         {
767           tf.append("(");
768           _print(tf, (SequenceNode) c.right());
769
770           if (c.left() != null)
771           {
772             tf.append(",");
773           }
774
775           _print(tf, (SequenceNode) c.left());
776           tf.append(")" + printNodeField(c));
777         }
778       }
779     }
780   }
781
782   // Test
783   public static void main(String[] args)
784   {
785     try
786     {
787       if (args == null || args.length != 1)
788       {
789         System.err.println(
790             "Takes one argument - file name of a newick tree file.");
791         System.exit(0);
792       }
793
794       File fn = new File(args[0]);
795
796       StringBuffer newickfile = new StringBuffer();
797       BufferedReader treefile = new BufferedReader(new FileReader(fn));
798       String l;
799
800       while ( (l = treefile.readLine()) != null)
801       {
802         newickfile.append(l);
803       }
804
805       treefile.close();
806       System.out.println("Read file :\n");
807
808       NewickFile trf = new NewickFile(args[0], "File");
809       trf.parse();
810       System.out.println("Original file :\n");
811
812       com.stevesoft.pat.Regex nonl = new com.stevesoft.pat.Regex("\n+", "");
813       System.out.println(nonl.replaceAll(newickfile.toString()) + "\n");
814
815       System.out.println("Parsed file.\n");
816       System.out.println("Default output type for original input.\n");
817       System.out.println(trf.print());
818       System.out.println("Without bootstraps.\n");
819       System.out.println(trf.print(false));
820       System.out.println("Without distances.\n");
821       System.out.println(trf.print(true, false));
822       System.out.println("Without bootstraps but with distanecs.\n");
823       System.out.println(trf.print(false, true));
824       System.out.println("Without bootstraps or distanecs.\n");
825       System.out.println(trf.print(false, false));
826       System.out.println("With bootstraps and with distances.\n");
827       System.out.println(trf.print(true, true));
828     }
829     catch (java.io.IOException e)
830     {
831       System.err.println("Exception\n" + e);
832       e.printStackTrace();
833     }
834   }
835 }