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