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