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