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