patch for JAL-1300 fixes JAL-1294: read and write COMBINE statements that use SEQUENC...
[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       }
342
343       text.append(newline);
344
345       text.append(colours.toString());
346       text.append(graphLine.toString());
347       if (graphGroup.size() > 0)
348       {
349         SequenceI oldRefSeq = refSeq;
350         SequenceGroup oldRefGroup = refGroup;
351         for (Map.Entry<Integer, String> combine_statement:graphGroup.entrySet())
352         {
353           Object[] seqRefAndGroup=graphGroup_refs.get(combine_statement.getKey());
354           
355           writeSequence_Ref(refSeq, (SequenceI)seqRefAndGroup[0]);
356           refSeq = (SequenceI)seqRefAndGroup[0];
357           
358           writeGroup_Ref(refGroup, (SequenceGroup)seqRefAndGroup[1]);
359           refGroup = (SequenceGroup)seqRefAndGroup[1];
360           text.append("COMBINE\t");
361           text.append(combine_statement.getValue());
362           text.append(newline);
363         }
364         writeSequence_Ref(refSeq, oldRefSeq);
365         refSeq = oldRefSeq;
366         
367         writeGroup_Ref(refGroup, oldRefGroup);
368         refGroup = oldRefGroup;
369       }
370       text.append(rowprops.toString());
371     }
372
373     if (list != null)
374     {
375       printGroups(list);
376     }
377
378     if (properties != null)
379     {
380       text.append(newline);
381       text.append(newline);
382       text.append("ALIGNMENT");
383       Enumeration en = properties.keys();
384       while (en.hasMoreElements())
385       {
386         String key = en.nextElement().toString();
387         text.append("\t");
388         text.append(key);
389         text.append("=");
390         text.append(properties.get(key));
391       }
392       // TODO: output alignment visualization settings here if required
393
394     }
395
396     return text.toString();
397   }
398
399   private Object writeGroup_Ref(SequenceGroup refGroup, SequenceGroup next_refGroup)
400   {
401     if (next_refGroup == null)
402     {
403
404       if (refGroup != null)
405       {
406         text.append(newline);
407         text.append("GROUP_REF\t");
408         text.append("ALIGNMENT");
409         text.append(newline);
410       }
411       return true;
412     }
413     else
414     {
415       if (refGroup == null || refGroup != next_refGroup)
416       {
417         text.append(newline);
418         text.append("GROUP_REF\t");
419         text.append(next_refGroup.getName());
420         text.append(newline);
421         return true;
422       }
423     }
424     return false;  
425   }
426   
427   private boolean writeSequence_Ref(SequenceI refSeq, SequenceI next_refSeq)
428   {
429
430     if (next_refSeq==null)
431     {
432       if (refSeq != null)
433       {
434         text.append(newline);
435         text.append("SEQUENCE_REF\t");
436         text.append("ALIGNMENT");
437         text.append(newline);
438         return true;
439       }
440     }
441     else
442     {
443       if (refSeq == null || refSeq != next_refSeq)
444       {
445         text.append(newline);
446         text.append("SEQUENCE_REF\t");
447         text.append(next_refSeq.getName());
448         text.append(newline);
449         return true;
450       }
451     }
452     return false;
453   }
454
455   public void printGroups(List<SequenceGroup> list)
456   {
457     SequenceI seqrep = null;
458     for (SequenceGroup sg : list)
459     {
460       if (!sg.hasSeqrep())
461       {
462         text.append("SEQUENCE_GROUP\t" + sg.getName() + "\t"
463                 + (sg.getStartRes() + 1) + "\t" + (sg.getEndRes() + 1)
464                 + "\t" + "-1\t");
465         seqrep = null;
466       }
467       else
468       {
469         seqrep = sg.getSeqrep();
470         text.append("SEQUENCE_REF\t");
471         text.append(seqrep.getName());
472         text.append(newline);
473         text.append("SEQUENCE_GROUP\t");
474         text.append(sg.getName());
475         text.append("\t");
476         text.append((seqrep.findPosition(sg.getStartRes())));
477         text.append("\t");
478         text.append((seqrep.findPosition(sg.getEndRes())));
479         text.append("\t");
480         text.append("-1\t");
481       }
482       for (int s = 0; s < sg.getSize(); s++)
483       {
484         text.append(sg.getSequenceAt(s).getName());
485         text.append("\t");
486       }
487       text.append(newline);
488       text.append("PROPERTIES\t");
489       text.append(sg.getName());
490       text.append("\t");
491
492       if (sg.getDescription() != null)
493       {
494         text.append("description=");
495         text.append(sg.getDescription());
496         text.append("\t");
497       }
498       if (sg.cs != null)
499       {
500         text.append("colour=");
501         text.append(ColourSchemeProperty.getColourName(sg.cs));
502         text.append("\t");
503         if (sg.cs.getThreshold() != 0)
504         {
505           text.append("pidThreshold=");
506           text.append(sg.cs.getThreshold());
507         }
508         if (sg.cs.conservationApplied())
509         {
510           text.append("consThreshold=");
511           text.append(sg.cs.getConservationInc());
512           text.append("\t");
513         }
514       }
515       text.append("outlineColour=");
516       text.append(jalview.util.Format.getHexString(sg.getOutlineColour()));
517       text.append("\t");
518
519       text.append("displayBoxes=");
520       text.append(sg.getDisplayBoxes());
521       text.append("\t");
522       text.append("displayText=");
523       text.append(sg.getDisplayText());
524       text.append("\t");
525       text.append("colourText=");
526       text.append(sg.getColourText());
527       text.append("\t");
528       text.append("showUnconserved=");
529       text.append(sg.getShowNonconserved());
530       text.append("\t");
531       if (sg.textColour != java.awt.Color.black)
532       {
533         text.append("textCol1=");
534         text.append(jalview.util.Format.getHexString(sg.textColour));
535         text.append("\t");
536       }
537       if (sg.textColour2 != java.awt.Color.white)
538       {
539         text.append("textCol2=");
540         text.append(jalview.util.Format.getHexString(sg.textColour2));
541         text.append("\t");
542       }
543       if (sg.thresholdTextColour != 0)
544       {
545         text.append("textColThreshold=");
546         text.append(sg.thresholdTextColour);
547         text.append("\t");
548       }
549       if (sg.idColour != null)
550       {
551         text.append("idColour=");
552         text.append(jalview.util.Format.getHexString(sg.idColour));
553         text.append("\t");
554       }
555       if (sg.isHidereps())
556       {
557         text.append("hide=true\t");
558       }
559       if (sg.isHideCols())
560       {
561         text.append("hidecols=true\t");
562       }
563       if (seqrep != null)
564       {
565         // terminate the last line and clear the sequence ref for the group
566         text.append(newline);
567         text.append("SEQUENCE_REF");
568       }
569       text.append(newline);
570       text.append(newline);
571
572     }
573   }
574
575   SequenceI refSeq = null;
576
577   String refSeqId = null;
578
579   public boolean readAnnotationFile(AlignmentI al, String file,
580           String protocol)
581   {
582     BufferedReader in = null;
583     try
584     {
585       if (protocol.equals(AppletFormatAdapter.FILE))
586       {
587         in = new BufferedReader(new FileReader(file));
588       }
589       else if (protocol.equals(AppletFormatAdapter.URL))
590       {
591         URL url = new URL(file);
592         in = new BufferedReader(new InputStreamReader(url.openStream()));
593       }
594       else if (protocol.equals(AppletFormatAdapter.PASTE))
595       {
596         in = new BufferedReader(new StringReader(file));
597       }
598       else if (protocol.equals(AppletFormatAdapter.CLASSLOADER))
599       {
600         java.io.InputStream is = getClass().getResourceAsStream("/" + file);
601         if (is != null)
602         {
603           in = new BufferedReader(new java.io.InputStreamReader(is));
604         }
605       }
606       if (in != null)
607       {
608         return parseAnnotationFrom(al, in);
609       }
610
611     } catch (Exception ex)
612     {
613       ex.printStackTrace();
614       System.out.println("Problem reading annotation file: " + ex);
615       if (nlinesread>0) {
616         System.out.println("Last read line "+nlinesread+": '"+lastread+"' (first 80 chars) ...");
617       }
618       return false;
619     }
620     return false;
621   }
622   long nlinesread=0;
623   String lastread="";
624   public boolean parseAnnotationFrom(AlignmentI al, BufferedReader in)
625           throws Exception
626   {
627     nlinesread = 0;
628     ArrayList<Object[]> combineAnnotation_calls = new ArrayList<Object[]>();
629     boolean modified = false;
630     String groupRef = null;
631     Hashtable groupRefRows = new Hashtable();
632
633     Hashtable autoAnnots = new Hashtable();
634     {
635       String line, label, description, token;
636       int graphStyle, index;
637       int refSeqIndex = 1;
638       int existingAnnotations = 0;
639       // when true - will add new rows regardless of whether they are duplicate
640       // auto-annotation like consensus or conservation graphs
641       boolean overrideAutoAnnot = false;
642       if (al.getAlignmentAnnotation() != null)
643       {
644         existingAnnotations = al.getAlignmentAnnotation().length;
645         if (existingAnnotations > 0)
646         {
647           AlignmentAnnotation[] aa = al.getAlignmentAnnotation();
648           for (int aai = 0; aai < aa.length; aai++)
649           {
650             if (aa[aai].autoCalculated)
651             {
652               // make a note of the name and description
653               autoAnnots.put(
654                       autoAnnotsKey(aa[aai], aa[aai].sequenceRef,
655                               (aa[aai].groupRef == null ? null
656                                       : aa[aai].groupRef.getName())),
657                       new Integer(1));
658             }
659           }
660         }
661       }
662
663       int alWidth = al.getWidth();
664
665       StringTokenizer st;
666       Annotation[] annotations;
667       AlignmentAnnotation annotation = null;
668
669       // First confirm this is an Annotation file
670       boolean jvAnnotationFile = false;
671       while ((line = in.readLine()) != null)
672       {
673         nlinesread++;lastread = new String(line);
674         if (line.indexOf("#") == 0)
675         {
676           continue;
677         }
678
679         if (line.indexOf("JALVIEW_ANNOTATION") > -1)
680         {
681           jvAnnotationFile = true;
682           break;
683         }
684       }
685
686       if (!jvAnnotationFile)
687       {
688         in.close();
689         return false;
690       }
691
692       while ((line = in.readLine()) != null)
693       {
694         nlinesread++;lastread = new String(line);
695         if (line.indexOf("#") == 0
696                 || line.indexOf("JALVIEW_ANNOTATION") > -1
697                 || line.length() == 0)
698         {
699           continue;
700         }
701
702         st = new StringTokenizer(line, "\t");
703         token = st.nextToken();
704         if (token.equalsIgnoreCase("COLOUR"))
705         {
706           // TODO: use graduated colour def'n here too
707           colourAnnotations(al, st.nextToken(), st.nextToken());
708           modified = true;
709           continue;
710         }
711
712         else if (token.equalsIgnoreCase("COMBINE"))
713         {
714           // keep a record of current state and resolve groupRef at end
715           combineAnnotation_calls.add(new Object[] { st, refSeq, groupRef});
716           modified = true;
717           continue;
718         }
719         else if (token.equalsIgnoreCase("ROWPROPERTIES"))
720         {
721           addRowProperties(al, st);
722           modified = true;
723           continue;
724         }
725         else if (token.equalsIgnoreCase("GRAPHLINE"))
726         {
727           addLine(al, st);
728           modified = true;
729           continue;
730         }
731
732         else if (token.equalsIgnoreCase("SEQUENCE_REF"))
733         {
734           if (st.hasMoreTokens())
735           {
736             refSeq = al.findName(refSeqId = st.nextToken());
737             if (refSeq == null)
738             {
739               refSeqId = null;
740             }
741             try
742             {
743               refSeqIndex = Integer.parseInt(st.nextToken());
744               if (refSeqIndex < 1)
745               {
746                 refSeqIndex = 1;
747                 System.out
748                         .println("WARNING: SEQUENCE_REF index must be > 0 in AnnotationFile");
749               }
750             } catch (Exception ex)
751             {
752               refSeqIndex = 1;
753             }
754           }
755           else
756           {
757             refSeq = null;
758             refSeqId = null;
759           }
760           continue;
761         }
762         else if (token.equalsIgnoreCase("GROUP_REF"))
763         {
764           // Group references could be forward or backwards, so they are
765           // resolved after the whole file is read in
766           groupRef = null;
767           if (st.hasMoreTokens())
768           {
769             groupRef = st.nextToken();
770             if (groupRef.length() < 1)
771             {
772               groupRef = null; // empty string
773             }
774             else
775             {
776               if (groupRefRows.get(groupRef) == null)
777               {
778                 groupRefRows.put(groupRef, new Vector());
779               }
780             }
781           }
782           continue;
783         }
784         else if (token.equalsIgnoreCase("SEQUENCE_GROUP"))
785         {
786           addGroup(al, st);
787           continue;
788         }
789
790         else if (token.equalsIgnoreCase("PROPERTIES"))
791         {
792           addProperties(al, st);
793           modified = true;
794           continue;
795         }
796
797         else if (token.equalsIgnoreCase("BELOW_ALIGNMENT"))
798         {
799           setBelowAlignment(al, st);
800           modified = true;
801           continue;
802         }
803         else if (token.equalsIgnoreCase("ALIGNMENT"))
804         {
805           addAlignmentDetails(al, st);
806           modified = true;
807           continue;
808         }
809
810         // Parse out the annotation row
811         graphStyle = AlignmentAnnotation.getGraphValueFromString(token);
812         label = st.nextToken();
813
814         index = 0;
815         annotations = new Annotation[alWidth];
816         description = null;
817         float score = Float.NaN;
818
819         if (st.hasMoreTokens())
820         {
821           line = st.nextToken();
822
823           if (line.indexOf("|") == -1)
824           {
825             description = line;
826             if (st.hasMoreTokens())
827               line = st.nextToken();
828           }
829
830           if (st.hasMoreTokens())
831           {
832             // This must be the score
833             score = Float.valueOf(st.nextToken()).floatValue();
834           }
835
836           st = new StringTokenizer(line, "|", true);
837
838           boolean emptyColumn = true;
839           boolean onlyOneElement = (st.countTokens() == 1);
840
841           while (st.hasMoreElements() && index < alWidth)
842           {
843             token = st.nextToken().trim();
844
845             if (onlyOneElement)
846             {
847               try
848               {
849                 score = Float.valueOf(token).floatValue();
850                 break;
851               } catch (NumberFormatException ex)
852               {
853               }
854             }
855
856             if (token.equals("|"))
857             {
858               if (emptyColumn)
859               {
860                 index++;
861               }
862
863               emptyColumn = true;
864             }
865             else
866             {
867               annotations[index++] = parseAnnotation(token, graphStyle);
868               emptyColumn = false;
869             }
870           }
871
872         }
873
874         annotation = new AlignmentAnnotation(label, description,
875                 (index == 0) ? null : annotations, 0, 0, graphStyle);
876
877         annotation.score = score;
878         if (!overrideAutoAnnot
879                 && autoAnnots.containsKey(autoAnnotsKey(annotation, refSeq,
880                         groupRef)))
881         {
882           // skip - we've already got an automatic annotation of this type.
883           continue;
884         }
885         // otherwise add it!
886         if (refSeq != null)
887         {
888
889           annotation.belowAlignment = false;
890           // make a copy of refSeq so we can find other matches in the alignment
891           SequenceI referedSeq = refSeq;
892           do
893           {
894             // copy before we do any mapping business.
895             // TODO: verify that undo/redo with 1:many sequence associated
896             // annotations can be undone correctly
897             AlignmentAnnotation ann = new AlignmentAnnotation(annotation);
898             annotation
899                     .createSequenceMapping(referedSeq, refSeqIndex, false);
900             annotation.adjustForAlignment();
901             referedSeq.addAlignmentAnnotation(annotation);
902             al.addAnnotation(annotation);
903             al.setAnnotationIndex(annotation,
904                     al.getAlignmentAnnotation().length
905                             - existingAnnotations - 1);
906             if (groupRef != null)
907             {
908               ((Vector) groupRefRows.get(groupRef)).addElement(annotation);
909             }
910             // and recover our virgin copy to use again if necessary.
911             annotation = ann;
912
913           } while (refSeqId != null
914                   && (referedSeq = al.findName(referedSeq, refSeqId, true)) != null);
915         }
916         else
917         {
918           al.addAnnotation(annotation);
919           al.setAnnotationIndex(annotation,
920                   al.getAlignmentAnnotation().length - existingAnnotations
921                           - 1);
922           if (groupRef != null)
923           {
924             ((Vector) groupRefRows.get(groupRef)).addElement(annotation);
925           }
926         }
927         // and set modification flag
928         modified = true;
929       }
930       // Resolve the groupRefs
931       Hashtable <String,SequenceGroup> groupRefLookup=new Hashtable<String,SequenceGroup>();
932       Enumeration en = groupRefRows.keys();
933
934       while (en.hasMoreElements())
935       {
936         groupRef = (String) en.nextElement();
937         boolean matched = false;
938         // Resolve group: TODO: add a getGroupByName method to alignments
939         for (SequenceGroup theGroup : al.getGroups())
940         {
941           if (theGroup.getName().equals(groupRef))
942           {
943             if (matched)
944             {
945               // TODO: specify and implement duplication of alignment annotation
946               // for multiple group references.
947               System.err
948                       .println("Ignoring 1:many group reference mappings for group name '"
949                               + groupRef + "'");
950             }
951             else
952             {
953               matched = true;
954               Vector rowset = (Vector) groupRefRows.get(groupRef);
955               groupRefLookup.put(groupRef,  theGroup);
956               if (rowset != null && rowset.size() > 0)
957               {
958                 AlignmentAnnotation alan = null;
959                 for (int elm = 0, elmSize = rowset.size(); elm < elmSize; elm++)
960                 {
961                   alan = (AlignmentAnnotation) rowset.elementAt(elm);
962                   alan.groupRef = theGroup;
963                 }
964               }
965             }
966           }
967         }
968         ((Vector) groupRefRows.get(groupRef)).removeAllElements();
969       }
970       // finally, combine all the annotation rows within each context.
971       /**
972        * number of combine statements in this annotation file. Used to create new groups for combined annotation graphs without disturbing existing ones
973        */
974       int combinecount = 0;
975       for (Object[] _combine_args:combineAnnotation_calls) {
976         combineAnnotations(al, 
977                 ++combinecount,
978                 (StringTokenizer) _combine_args[0], // st
979                 (SequenceI) _combine_args[1], // refSeq
980                 (_combine_args[2]==null) ? null : groupRefLookup.get((String)_combine_args[2]) // the reference group, or null
981                 );
982       }
983
984     }
985     return modified;
986   }
987
988   private Object autoAnnotsKey(AlignmentAnnotation annotation,
989           SequenceI refSeq, String groupRef)
990   {
991     return annotation.graph + "\t" + annotation.label + "\t"
992             + annotation.description + "\t"
993             + (refSeq != null ? refSeq.getDisplayId(true) : "");
994   }
995
996   Annotation parseAnnotation(String string, int graphStyle)
997   {
998     boolean hasSymbols = (graphStyle == AlignmentAnnotation.NO_GRAPH); // don't
999     // do the
1000     // glyph
1001     // test
1002     // if we
1003     // don't
1004     // want
1005     // secondary
1006     // structure
1007     String desc = null, displayChar = null;
1008     char ss = ' '; // secondaryStructure
1009     float value = 0;
1010     boolean parsedValue = false, dcset = false;
1011
1012     // find colour here
1013     java.awt.Color colour = null;
1014     int i = string.indexOf("[");
1015     int j = string.indexOf("]");
1016     if (i > -1 && j > -1)
1017     {
1018       UserColourScheme ucs = new UserColourScheme();
1019
1020       colour = ucs.getColourFromString(string.substring(i + 1, j));
1021       if (i > 0 && string.charAt(i - 1) == ',')
1022       {
1023         // clip the preceding comma as well
1024         i--;
1025       }
1026       string = string.substring(0, i) + string.substring(j + 1);
1027     }
1028
1029     StringTokenizer st = new StringTokenizer(string, ",", true);
1030     String token;
1031     boolean seenContent = false;
1032     int pass = 0;
1033     while (st.hasMoreTokens())
1034     {
1035       pass++;
1036       token = st.nextToken().trim();
1037       if (token.equals(","))
1038       {
1039         if (!seenContent && parsedValue && !dcset)
1040         {
1041           // allow the value below the bar/line to be empty
1042           dcset = true;
1043           displayChar = " ";
1044         }
1045         seenContent = false;
1046         continue;
1047       }
1048       else
1049       {
1050         seenContent = true;
1051       }
1052
1053       if (!parsedValue)
1054       {
1055         try
1056         {
1057           displayChar = token;
1058           // foo
1059           value = new Float(token).floatValue();
1060           parsedValue = true;
1061           continue;
1062         } catch (NumberFormatException ex)
1063         {
1064         }
1065       }
1066       else
1067       {
1068         if (token.length() == 1)
1069         {
1070           displayChar = token;
1071         }
1072       }
1073       if (hasSymbols
1074               && (token.equals("H") || token.equals("E")
1075                       || token.equals("S") || token.equals(" ")))
1076       {
1077         // Either this character represents a helix or sheet
1078         // or an integer which can be displayed
1079         ss = token.charAt(0);
1080         if (displayChar.equals(token.substring(0, 1)))
1081         {
1082           displayChar = "";
1083         }
1084       }
1085       else if (desc == null || (parsedValue && pass > 2))
1086       {
1087         desc = token;
1088       }
1089
1090     }
1091     // if (!dcset && string.charAt(string.length() - 1) == ',')
1092     // {
1093     // displayChar = " "; // empty display char symbol.
1094     // }
1095     if (displayChar != null && desc != null && desc.length() == 1)
1096     {
1097       if (displayChar.length() > 1)
1098       {
1099         // switch desc and displayChar - legacy support
1100         String tmp = displayChar;
1101         displayChar = desc;
1102         desc = tmp;
1103       }
1104       else
1105       {
1106         if (displayChar.equals(desc))
1107         {
1108           // duplicate label - hangover from the 'robust parser' above
1109           desc = null;
1110         }
1111       }
1112     }
1113     Annotation anot = new Annotation(displayChar, desc, ss, value);
1114
1115     anot.colour = colour;
1116
1117     return anot;
1118   }
1119
1120   void colourAnnotations(AlignmentI al, String label, String colour)
1121   {
1122     UserColourScheme ucs = new UserColourScheme(colour);
1123     Annotation[] annotations;
1124     for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1125     {
1126       if (al.getAlignmentAnnotation()[i].label.equalsIgnoreCase(label))
1127       {
1128         annotations = al.getAlignmentAnnotation()[i].annotations;
1129         for (int j = 0; j < annotations.length; j++)
1130         {
1131           if (annotations[j] != null)
1132           {
1133             annotations[j].colour = ucs.findColour('A');
1134           }
1135         }
1136       }
1137     }
1138   }
1139
1140   void combineAnnotations(AlignmentI al, int combineCount, StringTokenizer st, SequenceI seqRef, SequenceGroup groupRef)
1141   {
1142     String group = st.nextToken();
1143     // First make sure we are not overwriting the graphIndex
1144     int graphGroup=0;
1145     if (al.getAlignmentAnnotation() != null)
1146     {
1147       for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1148       {
1149         AlignmentAnnotation aa = al.getAlignmentAnnotation()[i];
1150         
1151         if (aa.graphGroup>graphGroup)
1152         {
1153           // try to number graphGroups in order of occurence.
1154           graphGroup=aa.graphGroup+1;
1155         }
1156         if (aa.sequenceRef==seqRef && aa.groupRef==groupRef && aa.label.equalsIgnoreCase(group))
1157         {
1158           if (aa.graphGroup>-1)
1159           {
1160             graphGroup = aa.graphGroup;
1161           } else {
1162             if (graphGroup <= combineCount)
1163             {
1164               graphGroup=combineCount+1;
1165             }
1166             aa.graphGroup = graphGroup;
1167           }
1168           break;
1169         }
1170       }
1171
1172       // Now update groups
1173       while (st.hasMoreTokens())
1174       {
1175         group = st.nextToken();
1176         for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1177         {
1178           AlignmentAnnotation aa = al.getAlignmentAnnotation()[i];
1179           if (aa.sequenceRef==seqRef && aa.groupRef==groupRef && aa.label.equalsIgnoreCase(group))
1180           {
1181             aa.graphGroup = graphGroup;
1182             break;
1183           }
1184         }
1185       }
1186     }
1187     else
1188     {
1189       System.err
1190               .println("Couldn't combine annotations. None are added to alignment yet!");
1191     }
1192   }
1193
1194   void addLine(AlignmentI al, StringTokenizer st)
1195   {
1196     String group = st.nextToken();
1197     AlignmentAnnotation annotation = null, alannot[] = al
1198             .getAlignmentAnnotation();
1199     if (alannot != null)
1200     {
1201       for (int i = 0; i < alannot.length; i++)
1202       {
1203         if (alannot[i].label.equalsIgnoreCase(group))
1204         {
1205           annotation = alannot[i];
1206           break;
1207         }
1208       }
1209     }
1210     if (annotation == null)
1211     {
1212       return;
1213     }
1214     float value = new Float(st.nextToken()).floatValue();
1215     String label = st.hasMoreTokens() ? st.nextToken() : null;
1216     java.awt.Color colour = null;
1217     if (st.hasMoreTokens())
1218     {
1219       UserColourScheme ucs = new UserColourScheme(st.nextToken());
1220       colour = ucs.findColour('A');
1221     }
1222
1223     annotation.setThreshold(new GraphLine(value, label, colour));
1224   }
1225
1226   void addGroup(AlignmentI al, StringTokenizer st)
1227   {
1228     SequenceGroup sg = new SequenceGroup();
1229     sg.setName(st.nextToken());
1230     String rng = "";
1231     try
1232     {
1233       rng = st.nextToken();
1234       if (rng.length() > 0 && !rng.startsWith("*"))
1235       {
1236         sg.setStartRes(Integer.parseInt(rng) - 1);
1237       }
1238       else
1239       {
1240         sg.setStartRes(0);
1241       }
1242       rng = st.nextToken();
1243       if (rng.length() > 0 && !rng.startsWith("*"))
1244       {
1245         sg.setEndRes(Integer.parseInt(rng) - 1);
1246       }
1247       else
1248       {
1249         sg.setEndRes(al.getWidth() - 1);
1250       }
1251     } catch (Exception e)
1252     {
1253       System.err
1254               .println("Couldn't parse Group Start or End Field as '*' or a valid column or sequence index: '"
1255                       + rng + "' - assuming alignment width for group.");
1256       // assume group is full width
1257       sg.setStartRes(0);
1258       sg.setEndRes(al.getWidth() - 1);
1259     }
1260
1261     String index = st.nextToken();
1262     if (index.equals("-1"))
1263     {
1264       while (st.hasMoreElements())
1265       {
1266         sg.addSequence(al.findName(st.nextToken()), false);
1267       }
1268     }
1269     else
1270     {
1271       StringTokenizer st2 = new StringTokenizer(index, ",");
1272
1273       while (st2.hasMoreTokens())
1274       {
1275         String tmp = st2.nextToken();
1276         if (tmp.equals("*"))
1277         {
1278           for (int i = 0; i < al.getHeight(); i++)
1279           {
1280             sg.addSequence(al.getSequenceAt(i), false);
1281           }
1282         }
1283         else if (tmp.indexOf("-") >= 0)
1284         {
1285           StringTokenizer st3 = new StringTokenizer(tmp, "-");
1286
1287           int start = (Integer.parseInt(st3.nextToken()));
1288           int end = (Integer.parseInt(st3.nextToken()));
1289
1290           if (end > start)
1291           {
1292             for (int i = start; i <= end; i++)
1293             {
1294               sg.addSequence(al.getSequenceAt(i - 1), false);
1295             }
1296           }
1297         }
1298         else
1299         {
1300           sg.addSequence(al.getSequenceAt(Integer.parseInt(tmp) - 1), false);
1301         }
1302       }
1303     }
1304
1305     if (refSeq != null)
1306     {
1307       sg.setStartRes(refSeq.findIndex(sg.getStartRes() + 1) - 1);
1308       sg.setEndRes(refSeq.findIndex(sg.getEndRes() + 1) - 1);
1309       sg.setSeqrep(refSeq);
1310     }
1311
1312     if (sg.getSize() > 0)
1313     {
1314       al.addGroup(sg);
1315     }
1316   }
1317
1318   void addRowProperties(AlignmentI al, StringTokenizer st)
1319   {
1320     String label = st.nextToken(), keyValue, key, value;
1321     boolean scaletofit = false, centerlab = false, showalllabs = false;
1322     while (st.hasMoreTokens())
1323     {
1324       keyValue = st.nextToken();
1325       key = keyValue.substring(0, keyValue.indexOf("="));
1326       value = keyValue.substring(keyValue.indexOf("=") + 1);
1327       if (key.equalsIgnoreCase("scaletofit"))
1328       {
1329         scaletofit = Boolean.valueOf(value).booleanValue();
1330       }
1331       if (key.equalsIgnoreCase("showalllabs"))
1332       {
1333         showalllabs = Boolean.valueOf(value).booleanValue();
1334       }
1335       if (key.equalsIgnoreCase("centrelabs"))
1336       {
1337         centerlab = Boolean.valueOf(value).booleanValue();
1338       }
1339       AlignmentAnnotation[] alr = al.getAlignmentAnnotation();
1340       if (alr != null)
1341       {
1342         for (int i = 0; i < alr.length; i++)
1343         {
1344           if (alr[i].label.equalsIgnoreCase(label))
1345           {
1346             alr[i].centreColLabels = centerlab;
1347             alr[i].scaleColLabel = scaletofit;
1348             alr[i].showAllColLabels = showalllabs;
1349           }
1350         }
1351       }
1352     }
1353   }
1354
1355   void addProperties(AlignmentI al, StringTokenizer st)
1356   {
1357
1358     // So far we have only added groups to the annotationHash,
1359     // the idea is in the future properties can be added to
1360     // alignments, other annotations etc
1361     if (al.getGroups() == null)
1362     {
1363       return;
1364     }
1365
1366     String name = st.nextToken();
1367     SequenceGroup sg = null;
1368     for (SequenceGroup _sg : al.getGroups())
1369     {
1370       if ((sg = _sg).getName().equals(name))
1371       {
1372         break;
1373       }
1374       else
1375       {
1376         sg = null;
1377       }
1378     }
1379
1380     if (sg != null)
1381     {
1382       String keyValue, key, value;
1383       ColourSchemeI def = sg.cs;
1384       sg.cs = null;
1385       while (st.hasMoreTokens())
1386       {
1387         keyValue = st.nextToken();
1388         key = keyValue.substring(0, keyValue.indexOf("="));
1389         value = keyValue.substring(keyValue.indexOf("=") + 1);
1390
1391         if (key.equalsIgnoreCase("description"))
1392         {
1393           sg.setDescription(value);
1394         }
1395         else if (key.equalsIgnoreCase("colour"))
1396         {
1397           sg.cs = ColourSchemeProperty.getColour(al, value);
1398         }
1399         else if (key.equalsIgnoreCase("pidThreshold"))
1400         {
1401           sg.cs.setThreshold(Integer.parseInt(value), true);
1402
1403         }
1404         else if (key.equalsIgnoreCase("consThreshold"))
1405         {
1406           sg.cs.setConservationInc(Integer.parseInt(value));
1407           Conservation c = new Conservation("Group",
1408                   ResidueProperties.propHash, 3, sg.getSequences(null),
1409                   sg.getStartRes(), sg.getEndRes() + 1);
1410
1411           c.calculate();
1412           c.verdict(false, 25); // TODO: refer to conservation percent threshold
1413
1414           sg.cs.setConservation(c);
1415
1416         }
1417         else if (key.equalsIgnoreCase("outlineColour"))
1418         {
1419           sg.setOutlineColour(new UserColourScheme(value).findColour('A'));
1420         }
1421         else if (key.equalsIgnoreCase("displayBoxes"))
1422         {
1423           sg.setDisplayBoxes(Boolean.valueOf(value).booleanValue());
1424         }
1425         else if (key.equalsIgnoreCase("showUnconserved"))
1426         {
1427           sg.setShowNonconserved(Boolean.valueOf(value).booleanValue());
1428         }
1429         else if (key.equalsIgnoreCase("displayText"))
1430         {
1431           sg.setDisplayText(Boolean.valueOf(value).booleanValue());
1432         }
1433         else if (key.equalsIgnoreCase("colourText"))
1434         {
1435           sg.setColourText(Boolean.valueOf(value).booleanValue());
1436         }
1437         else if (key.equalsIgnoreCase("textCol1"))
1438         {
1439           sg.textColour = new UserColourScheme(value).findColour('A');
1440         }
1441         else if (key.equalsIgnoreCase("textCol2"))
1442         {
1443           sg.textColour2 = new UserColourScheme(value).findColour('A');
1444         }
1445         else if (key.equalsIgnoreCase("textColThreshold"))
1446         {
1447           sg.thresholdTextColour = Integer.parseInt(value);
1448         }
1449         else if (key.equalsIgnoreCase("idColour"))
1450         {
1451           // consider warning if colour doesn't resolve to a real colour
1452           sg.setIdColour((def = new UserColourScheme(value))
1453                   .findColour('A'));
1454         }
1455         else if (key.equalsIgnoreCase("hide"))
1456         {
1457           // see bug https://mantis.lifesci.dundee.ac.uk/view.php?id=25847
1458           sg.setHidereps(true);
1459         }
1460         else if (key.equalsIgnoreCase("hidecols"))
1461         {
1462           // see bug https://mantis.lifesci.dundee.ac.uk/view.php?id=25847
1463           sg.setHideCols(true);
1464         }
1465         sg.recalcConservation();
1466       }
1467       if (sg.cs == null)
1468       {
1469         sg.cs = def;
1470       }
1471     }
1472   }
1473
1474   void setBelowAlignment(AlignmentI al, StringTokenizer st)
1475   {
1476     String token;
1477     AlignmentAnnotation aa, ala[] = al.getAlignmentAnnotation();
1478     if (ala == null)
1479     {
1480       System.err
1481               .print("Warning - no annotation to set below for sequence associated annotation:");
1482     }
1483     while (st.hasMoreTokens())
1484     {
1485       token = st.nextToken();
1486       if (ala == null)
1487       {
1488         System.err.print(" " + token);
1489       }
1490       else
1491       {
1492         for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
1493         {
1494           aa = al.getAlignmentAnnotation()[i];
1495           if (aa.sequenceRef == refSeq && aa.label.equals(token))
1496           {
1497             aa.belowAlignment = true;
1498           }
1499         }
1500       }
1501     }
1502     if (ala == null)
1503     {
1504       System.err.print("\n");
1505     }
1506   }
1507
1508   void addAlignmentDetails(AlignmentI al, StringTokenizer st)
1509   {
1510     String keyValue, key, value;
1511     while (st.hasMoreTokens())
1512     {
1513       keyValue = st.nextToken();
1514       key = keyValue.substring(0, keyValue.indexOf("="));
1515       value = keyValue.substring(keyValue.indexOf("=") + 1);
1516       al.setProperty(key, value);
1517     }
1518   }
1519
1520   /**
1521    * Write annotations as a CSV file of the form 'label, value, value, ...' for
1522    * each row.
1523    * 
1524    * @param annotations
1525    * @return CSV file as a string.
1526    */
1527   public String printCSVAnnotations(AlignmentAnnotation[] annotations)
1528   {
1529     StringBuffer sp = new StringBuffer();
1530     for (int i = 0; i < annotations.length; i++)
1531     {
1532       String atos = annotations[i].toString();
1533       int p = 0;
1534       do
1535       {
1536         int cp = atos.indexOf("\n", p);
1537         sp.append(annotations[i].label);
1538         sp.append(",");
1539         if (cp > p)
1540         {
1541           sp.append(atos.substring(p, cp + 1));
1542         }
1543         else
1544         {
1545           sp.append(atos.substring(p));
1546           sp.append(newline);
1547         }
1548         p = cp + 1;
1549       } while (p > 0);
1550     }
1551     return sp.toString();
1552   }
1553 }