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