JAL-1628 formatting and javadoc
[jalview.git] / src / jalview / io / AnnotationFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
3  * Copyright (C) 2014 The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.analysis.Conservation;
24 import jalview.api.AlignViewportI;
25 import jalview.datamodel.AlignmentAnnotation;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.Annotation;
28 import jalview.datamodel.ColumnSelection;
29 import jalview.datamodel.GraphLine;
30 import jalview.datamodel.HiddenSequences;
31 import jalview.datamodel.SequenceGroup;
32 import jalview.datamodel.SequenceI;
33 import jalview.schemes.ColourSchemeI;
34 import jalview.schemes.ColourSchemeProperty;
35 import jalview.schemes.ResidueProperties;
36 import jalview.schemes.UserColourScheme;
37
38 import java.io.BufferedReader;
39 import java.io.FileReader;
40 import java.io.InputStreamReader;
41 import java.io.StringReader;
42 import java.net.URL;
43 import java.util.ArrayList;
44 import java.util.BitSet;
45 import java.util.Enumeration;
46 import java.util.Hashtable;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.StringTokenizer;
50 import java.util.Vector;
51
52 public class AnnotationFile
53 {
54   public AnnotationFile()
55   {
56     init();
57   }
58
59   /**
60    * character used to write newlines
61    */
62   protected String newline = System.getProperty("line.separator");
63
64   /**
65    * set new line string and reset the output buffer
66    * 
67    * @param nl
68    */
69   public void setNewlineString(String nl)
70   {
71     newline = nl;
72     init();
73   }
74
75   public String getNewlineString()
76   {
77     return newline;
78   }
79
80   StringBuffer text;
81
82   private void init()
83   {
84     text = new StringBuffer("JALVIEW_ANNOTATION" + newline + "# Created: "
85             + new java.util.Date() + newline + newline);
86     refSeq = null;
87     refSeqId = null;
88   }
89
90   /**
91    * convenience method for pre-2.9 annotation files which have no view, hidden
92    * columns or hidden row keywords.
93    * 
94    * @param annotations
95    * @param list
96    * @param properties
97    * @return annotation file as a string.
98    */
99   public String printAnnotations(AlignmentAnnotation[] annotations,
100           List<SequenceGroup> list, Hashtable properties)
101   {
102     return printAnnotations(annotations, list, properties, null);
103
104   }
105
106   /**
107    * hold all the information about a particular view definition read from or
108    * written out in an annotations file.
109    */
110   public class ViewDef
111   {
112     public String viewname;
113
114     public HiddenSequences hidseqs;
115
116     public ColumnSelection hiddencols;
117
118     public Vector visibleGroups;
119
120     public Hashtable hiddenRepSeqs;
121
122     public ViewDef(String viewname, HiddenSequences hidseqs,
123             ColumnSelection hiddencols, Hashtable hiddenRepSeqs)
124     {
125       this.viewname = viewname;
126       this.hidseqs = hidseqs;
127       this.hiddencols = hiddencols;
128       this.hiddenRepSeqs = hiddenRepSeqs;
129     }
130   }
131
132   /**
133    * Prepare an annotation file given a set of annotations, groups, alignment
134    * properties and views.
135    * 
136    * @param annotations
137    * @param list
138    * @param properties
139    * @param views
140    * @return annotation file
141    */
142   public String printAnnotations(AlignmentAnnotation[] annotations,
143           List<SequenceGroup> list, Hashtable properties, ViewDef[] views)
144   {
145     // TODO: resolve views issue : annotationFile could contain visible region,
146     // or full data + hidden region specifications for a view.
147     if (views != null)
148     {
149       // are views defined and then annotation added to alignment or the other
150       // way around ?
151
152     }
153     if (annotations != null)
154     {
155       boolean oneColour = true;
156       AlignmentAnnotation row;
157       String comma;
158       SequenceI refSeq = null;
159       SequenceGroup refGroup = null;
160
161       StringBuffer colours = new StringBuffer();
162       StringBuffer graphLine = new StringBuffer();
163       StringBuffer rowprops = new StringBuffer();
164       Hashtable<Integer, String> graphGroup = new Hashtable<Integer, String>();
165       Hashtable<Integer, Object[]> graphGroup_refs = new Hashtable<Integer, Object[]>();
166       BitSet graphGroupSeen = new BitSet();
167
168       java.awt.Color color;
169
170       for (int i = 0; i < annotations.length; i++)
171       {
172         row = annotations[i];
173
174         if (!row.visible
175                 && !row.hasScore()
176                 && !(row.graphGroup > -1 && graphGroupSeen
177                         .get(row.graphGroup)))
178         {
179           continue;
180         }
181
182         color = null;
183         oneColour = true;
184
185         // mark any sequence references for the row
186         writeSequence_Ref(refSeq, row.sequenceRef);
187         refSeq = row.sequenceRef;
188         // mark any group references for the row
189         writeGroup_Ref(refGroup, row.groupRef);
190         refGroup = row.groupRef;
191
192         boolean hasGlyphs = row.hasIcons, hasLabels = row.hasText, hasValues = row.hasScore, hasText = false;
193         // lookahead to check what the annotation row object actually contains.
194         for (int j = 0; row.annotations != null
195                 && j < row.annotations.length
196                 && (!hasGlyphs || !hasLabels || !hasValues); j++)
197         {
198           if (row.annotations[j] != null)
199           {
200             hasLabels |= (row.annotations[j].displayCharacter != null
201                     && row.annotations[j].displayCharacter.length() > 0 && !row.annotations[j].displayCharacter
202                     .equals(" "));
203             hasGlyphs |= (row.annotations[j].secondaryStructure != 0 && row.annotations[j].secondaryStructure != ' ');
204             hasValues |= (row.annotations[j].value != Float.NaN); // NaNs can't
205             // be
206             // rendered..
207             hasText |= (row.annotations[j].description != null && row.annotations[j].description
208                     .length() > 0);
209           }
210         }
211
212         if (row.graph == AlignmentAnnotation.NO_GRAPH)
213         {
214           text.append("NO_GRAPH\t");
215           hasValues = false; // only secondary structure
216           // hasLabels = false; // and annotation description string.
217         }
218         else
219         {
220           if (row.graph == AlignmentAnnotation.BAR_GRAPH)
221           {
222             text.append("BAR_GRAPH\t");
223             hasGlyphs = false; // no secondary structure
224
225           }
226           else if (row.graph == AlignmentAnnotation.LINE_GRAPH)
227           {
228             hasGlyphs = false; // no secondary structure
229             text.append("LINE_GRAPH\t");
230           }
231
232           if (row.getThreshold() != null)
233           {
234             graphLine.append("GRAPHLINE\t");
235             graphLine.append(row.label);
236             graphLine.append("\t");
237             graphLine.append(row.getThreshold().value);
238             graphLine.append("\t");
239             graphLine.append(row.getThreshold().label);
240             graphLine.append("\t");
241             graphLine.append(jalview.util.Format.getHexString(row
242                     .getThreshold().colour));
243             graphLine.append(newline);
244           }
245
246           if (row.graphGroup > -1)
247           {
248             graphGroupSeen.set(row.graphGroup);
249             Integer key = new Integer(row.graphGroup);
250             if (graphGroup.containsKey(key))
251             {
252               graphGroup.put(key, graphGroup.get(key) + "\t" + row.label);
253
254             }
255             else
256             {
257               graphGroup_refs.put(key, new Object[]
258               { refSeq, refGroup });
259               graphGroup.put(key, row.label);
260             }
261           }
262         }
263
264         text.append(row.label + "\t");
265         if (row.description != null)
266         {
267           text.append(row.description + "\t");
268         }
269         for (int j = 0; row.annotations != null
270                 && j < row.annotations.length; j++)
271         {
272           if (refSeq != null
273                   && jalview.util.Comparison.isGap(refSeq.getCharAt(j)))
274           {
275             continue;
276           }
277
278           if (row.annotations[j] != null)
279           {
280             comma = "";
281             if (hasGlyphs) // could be also hasGlyphs || ...
282             {
283
284               text.append(comma);
285               if (row.annotations[j].secondaryStructure != ' ')
286               {
287                 // only write out the field if its not whitespace.
288                 text.append(row.annotations[j].secondaryStructure);
289               }
290               comma = ",";
291             }
292             if (hasValues)
293             {
294               if (row.annotations[j].value != Float.NaN)
295               {
296                 text.append(comma + row.annotations[j].value);
297               }
298               else
299               {
300                 System.err.println("Skipping NaN - not valid value.");
301                 text.append(comma + 0f);// row.annotations[j].value);
302               }
303               comma = ",";
304             }
305             if (hasLabels)
306             {
307               // TODO: labels are emitted after values for bar graphs.
308               if // empty labels are allowed, so
309               (row.annotations[j].displayCharacter != null
310                       && row.annotations[j].displayCharacter.length() > 0
311                       && !row.annotations[j].displayCharacter.equals(" "))
312               {
313                 text.append(comma + row.annotations[j].displayCharacter);
314                 comma = ",";
315               }
316             }
317             if (hasText)
318             {
319               if (row.annotations[j].description != null
320                       && row.annotations[j].description.length() > 0
321                       && !row.annotations[j].description
322                               .equals(row.annotations[j].displayCharacter))
323               {
324                 text.append(comma + row.annotations[j].description);
325                 comma = ",";
326               }
327             }
328             if (color != null && !color.equals(row.annotations[j].colour))
329             {
330               oneColour = false;
331             }
332
333             color = row.annotations[j].colour;
334
335             if (row.annotations[j].colour != null
336                     && row.annotations[j].colour != java.awt.Color.black)
337             {
338               text.append(comma
339                       + "["
340                       + jalview.util.Format
341                               .getHexString(row.annotations[j].colour)
342                       + "]");
343               comma = ",";
344             }
345           }
346           text.append("|");
347         }
348
349         if (row.hasScore())
350         {
351           text.append("\t" + row.score);
352         }
353
354         text.append(newline);
355
356         if (color != null && color != java.awt.Color.black && oneColour)
357         {
358           colours.append("COLOUR\t");
359           colours.append(row.label);
360           colours.append("\t");
361           colours.append(jalview.util.Format.getHexString(color));
362           colours.append(newline);
363         }
364         if (row.scaleColLabel || row.showAllColLabels
365                 || row.centreColLabels)
366         {
367           rowprops.append("ROWPROPERTIES\t");
368           rowprops.append(row.label);
369           rowprops.append("\tscaletofit=");
370           rowprops.append(row.scaleColLabel);
371           rowprops.append("\tshowalllabs=");
372           rowprops.append(row.showAllColLabels);
373           rowprops.append("\tcentrelabs=");
374           rowprops.append(row.centreColLabels);
375           rowprops.append(newline);
376         }
377         if (graphLine.length() > 0)
378         {
379           text.append(graphLine.toString());
380           graphLine.setLength(0);
381         }
382       }
383
384       text.append(newline);
385
386       text.append(colours.toString());
387       if (graphGroup.size() > 0)
388       {
389         SequenceI oldRefSeq = refSeq;
390         SequenceGroup oldRefGroup = refGroup;
391         for (Map.Entry<Integer, String> combine_statement : graphGroup
392                 .entrySet())
393         {
394           Object[] seqRefAndGroup = graphGroup_refs.get(combine_statement
395                   .getKey());
396
397           writeSequence_Ref(refSeq, (SequenceI) seqRefAndGroup[0]);
398           refSeq = (SequenceI) seqRefAndGroup[0];
399
400           writeGroup_Ref(refGroup, (SequenceGroup) seqRefAndGroup[1]);
401           refGroup = (SequenceGroup) seqRefAndGroup[1];
402           text.append("COMBINE\t");
403           text.append(combine_statement.getValue());
404           text.append(newline);
405         }
406         writeSequence_Ref(refSeq, oldRefSeq);
407         refSeq = oldRefSeq;
408
409         writeGroup_Ref(refGroup, oldRefGroup);
410         refGroup = oldRefGroup;
411       }
412       text.append(rowprops.toString());
413     }
414
415     if (list != null)
416     {
417       printGroups(list);
418     }
419
420     if (properties != null)
421     {
422       text.append(newline);
423       text.append(newline);
424       text.append("ALIGNMENT");
425       Enumeration en = properties.keys();
426       while (en.hasMoreElements())
427       {
428         String key = en.nextElement().toString();
429         text.append("\t");
430         text.append(key);
431         text.append("=");
432         text.append(properties.get(key));
433       }
434       // TODO: output alignment visualization settings here if required
435
436     }
437
438     return text.toString();
439   }
440
441   private Object writeGroup_Ref(SequenceGroup refGroup,
442           SequenceGroup next_refGroup)
443   {
444     if (next_refGroup == null)
445     {
446
447       if (refGroup != null)
448       {
449         text.append(newline);
450         text.append("GROUP_REF\t");
451         text.append("ALIGNMENT");
452         text.append(newline);
453       }
454       return true;
455     }
456     else
457     {
458       if (refGroup == null || refGroup != next_refGroup)
459       {
460         text.append(newline);
461         text.append("GROUP_REF\t");
462         text.append(next_refGroup.getName());
463         text.append(newline);
464         return true;
465       }
466     }
467     return false;
468   }
469
470   private boolean writeSequence_Ref(SequenceI refSeq, SequenceI next_refSeq)
471   {
472
473     if (next_refSeq == null)
474     {
475       if (refSeq != null)
476       {
477         text.append(newline);
478         text.append("SEQUENCE_REF\t");
479         text.append("ALIGNMENT");
480         text.append(newline);
481         return true;
482       }
483     }
484     else
485     {
486       if (refSeq == null || refSeq != next_refSeq)
487       {
488         text.append(newline);
489         text.append("SEQUENCE_REF\t");
490         text.append(next_refSeq.getName());
491         text.append(newline);
492         return true;
493       }
494     }
495     return false;
496   }
497
498   public void printGroups(List<SequenceGroup> list)
499   {
500     SequenceI seqrep = null;
501     for (SequenceGroup sg : list)
502     {
503       if (!sg.hasSeqrep())
504       {
505         text.append("SEQUENCE_GROUP\t" + sg.getName() + "\t"
506                 + (sg.getStartRes() + 1) + "\t" + (sg.getEndRes() + 1)
507                 + "\t" + "-1\t");
508         seqrep = null;
509       }
510       else
511       {
512         seqrep = sg.getSeqrep();
513         text.append("SEQUENCE_REF\t");
514         text.append(seqrep.getName());
515         text.append(newline);
516         text.append("SEQUENCE_GROUP\t");
517         text.append(sg.getName());
518         text.append("\t");
519         text.append((seqrep.findPosition(sg.getStartRes())));
520         text.append("\t");
521         text.append((seqrep.findPosition(sg.getEndRes())));
522         text.append("\t");
523         text.append("-1\t");
524       }
525       for (int s = 0; s < sg.getSize(); s++)
526       {
527         text.append(sg.getSequenceAt(s).getName());
528         text.append("\t");
529       }
530       text.append(newline);
531       text.append("PROPERTIES\t");
532       text.append(sg.getName());
533       text.append("\t");
534
535       if (sg.getDescription() != null)
536       {
537         text.append("description=");
538         text.append(sg.getDescription());
539         text.append("\t");
540       }
541       if (sg.cs != null)
542       {
543         text.append("colour=");
544         text.append(ColourSchemeProperty.getColourName(sg.cs));
545         text.append("\t");
546         if (sg.cs.getThreshold() != 0)
547         {
548           text.append("pidThreshold=");
549           text.append(sg.cs.getThreshold());
550         }
551         if (sg.cs.conservationApplied())
552         {
553           text.append("consThreshold=");
554           text.append(sg.cs.getConservationInc());
555           text.append("\t");
556         }
557       }
558       text.append("outlineColour=");
559       text.append(jalview.util.Format.getHexString(sg.getOutlineColour()));
560       text.append("\t");
561
562       text.append("displayBoxes=");
563       text.append(sg.getDisplayBoxes());
564       text.append("\t");
565       text.append("displayText=");
566       text.append(sg.getDisplayText());
567       text.append("\t");
568       text.append("colourText=");
569       text.append(sg.getColourText());
570       text.append("\t");
571       text.append("showUnconserved=");
572       text.append(sg.getShowNonconserved());
573       text.append("\t");
574       if (sg.textColour != java.awt.Color.black)
575       {
576         text.append("textCol1=");
577         text.append(jalview.util.Format.getHexString(sg.textColour));
578         text.append("\t");
579       }
580       if (sg.textColour2 != java.awt.Color.white)
581       {
582         text.append("textCol2=");
583         text.append(jalview.util.Format.getHexString(sg.textColour2));
584         text.append("\t");
585       }
586       if (sg.thresholdTextColour != 0)
587       {
588         text.append("textColThreshold=");
589         text.append(sg.thresholdTextColour);
590         text.append("\t");
591       }
592       if (sg.idColour != null)
593       {
594         text.append("idColour=");
595         text.append(jalview.util.Format.getHexString(sg.idColour));
596         text.append("\t");
597       }
598       if (sg.isHidereps())
599       {
600         text.append("hide=true\t");
601       }
602       if (sg.isHideCols())
603       {
604         text.append("hidecols=true\t");
605       }
606       if (seqrep != null)
607       {
608         // terminate the last line and clear the sequence ref for the group
609         text.append(newline);
610         text.append("SEQUENCE_REF");
611       }
612       text.append(newline);
613       text.append(newline);
614
615     }
616   }
617
618   SequenceI refSeq = null;
619
620   String refSeqId = null;
621
622   public boolean readAnnotationFile(AlignmentI al, String file,
623           String protocol)
624   {
625     BufferedReader in = null;
626     try
627     {
628       if (protocol.equals(AppletFormatAdapter.FILE))
629       {
630         in = new BufferedReader(new FileReader(file));
631       }
632       else if (protocol.equals(AppletFormatAdapter.URL))
633       {
634         URL url = new URL(file);
635         in = new BufferedReader(new InputStreamReader(url.openStream()));
636       }
637       else if (protocol.equals(AppletFormatAdapter.PASTE))
638       {
639         in = new BufferedReader(new StringReader(file));
640       }
641       else if (protocol.equals(AppletFormatAdapter.CLASSLOADER))
642       {
643         java.io.InputStream is = getClass().getResourceAsStream("/" + file);
644         if (is != null)
645         {
646           in = new BufferedReader(new java.io.InputStreamReader(is));
647         }
648       }
649       if (in != null)
650       {
651         return parseAnnotationFrom(al, colSel, in);
652       }
653
654     } catch (Exception ex)
655     {
656       ex.printStackTrace();
657       System.out.println("Problem reading annotation file: " + ex);
658       if (nlinesread > 0)
659       {
660         System.out.println("Last read line " + nlinesread + ": '"
661                 + lastread + "' (first 80 chars) ...");
662       }
663       return false;
664     }
665     return false;
666   }
667
668   long nlinesread = 0;
669
670   String lastread = "";
671
672   private static String GRAPHLINE = "GRAPHLINE", COMBINE = "COMBINE";
673
674   public boolean parseAnnotationFrom(AlignmentI al, ColumnSelection colSel,
675           BufferedReader in)
676           throws Exception
677   {
678     nlinesread = 0;
679     ArrayList<Object[]> combineAnnotation_calls = new ArrayList<Object[]>();
680     ArrayList<Object[]> deferredAnnotation_calls = new ArrayList<Object[]>();
681     boolean modified = false;
682     String groupRef = null;
683     Hashtable groupRefRows = new Hashtable();
684
685     Hashtable autoAnnots = new Hashtable();
686     {
687       String line, label, description, token;
688       int graphStyle, index;
689       int refSeqIndex = 1;
690       int existingAnnotations = 0;
691       // when true - will add new rows regardless of whether they are duplicate
692       // auto-annotation like consensus or conservation graphs
693       boolean overrideAutoAnnot = false;
694       if (al.getAlignmentAnnotation() != null)
695       {
696         existingAnnotations = al.getAlignmentAnnotation().length;
697         if (existingAnnotations > 0)
698         {
699           AlignmentAnnotation[] aa = al.getAlignmentAnnotation();
700           for (int aai = 0; aai < aa.length; aai++)
701           {
702             if (aa[aai].autoCalculated)
703             {
704               // make a note of the name and description
705               autoAnnots.put(
706                       autoAnnotsKey(aa[aai], aa[aai].sequenceRef,
707                               (aa[aai].groupRef == null ? null
708                                       : aa[aai].groupRef.getName())),
709                       new Integer(1));
710             }
711           }
712         }
713       }
714
715       int alWidth = al.getWidth();
716
717       StringTokenizer st;
718       Annotation[] annotations;
719       AlignmentAnnotation annotation = null;
720
721       // First confirm this is an Annotation file
722       boolean jvAnnotationFile = false;
723       while ((line = in.readLine()) != null)
724       {
725         nlinesread++;
726         lastread = new String(line);
727         if (line.indexOf("#") == 0)
728         {
729           continue;
730         }
731
732         if (line.indexOf("JALVIEW_ANNOTATION") > -1)
733         {
734           jvAnnotationFile = true;
735           break;
736         }
737       }
738
739       if (!jvAnnotationFile)
740       {
741         in.close();
742         return false;
743       }
744
745       while ((line = in.readLine()) != null)
746       {
747         nlinesread++;
748         lastread = new String(line);
749         if (line.indexOf("#") == 0
750                 || line.indexOf("JALVIEW_ANNOTATION") > -1
751                 || line.length() == 0)
752         {
753           continue;
754         }
755
756         st = new StringTokenizer(line, "\t");
757         token = st.nextToken();
758         if (token.equalsIgnoreCase("COLOUR"))
759         {
760           // TODO: use graduated colour def'n here too
761           colourAnnotations(al, st.nextToken(), st.nextToken());
762           modified = true;
763           continue;
764         }
765
766         else if (token.equalsIgnoreCase(COMBINE))
767         {
768           // keep a record of current state and resolve groupRef at end
769           combineAnnotation_calls.add(new Object[]
770           { st, refSeq, groupRef });
771           modified = true;
772           continue;
773         }
774         else if (token.equalsIgnoreCase("ROWPROPERTIES"))
775         {
776           addRowProperties(al, st);
777           modified = true;
778           continue;
779         }
780         else if (token.equalsIgnoreCase(GRAPHLINE))
781         {
782           // resolve at end
783           deferredAnnotation_calls.add(new Object[]
784           { GRAPHLINE, st, refSeq, groupRef });
785           modified = true;
786           continue;
787         }
788
789         else if (token.equalsIgnoreCase("SEQUENCE_REF"))
790         {
791           if (st.hasMoreTokens())
792           {
793             refSeq = al.findName(refSeqId = st.nextToken());
794             if (refSeq == null)
795             {
796               refSeqId = null;
797             }
798             try
799             {
800               refSeqIndex = Integer.parseInt(st.nextToken());
801               if (refSeqIndex < 1)
802               {
803                 refSeqIndex = 1;
804                 System.out
805                         .println("WARNING: SEQUENCE_REF index must be > 0 in AnnotationFile");
806               }
807             } catch (Exception ex)
808             {
809               refSeqIndex = 1;
810             }
811           }
812           else
813           {
814             refSeq = null;
815             refSeqId = null;
816           }
817           continue;
818         }
819         else if (token.equalsIgnoreCase("GROUP_REF"))
820         {
821           // Group references could be forward or backwards, so they are
822           // resolved after the whole file is read in
823           groupRef = null;
824           if (st.hasMoreTokens())
825           {
826             groupRef = st.nextToken();
827             if (groupRef.length() < 1)
828             {
829               groupRef = null; // empty string
830             }
831             else
832             {
833               if (groupRefRows.get(groupRef) == null)
834               {
835                 groupRefRows.put(groupRef, new Vector());
836               }
837             }
838           }
839           continue;
840         }
841         else if (token.equalsIgnoreCase("SEQUENCE_GROUP"))
842         {
843           addGroup(al, st);
844           modified = true;
845           continue;
846         }
847
848         else if (token.equalsIgnoreCase("PROPERTIES"))
849         {
850           addProperties(al, st);
851           modified = true;
852           continue;
853         }
854
855         else if (token.equalsIgnoreCase("BELOW_ALIGNMENT"))
856         {
857           setBelowAlignment(al, st);
858           modified = true;
859           continue;
860         }
861         else if (token.equalsIgnoreCase("ALIGNMENT"))
862         {
863           addAlignmentDetails(al, st);
864           modified = true;
865           continue;
866         }
867
868         // Parse out the annotation row
869         graphStyle = AlignmentAnnotation.getGraphValueFromString(token);
870         label = st.nextToken();
871
872         index = 0;
873         annotations = new Annotation[alWidth];
874         description = null;
875         float score = Float.NaN;
876
877         if (st.hasMoreTokens())
878         {
879           line = st.nextToken();
880
881           if (line.indexOf("|") == -1)
882           {
883             description = line;
884             if (st.hasMoreTokens())
885             {
886               line = st.nextToken();
887             }
888           }
889
890           if (st.hasMoreTokens())
891           {
892             // This must be the score
893             score = Float.valueOf(st.nextToken()).floatValue();
894           }
895
896           st = new StringTokenizer(line, "|", true);
897
898           boolean emptyColumn = true;
899           boolean onlyOneElement = (st.countTokens() == 1);
900
901           while (st.hasMoreElements() && index < alWidth)
902           {
903             token = st.nextToken().trim();
904
905             if (onlyOneElement)
906             {
907               try
908               {
909                 score = Float.valueOf(token).floatValue();
910                 break;
911               } catch (NumberFormatException ex)
912               {
913               }
914             }
915
916             if (token.equals("|"))
917             {
918               if (emptyColumn)
919               {
920                 index++;
921               }
922
923               emptyColumn = true;
924             }
925             else
926             {
927               annotations[index++] = parseAnnotation(token, graphStyle);
928               emptyColumn = false;
929             }
930           }
931
932         }
933
934         annotation = new AlignmentAnnotation(label, description,
935                 (index == 0) ? null : annotations, 0, 0, graphStyle);
936
937         annotation.score = score;
938         if (!overrideAutoAnnot
939                 && autoAnnots.containsKey(autoAnnotsKey(annotation, refSeq,
940                         groupRef)))
941         {
942           // skip - we've already got an automatic annotation of this type.
943           continue;
944         }
945         // otherwise add it!
946         if (refSeq != null)
947         {
948
949           annotation.belowAlignment = false;
950           // make a copy of refSeq so we can find other matches in the alignment
951           SequenceI referedSeq = refSeq;
952           do
953           {
954             // copy before we do any mapping business.
955             // TODO: verify that undo/redo with 1:many sequence associated
956             // annotations can be undone correctly
957             AlignmentAnnotation ann = new AlignmentAnnotation(annotation);
958             annotation
959                     .createSequenceMapping(referedSeq, refSeqIndex, false);
960             annotation.adjustForAlignment();
961             referedSeq.addAlignmentAnnotation(annotation);
962             al.addAnnotation(annotation);
963             al.setAnnotationIndex(annotation,
964                     al.getAlignmentAnnotation().length
965                             - existingAnnotations - 1);
966             if (groupRef != null)
967             {
968               ((Vector) groupRefRows.get(groupRef)).addElement(annotation);
969             }
970             // and recover our virgin copy to use again if necessary.
971             annotation = ann;
972
973           } while (refSeqId != null
974                   && (referedSeq = al.findName(referedSeq, refSeqId, true)) != null);
975         }
976         else
977         {
978           al.addAnnotation(annotation);
979           al.setAnnotationIndex(annotation,
980                   al.getAlignmentAnnotation().length - existingAnnotations
981                           - 1);
982           if (groupRef != null)
983           {
984             ((Vector) groupRefRows.get(groupRef)).addElement(annotation);
985           }
986         }
987         // and set modification flag
988         modified = true;
989       }
990       // Resolve the groupRefs
991       Hashtable<String, SequenceGroup> groupRefLookup = new Hashtable<String, SequenceGroup>();
992       Enumeration en = groupRefRows.keys();
993
994       while (en.hasMoreElements())
995       {
996         groupRef = (String) en.nextElement();
997         boolean matched = false;
998         // Resolve group: TODO: add a getGroupByName method to alignments
999         for (SequenceGroup theGroup : al.getGroups())
1000         {
1001           if (theGroup.getName().equals(groupRef))
1002           {
1003             if (matched)
1004             {
1005               // TODO: specify and implement duplication of alignment annotation
1006               // for multiple group references.
1007               System.err
1008                       .println("Ignoring 1:many group reference mappings for group name '"
1009                               + groupRef + "'");
1010             }
1011             else
1012             {
1013               matched = true;
1014               Vector rowset = (Vector) groupRefRows.get(groupRef);
1015               groupRefLookup.put(groupRef, theGroup);
1016               if (rowset != null && rowset.size() > 0)
1017               {
1018                 AlignmentAnnotation alan = null;
1019                 for (int elm = 0, elmSize = rowset.size(); elm < elmSize; elm++)
1020                 {
1021                   alan = (AlignmentAnnotation) rowset.elementAt(elm);
1022                   alan.groupRef = theGroup;
1023                 }
1024               }
1025             }
1026           }
1027         }
1028         ((Vector) groupRefRows.get(groupRef)).removeAllElements();
1029       }
1030       // process any deferred attribute settings for each context
1031       for (Object[] _deferred_args : deferredAnnotation_calls)
1032       {
1033         if (_deferred_args[0] == GRAPHLINE)
1034         {
1035           addLine(al,
1036                   (StringTokenizer) _deferred_args[1], // st
1037                   (SequenceI) _deferred_args[2], // refSeq
1038                   (_deferred_args[3] == null) ? null : groupRefLookup
1039                           .get(_deferred_args[3]) // the reference
1040                                                            // group, or null
1041           );
1042         }
1043       }
1044
1045       // finally, combine all the annotation rows within each context.
1046       /**
1047        * number of combine statements in this annotation file. Used to create
1048        * new groups for combined annotation graphs without disturbing existing
1049        * ones
1050        */
1051       int combinecount = 0;
1052       for (Object[] _combine_args : combineAnnotation_calls)
1053       {
1054         combineAnnotations(al,
1055                 ++combinecount,
1056                 (StringTokenizer) _combine_args[0], // st
1057                 (SequenceI) _combine_args[1], // refSeq
1058                 (_combine_args[2] == null) ? null : groupRefLookup
1059                         .get(_combine_args[2]) // the reference group,
1060                                                         // or null
1061         );
1062       }
1063     }
1064     return modified;
1065   }
1066
1067   private Object autoAnnotsKey(AlignmentAnnotation annotation,
1068           SequenceI refSeq, String groupRef)
1069   {
1070     return annotation.graph + "\t" + annotation.label + "\t"
1071             + annotation.description + "\t"
1072             + (refSeq != null ? refSeq.getDisplayId(true) : "");
1073   }
1074
1075   Annotation parseAnnotation(String string, int graphStyle)
1076   {
1077     boolean hasSymbols = (graphStyle == AlignmentAnnotation.NO_GRAPH); // don't
1078     // do the
1079     // glyph
1080     // test
1081     // if we
1082     // don't
1083     // want
1084     // secondary
1085     // structure
1086     String desc = null, displayChar = null;
1087     char ss = ' '; // secondaryStructure
1088     float value = 0;
1089     boolean parsedValue = false, dcset = false;
1090
1091     // find colour here
1092     java.awt.Color colour = null;
1093     int i = string.indexOf("[");
1094     int j = string.indexOf("]");
1095     if (i > -1 && j > -1)
1096     {
1097       UserColourScheme ucs = new UserColourScheme();
1098
1099       colour = ucs.getColourFromString(string.substring(i + 1, j));
1100       if (i > 0 && string.charAt(i - 1) == ',')
1101       {
1102         // clip the preceding comma as well
1103         i--;
1104       }
1105       string = string.substring(0, i) + string.substring(j + 1);
1106     }
1107
1108     StringTokenizer st = new StringTokenizer(string, ",", true);
1109     String token;
1110     boolean seenContent = false;
1111     int pass = 0;
1112     while (st.hasMoreTokens())
1113     {
1114       pass++;
1115       token = st.nextToken().trim();
1116       if (token.equals(","))
1117       {
1118         if (!seenContent && parsedValue && !dcset)
1119         {
1120           // allow the value below the bar/line to be empty
1121           dcset = true;
1122           displayChar = " ";
1123         }
1124         seenContent = false;
1125         continue;
1126       }
1127       else
1128       {
1129         seenContent = true;
1130       }
1131
1132       if (!parsedValue)
1133       {
1134         try
1135         {
1136           displayChar = token;
1137           // foo
1138           value = new Float(token).floatValue();
1139           parsedValue = true;
1140           continue;
1141         } catch (NumberFormatException ex)
1142         {
1143         }
1144       }
1145       else
1146       {
1147         if (token.length() == 1)
1148         {
1149           displayChar = token;
1150         }
1151       }
1152       if (hasSymbols
1153               && (token.equals("H") || token.equals("E")
1154                       || token.equals("S") || token.equals(" ")))
1155       {
1156         // Either this character represents a helix or sheet
1157         // or an integer which can be displayed
1158         ss = token.charAt(0);
1159         if (displayChar.equals(token.substring(0, 1)))
1160         {
1161           displayChar = "";
1162         }
1163       }
1164       else if (desc == null || (parsedValue && pass > 2))
1165       {
1166         desc = token;
1167       }
1168
1169     }
1170     // if (!dcset && string.charAt(string.length() - 1) == ',')
1171     // {
1172     // displayChar = " "; // empty display char symbol.
1173     // }
1174     if (displayChar != null && desc != null && desc.length() == 1)
1175     {
1176       if (displayChar.length() > 1)
1177       {
1178         // switch desc and displayChar - legacy support
1179         String tmp = displayChar;
1180         displayChar = desc;
1181         desc = tmp;
1182       }
1183       else
1184       {
1185         if (displayChar.equals(desc))
1186         {
1187           // duplicate label - hangover from the 'robust parser' above
1188           desc = null;
1189         }
1190       }
1191     }
1192     Annotation anot = new Annotation(displayChar, desc, ss, value);
1193
1194     anot.colour = colour;
1195
1196     return anot;
1197   }
1198
1199   void colourAnnotations(AlignmentI al, String label, String colour)
1200   {
1201     UserColourScheme ucs = new UserColourScheme(colour);
1202     Annotation[] annotations;
1203     for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1204     {
1205       if (al.getAlignmentAnnotation()[i].label.equalsIgnoreCase(label))
1206       {
1207         annotations = al.getAlignmentAnnotation()[i].annotations;
1208         for (int j = 0; j < annotations.length; j++)
1209         {
1210           if (annotations[j] != null)
1211           {
1212             annotations[j].colour = ucs.findColour('A');
1213           }
1214         }
1215       }
1216     }
1217   }
1218
1219   void combineAnnotations(AlignmentI al, int combineCount,
1220           StringTokenizer st, SequenceI seqRef, SequenceGroup groupRef)
1221   {
1222     String group = st.nextToken();
1223     // First make sure we are not overwriting the graphIndex
1224     int graphGroup = 0;
1225     if (al.getAlignmentAnnotation() != null)
1226     {
1227       for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1228       {
1229         AlignmentAnnotation aa = al.getAlignmentAnnotation()[i];
1230
1231         if (aa.graphGroup > graphGroup)
1232         {
1233           // try to number graphGroups in order of occurence.
1234           graphGroup = aa.graphGroup + 1;
1235         }
1236         if (aa.sequenceRef == seqRef && aa.groupRef == groupRef
1237                 && aa.label.equalsIgnoreCase(group))
1238         {
1239           if (aa.graphGroup > -1)
1240           {
1241             graphGroup = aa.graphGroup;
1242           }
1243           else
1244           {
1245             if (graphGroup <= combineCount)
1246             {
1247               graphGroup = combineCount + 1;
1248             }
1249             aa.graphGroup = graphGroup;
1250           }
1251           break;
1252         }
1253       }
1254
1255       // Now update groups
1256       while (st.hasMoreTokens())
1257       {
1258         group = st.nextToken();
1259         for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1260         {
1261           AlignmentAnnotation aa = al.getAlignmentAnnotation()[i];
1262           if (aa.sequenceRef == seqRef && aa.groupRef == groupRef
1263                   && aa.label.equalsIgnoreCase(group))
1264           {
1265             aa.graphGroup = graphGroup;
1266             break;
1267           }
1268         }
1269       }
1270     }
1271     else
1272     {
1273       System.err
1274               .println("Couldn't combine annotations. None are added to alignment yet!");
1275     }
1276   }
1277
1278   void addLine(AlignmentI al, StringTokenizer st, SequenceI seqRef,
1279           SequenceGroup groupRef)
1280   {
1281     String group = st.nextToken();
1282     AlignmentAnnotation annotation = null, alannot[] = al
1283             .getAlignmentAnnotation();
1284     float value = new Float(st.nextToken()).floatValue();
1285     String label = st.hasMoreTokens() ? st.nextToken() : null;
1286     java.awt.Color colour = null;
1287     if (st.hasMoreTokens())
1288     {
1289       UserColourScheme ucs = new UserColourScheme(st.nextToken());
1290       colour = ucs.findColour('A');
1291     }
1292     if (alannot != null)
1293     {
1294       for (int i = 0; i < alannot.length; i++)
1295       {
1296         if (alannot[i].label.equalsIgnoreCase(group)
1297                 && (seqRef == null || alannot[i].sequenceRef == seqRef)
1298                 && (groupRef == null || alannot[i].groupRef == groupRef))
1299         {
1300           alannot[i].setThreshold(new GraphLine(value, label, colour));
1301         }
1302       }
1303     }
1304     if (annotation == null)
1305     {
1306       return;
1307     }
1308   }
1309
1310   void addGroup(AlignmentI al, StringTokenizer st)
1311   {
1312     SequenceGroup sg = new SequenceGroup();
1313     sg.setName(st.nextToken());
1314     String rng = "";
1315     try
1316     {
1317       rng = st.nextToken();
1318       if (rng.length() > 0 && !rng.startsWith("*"))
1319       {
1320         sg.setStartRes(Integer.parseInt(rng) - 1);
1321       }
1322       else
1323       {
1324         sg.setStartRes(0);
1325       }
1326       rng = st.nextToken();
1327       if (rng.length() > 0 && !rng.startsWith("*"))
1328       {
1329         sg.setEndRes(Integer.parseInt(rng) - 1);
1330       }
1331       else
1332       {
1333         sg.setEndRes(al.getWidth() - 1);
1334       }
1335     } catch (Exception e)
1336     {
1337       System.err
1338               .println("Couldn't parse Group Start or End Field as '*' or a valid column or sequence index: '"
1339                       + rng + "' - assuming alignment width for group.");
1340       // assume group is full width
1341       sg.setStartRes(0);
1342       sg.setEndRes(al.getWidth() - 1);
1343     }
1344
1345     String index = st.nextToken();
1346     if (index.equals("-1"))
1347     {
1348       while (st.hasMoreElements())
1349       {
1350         sg.addSequence(al.findName(st.nextToken()), false);
1351       }
1352     }
1353     else
1354     {
1355       StringTokenizer st2 = new StringTokenizer(index, ",");
1356
1357       while (st2.hasMoreTokens())
1358       {
1359         String tmp = st2.nextToken();
1360         if (tmp.equals("*"))
1361         {
1362           for (int i = 0; i < al.getHeight(); i++)
1363           {
1364             sg.addSequence(al.getSequenceAt(i), false);
1365           }
1366         }
1367         else if (tmp.indexOf("-") >= 0)
1368         {
1369           StringTokenizer st3 = new StringTokenizer(tmp, "-");
1370
1371           int start = (Integer.parseInt(st3.nextToken()));
1372           int end = (Integer.parseInt(st3.nextToken()));
1373
1374           if (end > start)
1375           {
1376             for (int i = start; i <= end; i++)
1377             {
1378               sg.addSequence(al.getSequenceAt(i - 1), false);
1379             }
1380           }
1381         }
1382         else
1383         {
1384           sg.addSequence(al.getSequenceAt(Integer.parseInt(tmp) - 1), false);
1385         }
1386       }
1387     }
1388
1389     if (refSeq != null)
1390     {
1391       sg.setStartRes(refSeq.findIndex(sg.getStartRes() + 1) - 1);
1392       sg.setEndRes(refSeq.findIndex(sg.getEndRes() + 1) - 1);
1393       sg.setSeqrep(refSeq);
1394     }
1395
1396     if (sg.getSize() > 0)
1397     {
1398       al.addGroup(sg);
1399     }
1400   }
1401
1402   void addRowProperties(AlignmentI al, StringTokenizer st)
1403   {
1404     String label = st.nextToken(), keyValue, key, value;
1405     boolean scaletofit = false, centerlab = false, showalllabs = false;
1406     while (st.hasMoreTokens())
1407     {
1408       keyValue = st.nextToken();
1409       key = keyValue.substring(0, keyValue.indexOf("="));
1410       value = keyValue.substring(keyValue.indexOf("=") + 1);
1411       if (key.equalsIgnoreCase("scaletofit"))
1412       {
1413         scaletofit = Boolean.valueOf(value).booleanValue();
1414       }
1415       if (key.equalsIgnoreCase("showalllabs"))
1416       {
1417         showalllabs = Boolean.valueOf(value).booleanValue();
1418       }
1419       if (key.equalsIgnoreCase("centrelabs"))
1420       {
1421         centerlab = Boolean.valueOf(value).booleanValue();
1422       }
1423       AlignmentAnnotation[] alr = al.getAlignmentAnnotation();
1424       if (alr != null)
1425       {
1426         for (int i = 0; i < alr.length; i++)
1427         {
1428           if (alr[i].label.equalsIgnoreCase(label))
1429           {
1430             alr[i].centreColLabels = centerlab;
1431             alr[i].scaleColLabel = scaletofit;
1432             alr[i].showAllColLabels = showalllabs;
1433           }
1434         }
1435       }
1436     }
1437   }
1438
1439   void addProperties(AlignmentI al, StringTokenizer st)
1440   {
1441
1442     // So far we have only added groups to the annotationHash,
1443     // the idea is in the future properties can be added to
1444     // alignments, other annotations etc
1445     if (al.getGroups() == null)
1446     {
1447       return;
1448     }
1449
1450     String name = st.nextToken();
1451     SequenceGroup sg = null;
1452     for (SequenceGroup _sg : al.getGroups())
1453     {
1454       if ((sg = _sg).getName().equals(name))
1455       {
1456         break;
1457       }
1458       else
1459       {
1460         sg = null;
1461       }
1462     }
1463
1464     if (sg != null)
1465     {
1466       String keyValue, key, value;
1467       ColourSchemeI def = sg.cs;
1468       sg.cs = null;
1469       while (st.hasMoreTokens())
1470       {
1471         keyValue = st.nextToken();
1472         key = keyValue.substring(0, keyValue.indexOf("="));
1473         value = keyValue.substring(keyValue.indexOf("=") + 1);
1474
1475         if (key.equalsIgnoreCase("description"))
1476         {
1477           sg.setDescription(value);
1478         }
1479         else if (key.equalsIgnoreCase("colour"))
1480         {
1481           sg.cs = ColourSchemeProperty.getColour(al, value);
1482         }
1483         else if (key.equalsIgnoreCase("pidThreshold"))
1484         {
1485           sg.cs.setThreshold(Integer.parseInt(value), true);
1486
1487         }
1488         else if (key.equalsIgnoreCase("consThreshold"))
1489         {
1490           sg.cs.setConservationInc(Integer.parseInt(value));
1491           Conservation c = new Conservation("Group",
1492                   ResidueProperties.propHash, 3, sg.getSequences(null),
1493                   sg.getStartRes(), sg.getEndRes() + 1);
1494
1495           c.calculate();
1496           c.verdict(false, 25); // TODO: refer to conservation percent threshold
1497
1498           sg.cs.setConservation(c);
1499
1500         }
1501         else if (key.equalsIgnoreCase("outlineColour"))
1502         {
1503           sg.setOutlineColour(new UserColourScheme(value).findColour('A'));
1504         }
1505         else if (key.equalsIgnoreCase("displayBoxes"))
1506         {
1507           sg.setDisplayBoxes(Boolean.valueOf(value).booleanValue());
1508         }
1509         else if (key.equalsIgnoreCase("showUnconserved"))
1510         {
1511           sg.setShowNonconserved(Boolean.valueOf(value).booleanValue());
1512         }
1513         else if (key.equalsIgnoreCase("displayText"))
1514         {
1515           sg.setDisplayText(Boolean.valueOf(value).booleanValue());
1516         }
1517         else if (key.equalsIgnoreCase("colourText"))
1518         {
1519           sg.setColourText(Boolean.valueOf(value).booleanValue());
1520         }
1521         else if (key.equalsIgnoreCase("textCol1"))
1522         {
1523           sg.textColour = new UserColourScheme(value).findColour('A');
1524         }
1525         else if (key.equalsIgnoreCase("textCol2"))
1526         {
1527           sg.textColour2 = new UserColourScheme(value).findColour('A');
1528         }
1529         else if (key.equalsIgnoreCase("textColThreshold"))
1530         {
1531           sg.thresholdTextColour = Integer.parseInt(value);
1532         }
1533         else if (key.equalsIgnoreCase("idColour"))
1534         {
1535           // consider warning if colour doesn't resolve to a real colour
1536           sg.setIdColour((def = new UserColourScheme(value))
1537                   .findColour('A'));
1538         }
1539         else if (key.equalsIgnoreCase("hide"))
1540         {
1541           // see bug https://mantis.lifesci.dundee.ac.uk/view.php?id=25847
1542           sg.setHidereps(true);
1543         }
1544         else if (key.equalsIgnoreCase("hidecols"))
1545         {
1546           // see bug https://mantis.lifesci.dundee.ac.uk/view.php?id=25847
1547           sg.setHideCols(true);
1548         }
1549         sg.recalcConservation();
1550       }
1551       if (sg.cs == null)
1552       {
1553         sg.cs = def;
1554       }
1555     }
1556   }
1557
1558   void setBelowAlignment(AlignmentI al, StringTokenizer st)
1559   {
1560     String token;
1561     AlignmentAnnotation aa, ala[] = al.getAlignmentAnnotation();
1562     if (ala == null)
1563     {
1564       System.err
1565               .print("Warning - no annotation to set below for sequence associated annotation:");
1566     }
1567     while (st.hasMoreTokens())
1568     {
1569       token = st.nextToken();
1570       if (ala == null)
1571       {
1572         System.err.print(" " + token);
1573       }
1574       else
1575       {
1576         for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1577         {
1578           aa = al.getAlignmentAnnotation()[i];
1579           if (aa.sequenceRef == refSeq && aa.label.equals(token))
1580           {
1581             aa.belowAlignment = true;
1582           }
1583         }
1584       }
1585     }
1586     if (ala == null)
1587     {
1588       System.err.print("\n");
1589     }
1590   }
1591
1592   void addAlignmentDetails(AlignmentI al, StringTokenizer st)
1593   {
1594     String keyValue, key, value;
1595     while (st.hasMoreTokens())
1596     {
1597       keyValue = st.nextToken();
1598       key = keyValue.substring(0, keyValue.indexOf("="));
1599       value = keyValue.substring(keyValue.indexOf("=") + 1);
1600       al.setProperty(key, value);
1601     }
1602   }
1603
1604   /**
1605    * Write annotations as a CSV file of the form 'label, value, value, ...' for
1606    * each row.
1607    * 
1608    * @param annotations
1609    * @return CSV file as a string.
1610    */
1611   public String printCSVAnnotations(AlignmentAnnotation[] annotations)
1612   {
1613     StringBuffer sp = new StringBuffer();
1614     for (int i = 0; i < annotations.length; i++)
1615     {
1616       String atos = annotations[i].toString();
1617       int p = 0;
1618       do
1619       {
1620         int cp = atos.indexOf("\n", p);
1621         sp.append(annotations[i].label);
1622         sp.append(",");
1623         if (cp > p)
1624         {
1625           sp.append(atos.substring(p, cp + 1));
1626         }
1627         else
1628         {
1629           sp.append(atos.substring(p));
1630           sp.append(newline);
1631         }
1632         p = cp + 1;
1633       } while (p > 0);
1634     }
1635     return sp.toString();
1636   }
1637 }