JAL-2388 Hidden cols separated from column selection (almost complete)
[jalview.git] / src / jalview / datamodel / AlignmentAnnotation.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ 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.datamodel;
22
23 import jalview.analysis.Rna;
24 import jalview.analysis.SecStrConsensus.SimpleBP;
25 import jalview.analysis.WUSSParseException;
26
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Map.Entry;
34
35 /**
36  * DOCUMENT ME!
37  * 
38  * @author $author$
39  * @version $Revision$
40  */
41 public class AlignmentAnnotation
42 {
43   private static final String ANNOTATION_ID_PREFIX = "ann";
44
45   /*
46    * Identifers for different types of profile data
47    */
48   public static final int SEQUENCE_PROFILE = 0;
49
50   public static final int STRUCTURE_PROFILE = 1;
51
52   public static final int CDNA_PROFILE = 2;
53
54   private static long counter = 0;
55
56   /**
57    * If true, this annotations is calculated every edit, eg consensus, quality
58    * or conservation graphs
59    */
60   public boolean autoCalculated = false;
61
62   /**
63    * unique ID for this annotation, used to match up the same annotation row
64    * shown in multiple views and alignments
65    */
66   public String annotationId;
67
68   /**
69    * the sequence this annotation is associated with (or null)
70    */
71   public SequenceI sequenceRef;
72
73   /** label shown in dropdown menus and in the annotation label area */
74   public String label;
75
76   /** longer description text shown as a tooltip */
77   public String description;
78
79   /** Array of annotations placed in the current coordinate system */
80   public Annotation[] annotations;
81
82   public List<SimpleBP> bps = null;
83
84   /**
85    * RNA secondary structure contact positions
86    */
87   public SequenceFeature[] _rnasecstr = null;
88
89   /**
90    * position of annotation resulting in invalid WUSS parsing or -1. -2 means
91    * there was no RNA structure in this annotation
92    */
93   private long invalidrnastruc = -2;
94
95   /**
96    * Updates the _rnasecstr field Determines the positions that base pair and
97    * the positions of helices based on secondary structure from a Stockholm file
98    * 
99    * @param RNAannot
100    */
101   private void _updateRnaSecStr(CharSequence RNAannot)
102   {
103     try
104     {
105       bps = Rna.getModeleBP(RNAannot);
106       _rnasecstr = Rna.getBasePairs(bps);
107       invalidrnastruc = -1;
108     } catch (WUSSParseException px)
109     {
110       // DEBUG System.out.println(px);
111       invalidrnastruc = px.getProblemPos();
112     }
113     if (invalidrnastruc > -1)
114     {
115       return;
116     }
117     Rna.HelixMap(_rnasecstr);
118     // setRNAStruc(RNAannot);
119
120     if (_rnasecstr != null && _rnasecstr.length > 0)
121     {
122       // show all the RNA secondary structure annotation symbols.
123       isrna = true;
124       showAllColLabels = true;
125       scaleColLabel = true;
126       _markRnaHelices();
127     }
128     // System.out.println("featuregroup " + _rnasecstr[0].getFeatureGroup());
129
130   }
131
132   private void _markRnaHelices()
133   {
134     int mxval = 0;
135     // Figure out number of helices
136     // Length of rnasecstr is the number of pairs of positions that base pair
137     // with each other in the secondary structure
138     for (int x = 0; x < _rnasecstr.length; x++)
139     {
140
141       /*
142        * System.out.println(this.annotation._rnasecstr[x] + " Begin" +
143        * this.annotation._rnasecstr[x].getBegin());
144        */
145       // System.out.println(this.annotation._rnasecstr[x].getFeatureGroup());
146       int val = 0;
147       try
148       {
149         val = Integer.valueOf(_rnasecstr[x].getFeatureGroup());
150         if (mxval < val)
151         {
152           mxval = val;
153         }
154       } catch (NumberFormatException q)
155       {
156       }
157       ;
158
159       annotations[_rnasecstr[x].getBegin()].value = val;
160       annotations[_rnasecstr[x].getEnd()].value = val;
161
162       // annotations[_rnasecstr[x].getBegin()].displayCharacter = "" + val;
163       // annotations[_rnasecstr[x].getEnd()].displayCharacter = "" + val;
164     }
165     setScore(mxval);
166   }
167
168   /**
169    * map of positions in the associated annotation
170    */
171   private Map<Integer, Annotation> sequenceMapping;
172
173   /**
174    * lower range for quantitative data
175    */
176   public float graphMin;
177
178   /**
179    * Upper range for quantitative data
180    */
181   public float graphMax;
182
183   /**
184    * Score associated with label and description.
185    */
186   public double score = Double.NaN;
187
188   /**
189    * flag indicating if annotation has a score.
190    */
191   public boolean hasScore = false;
192
193   public GraphLine threshold;
194
195   // Graphical hints and tips
196
197   /** Can this row be edited by the user ? */
198   public boolean editable = false;
199
200   /** Indicates if annotation has a graphical symbol track */
201   public boolean hasIcons; //
202
203   /** Indicates if annotation has a text character label */
204   public boolean hasText;
205
206   /** is the row visible */
207   public boolean visible = true;
208
209   public int graphGroup = -1;
210
211   /** Displayed height of row in pixels */
212   public int height = 0;
213
214   public int graph = 0;
215
216   public int graphHeight = 40;
217
218   public boolean padGaps = false;
219
220   public static final int NO_GRAPH = 0;
221
222   public static final int BAR_GRAPH = 1;
223
224   public static final int LINE_GRAPH = 2;
225
226   public boolean belowAlignment = true;
227
228   public SequenceGroup groupRef = null;
229
230   /**
231    * display every column label, even if there is a row of identical labels
232    */
233   public boolean showAllColLabels = false;
234
235   /**
236    * scale the column label to fit within the alignment column.
237    */
238   public boolean scaleColLabel = false;
239
240   /**
241    * centre the column labels relative to the alignment column
242    */
243   public boolean centreColLabels = false;
244
245   private boolean isrna;
246
247   /*
248    * (non-Javadoc)
249    * 
250    * @see java.lang.Object#finalize()
251    */
252   @Override
253   protected void finalize() throws Throwable
254   {
255     sequenceRef = null;
256     groupRef = null;
257     super.finalize();
258   }
259
260   public static int getGraphValueFromString(String string)
261   {
262     if (string.equalsIgnoreCase("BAR_GRAPH"))
263     {
264       return BAR_GRAPH;
265     }
266     else if (string.equalsIgnoreCase("LINE_GRAPH"))
267     {
268       return LINE_GRAPH;
269     }
270     else
271     {
272       return NO_GRAPH;
273     }
274   }
275
276   // JBPNote: what does this do ?
277   public void ConcenStru(CharSequence RNAannot) throws WUSSParseException
278   {
279     bps = Rna.getModeleBP(RNAannot);
280   }
281
282   /**
283    * Creates a new AlignmentAnnotation object.
284    * 
285    * @param label
286    *          short label shown under sequence labels
287    * @param description
288    *          text displayed on mouseover
289    * @param annotations
290    *          set of positional annotation elements
291    */
292   public AlignmentAnnotation(String label, String description,
293           Annotation[] annotations)
294   {
295     setAnnotationId();
296     // always editable?
297     editable = true;
298     this.label = label;
299     this.description = description;
300     this.annotations = annotations;
301
302     validateRangeAndDisplay();
303   }
304
305   /**
306    * Checks if annotation labels represent secondary structures
307    * 
308    */
309   void areLabelsSecondaryStructure()
310   {
311     boolean nonSSLabel = false;
312     isrna = false;
313     StringBuffer rnastring = new StringBuffer();
314
315     char firstChar = 0;
316     for (int i = 0; i < annotations.length; i++)
317     {
318       if (annotations[i] == null)
319       {
320         continue;
321       }
322       if (annotations[i].secondaryStructure == 'H'
323               || annotations[i].secondaryStructure == 'E')
324       {
325         hasIcons |= true;
326       }
327       else
328       // Check for RNA secondary structure
329       {
330         // System.out.println(annotations[i].secondaryStructure);
331         // TODO: 2.8.2 should this ss symbol validation check be a function in
332         // RNA/ResidueProperties ?
333         if (annotations[i].secondaryStructure == '('
334                 || annotations[i].secondaryStructure == '['
335                 || annotations[i].secondaryStructure == '<'
336                 || annotations[i].secondaryStructure == '{'
337                 || annotations[i].secondaryStructure == 'A'
338                 || annotations[i].secondaryStructure == 'B'
339                 || annotations[i].secondaryStructure == 'C'
340                 || annotations[i].secondaryStructure == 'D'
341                 || annotations[i].secondaryStructure == 'E'
342                 || annotations[i].secondaryStructure == 'F'
343                 || annotations[i].secondaryStructure == 'G'
344                 || annotations[i].secondaryStructure == 'H'
345                 || annotations[i].secondaryStructure == 'I'
346                 || annotations[i].secondaryStructure == 'J'
347                 || annotations[i].secondaryStructure == 'K'
348                 || annotations[i].secondaryStructure == 'L'
349                 || annotations[i].secondaryStructure == 'M'
350                 || annotations[i].secondaryStructure == 'N'
351                 || annotations[i].secondaryStructure == 'O'
352                 || annotations[i].secondaryStructure == 'P'
353                 || annotations[i].secondaryStructure == 'Q'
354                 || annotations[i].secondaryStructure == 'R'
355                 || annotations[i].secondaryStructure == 'S'
356                 || annotations[i].secondaryStructure == 'T'
357                 || annotations[i].secondaryStructure == 'U'
358                 || annotations[i].secondaryStructure == 'V'
359                 || annotations[i].secondaryStructure == 'W'
360                 || annotations[i].secondaryStructure == 'X'
361                 || annotations[i].secondaryStructure == 'Y'
362                 || annotations[i].secondaryStructure == 'Z')
363         {
364           hasIcons |= true;
365           isrna |= true;
366         }
367       }
368
369       // System.out.println("displaychar " + annotations[i].displayCharacter);
370
371       if (annotations[i].displayCharacter == null
372               || annotations[i].displayCharacter.length() == 0)
373       {
374         rnastring.append('.');
375         continue;
376       }
377       if (annotations[i].displayCharacter.length() == 1)
378       {
379         firstChar = annotations[i].displayCharacter.charAt(0);
380         // check to see if it looks like a sequence or is secondary structure
381         // labelling.
382         if (annotations[i].secondaryStructure != ' '
383                 && !hasIcons
384                 &&
385                 // Uncomment to only catch case where
386                 // displayCharacter==secondary
387                 // Structure
388                 // to correctly redisplay SS annotation imported from Stockholm,
389                 // exported to JalviewXML and read back in again.
390                 // &&
391                 // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
392                 firstChar != ' '
393                 && firstChar != '$'
394                 && firstChar != 0xCE
395                 && firstChar != '('
396                 && firstChar != '['
397                 && firstChar != '>'
398                 && firstChar != '{'
399                 && firstChar != 'A'
400                 && firstChar != 'B'
401                 && firstChar != 'C'
402                 && firstChar != 'D'
403                 && firstChar != 'E'
404                 && firstChar != 'F'
405                 && firstChar != 'G'
406                 && firstChar != 'H'
407                 && firstChar != 'I'
408                 && firstChar != 'J'
409                 && firstChar != 'K'
410                 && firstChar != 'L'
411                 && firstChar != 'M'
412                 && firstChar != 'N'
413                 && firstChar != 'O'
414                 && firstChar != 'P'
415                 && firstChar != 'Q'
416                 && firstChar != 'R'
417                 && firstChar != 'S'
418                 && firstChar != 'T'
419                 && firstChar != 'U'
420                 && firstChar != 'V'
421                 && firstChar != 'W'
422                 && firstChar != 'X'
423                 && firstChar != 'Y'
424                 && firstChar != 'Z'
425                 && firstChar != '-'
426                 && firstChar < jalview.schemes.ResidueProperties.aaIndex.length)
427         {
428           if (jalview.schemes.ResidueProperties.aaIndex[firstChar] < 23) // TODO:
429                                                                          // parameterise
430                                                                          // to
431                                                                          // gap
432                                                                          // symbol
433                                                                          // number
434           {
435             nonSSLabel = true;
436           }
437         }
438       }
439       else
440       {
441         rnastring.append(annotations[i].displayCharacter.charAt(1));
442       }
443
444       if (annotations[i].displayCharacter.length() > 0)
445       {
446         hasText = true;
447       }
448     }
449
450     if (nonSSLabel)
451     {
452       hasIcons = false;
453       for (int j = 0; j < annotations.length; j++)
454       {
455         if (annotations[j] != null
456                 && annotations[j].secondaryStructure != ' ')
457         {
458           annotations[j].displayCharacter = String
459                   .valueOf(annotations[j].secondaryStructure);
460           annotations[j].secondaryStructure = ' ';
461         }
462
463       }
464     }
465     else
466     {
467       if (isrna)
468       {
469         _updateRnaSecStr(new AnnotCharSequence());
470       }
471     }
472   }
473
474   /**
475    * flyweight access to positions in the alignment annotation row for RNA
476    * processing
477    * 
478    * @author jimp
479    * 
480    */
481   private class AnnotCharSequence implements CharSequence
482   {
483     int offset = 0;
484
485     int max = 0;
486
487     public AnnotCharSequence()
488     {
489       this(0, annotations.length);
490     }
491
492     AnnotCharSequence(int start, int end)
493     {
494       offset = start;
495       max = end;
496     }
497
498     @Override
499     public CharSequence subSequence(int start, int end)
500     {
501       return new AnnotCharSequence(offset + start, offset + end);
502     }
503
504     @Override
505     public int length()
506     {
507       return max - offset;
508     }
509
510     @Override
511     public char charAt(int index)
512     {
513       return ((index + offset < 0) || (index + offset) >= max
514               || annotations[index + offset] == null
515               || (annotations[index + offset].secondaryStructure <= ' ') ? ' '
516               : annotations[index + offset].displayCharacter == null
517                       || annotations[index + offset].displayCharacter
518                               .length() == 0 ? annotations[index + offset].secondaryStructure
519                       : annotations[index + offset].displayCharacter
520                               .charAt(0));
521     }
522
523     @Override
524     public String toString()
525     {
526       char[] string = new char[max - offset];
527       int mx = annotations.length;
528
529       for (int i = offset; i < mx; i++)
530       {
531         string[i] = (annotations[i] == null || (annotations[i].secondaryStructure <= 32)) ? ' '
532                 : (annotations[i].displayCharacter == null
533                         || annotations[i].displayCharacter.length() == 0 ? annotations[i].secondaryStructure
534                         : annotations[i].displayCharacter.charAt(0));
535       }
536       return new String(string);
537     }
538   };
539
540   private long _lastrnaannot = -1;
541
542   public String getRNAStruc()
543   {
544     if (isrna)
545     {
546       String rnastruc = new AnnotCharSequence().toString();
547       if (_lastrnaannot != rnastruc.hashCode())
548       {
549         // ensure rna structure contacts are up to date
550         _lastrnaannot = rnastruc.hashCode();
551         _updateRnaSecStr(rnastruc);
552       }
553       return rnastruc;
554     }
555     return null;
556   }
557
558   /**
559    * Creates a new AlignmentAnnotation object.
560    * 
561    * @param label
562    *          DOCUMENT ME!
563    * @param description
564    *          DOCUMENT ME!
565    * @param annotations
566    *          DOCUMENT ME!
567    * @param min
568    *          DOCUMENT ME!
569    * @param max
570    *          DOCUMENT ME!
571    * @param winLength
572    *          DOCUMENT ME!
573    */
574   public AlignmentAnnotation(String label, String description,
575           Annotation[] annotations, float min, float max, int graphType)
576   {
577     setAnnotationId();
578     // graphs are not editable
579     editable = graphType == 0;
580
581     this.label = label;
582     this.description = description;
583     this.annotations = annotations;
584     graph = graphType;
585     graphMin = min;
586     graphMax = max;
587     validateRangeAndDisplay();
588   }
589
590   /**
591    * checks graphMin and graphMax, secondary structure symbols, sets graphType
592    * appropriately, sets null labels to the empty string if appropriate.
593    */
594   public void validateRangeAndDisplay()
595   {
596
597     if (annotations == null)
598     {
599       visible = false; // try to prevent renderer from displaying.
600       invalidrnastruc = -1;
601       return; // this is a non-annotation row annotation - ie a sequence score.
602     }
603
604     int graphType = graph;
605     float min = graphMin;
606     float max = graphMax;
607     boolean drawValues = true;
608     _linecolour = null;
609     if (min == max)
610     {
611       min = 999999999;
612       for (int i = 0; i < annotations.length; i++)
613       {
614         if (annotations[i] == null)
615         {
616           continue;
617         }
618
619         if (drawValues && annotations[i].displayCharacter != null
620                 && annotations[i].displayCharacter.length() > 1)
621         {
622           drawValues = false;
623         }
624
625         if (annotations[i].value > max)
626         {
627           max = annotations[i].value;
628         }
629
630         if (annotations[i].value < min)
631         {
632           min = annotations[i].value;
633         }
634         if (_linecolour == null && annotations[i].colour != null)
635         {
636           _linecolour = annotations[i].colour;
637         }
638       }
639       // ensure zero is origin for min/max ranges on only one side of zero
640       if (min > 0)
641       {
642         min = 0;
643       }
644       else
645       {
646         if (max < 0)
647         {
648           max = 0;
649         }
650       }
651     }
652
653     graphMin = min;
654     graphMax = max;
655
656     areLabelsSecondaryStructure();
657
658     if (!drawValues && graphType != NO_GRAPH)
659     {
660       for (int i = 0; i < annotations.length; i++)
661       {
662         if (annotations[i] != null)
663         {
664           annotations[i].displayCharacter = "";
665         }
666       }
667     }
668   }
669
670   /**
671    * Copy constructor creates a new independent annotation row with the same
672    * associated sequenceRef
673    * 
674    * @param annotation
675    */
676   public AlignmentAnnotation(AlignmentAnnotation annotation)
677   {
678     setAnnotationId();
679     this.label = new String(annotation.label);
680     if (annotation.description != null)
681     {
682       this.description = new String(annotation.description);
683     }
684     this.graphMin = annotation.graphMin;
685     this.graphMax = annotation.graphMax;
686     this.graph = annotation.graph;
687     this.graphHeight = annotation.graphHeight;
688     this.graphGroup = annotation.graphGroup;
689     this.groupRef = annotation.groupRef;
690     this.editable = annotation.editable;
691     this.autoCalculated = annotation.autoCalculated;
692     this.hasIcons = annotation.hasIcons;
693     this.hasText = annotation.hasText;
694     this.height = annotation.height;
695     this.label = annotation.label;
696     this.padGaps = annotation.padGaps;
697     this.visible = annotation.visible;
698     this.centreColLabels = annotation.centreColLabels;
699     this.scaleColLabel = annotation.scaleColLabel;
700     this.showAllColLabels = annotation.showAllColLabels;
701     this.calcId = annotation.calcId;
702     if (annotation.properties != null)
703     {
704       properties = new HashMap<String, String>();
705       for (Map.Entry<String, String> val : annotation.properties.entrySet())
706       {
707         properties.put(val.getKey(), val.getValue());
708       }
709     }
710     if (this.hasScore = annotation.hasScore)
711     {
712       this.score = annotation.score;
713     }
714     if (annotation.threshold != null)
715     {
716       threshold = new GraphLine(annotation.threshold);
717     }
718     Annotation[] ann = annotation.annotations;
719     if (annotation.annotations != null)
720     {
721       this.annotations = new Annotation[ann.length];
722       for (int i = 0; i < ann.length; i++)
723       {
724         if (ann[i] != null)
725         {
726           annotations[i] = new Annotation(ann[i]);
727           if (_linecolour != null)
728           {
729             _linecolour = annotations[i].colour;
730           }
731         }
732       }
733     }
734     if (annotation.sequenceRef != null)
735     {
736       this.sequenceRef = annotation.sequenceRef;
737       if (annotation.sequenceMapping != null)
738       {
739         Integer p = null;
740         sequenceMapping = new HashMap<Integer, Annotation>();
741         Iterator<Integer> pos = annotation.sequenceMapping.keySet()
742                 .iterator();
743         while (pos.hasNext())
744         {
745           // could optimise this!
746           p = pos.next();
747           Annotation a = annotation.sequenceMapping.get(p);
748           if (a == null)
749           {
750             continue;
751           }
752           if (ann != null)
753           {
754             for (int i = 0; i < ann.length; i++)
755             {
756               if (ann[i] == a)
757               {
758                 sequenceMapping.put(p, annotations[i]);
759               }
760             }
761           }
762         }
763       }
764       else
765       {
766         this.sequenceMapping = null;
767       }
768     }
769     // TODO: check if we need to do this: JAL-952
770     // if (this.isrna=annotation.isrna)
771     {
772       // _rnasecstr=new SequenceFeature[annotation._rnasecstr];
773     }
774     validateRangeAndDisplay(); // construct hashcodes, etc.
775   }
776
777   /**
778    * clip the annotation to the columns given by startRes and endRes (inclusive)
779    * and prune any existing sequenceMapping to just those columns.
780    * 
781    * @param startRes
782    * @param endRes
783    */
784   public void restrict(int startRes, int endRes)
785   {
786     if (annotations == null)
787     {
788       // non-positional
789       return;
790     }
791     if (startRes < 0)
792     {
793       startRes = 0;
794     }
795     if (startRes >= annotations.length)
796     {
797       startRes = annotations.length - 1;
798     }
799     if (endRes >= annotations.length)
800     {
801       endRes = annotations.length - 1;
802     }
803     if (annotations == null)
804     {
805       return;
806     }
807     Annotation[] temp = new Annotation[endRes - startRes + 1];
808     if (startRes < annotations.length)
809     {
810       System.arraycopy(annotations, startRes, temp, 0, endRes - startRes
811               + 1);
812     }
813     if (sequenceRef != null)
814     {
815       // Clip the mapping, if it exists.
816       int spos = sequenceRef.findPosition(startRes);
817       int epos = sequenceRef.findPosition(endRes);
818       if (sequenceMapping != null)
819       {
820         Map<Integer, Annotation> newmapping = new HashMap<Integer, Annotation>();
821         Iterator<Integer> e = sequenceMapping.keySet().iterator();
822         while (e.hasNext())
823         {
824           Integer pos = e.next();
825           if (pos.intValue() >= spos && pos.intValue() <= epos)
826           {
827             newmapping.put(pos, sequenceMapping.get(pos));
828           }
829         }
830         sequenceMapping.clear();
831         sequenceMapping = newmapping;
832       }
833     }
834     annotations = temp;
835   }
836
837   /**
838    * set the annotation row to be at least length Annotations
839    * 
840    * @param length
841    *          minimum number of columns required in the annotation row
842    * @return false if the annotation row is greater than length
843    */
844   public boolean padAnnotation(int length)
845   {
846     if (annotations == null)
847     {
848       return true; // annotation row is correct - null == not visible and
849       // undefined length
850     }
851     if (annotations.length < length)
852     {
853       Annotation[] na = new Annotation[length];
854       System.arraycopy(annotations, 0, na, 0, annotations.length);
855       annotations = na;
856       return true;
857     }
858     return annotations.length > length;
859
860   }
861
862   /**
863    * DOCUMENT ME!
864    * 
865    * @return DOCUMENT ME!
866    */
867   @Override
868   public String toString()
869   {
870     if (annotations == null)
871     {
872       return "";
873     }
874     StringBuilder buffer = new StringBuilder(256);
875
876     for (int i = 0; i < annotations.length; i++)
877     {
878       if (annotations[i] != null)
879       {
880         if (graph != 0)
881         {
882           buffer.append(annotations[i].value);
883         }
884         else if (hasIcons)
885         {
886           buffer.append(annotations[i].secondaryStructure);
887         }
888         else
889         {
890           buffer.append(annotations[i].displayCharacter);
891         }
892       }
893
894       buffer.append(", ");
895     }
896     // TODO: remove disgusting hack for 'special' treatment of consensus line.
897     if (label.indexOf("Consensus") == 0)
898     {
899       buffer.append("\n");
900
901       for (int i = 0; i < annotations.length; i++)
902       {
903         if (annotations[i] != null)
904         {
905           buffer.append(annotations[i].description);
906         }
907
908         buffer.append(", ");
909       }
910     }
911
912     return buffer.toString();
913   }
914
915   public void setThreshold(GraphLine line)
916   {
917     threshold = line;
918   }
919
920   public GraphLine getThreshold()
921   {
922     return threshold;
923   }
924
925   /**
926    * Attach the annotation to seqRef, starting from startRes position. If
927    * alreadyMapped is true then the indices of the annotation[] array are
928    * sequence positions rather than alignment column positions.
929    * 
930    * @param seqRef
931    * @param startRes
932    * @param alreadyMapped
933    */
934   public void createSequenceMapping(SequenceI seqRef, int startRes,
935           boolean alreadyMapped)
936   {
937
938     if (seqRef == null)
939     {
940       return;
941     }
942     sequenceRef = seqRef;
943     if (annotations == null)
944     {
945       return;
946     }
947     sequenceMapping = new HashMap<Integer, Annotation>();
948
949     int seqPos;
950
951     for (int i = 0; i < annotations.length; i++)
952     {
953       if (annotations[i] != null)
954       {
955         if (alreadyMapped)
956         {
957           seqPos = seqRef.findPosition(i);
958         }
959         else
960         {
961           seqPos = i + startRes;
962         }
963
964         sequenceMapping.put(new Integer(seqPos), annotations[i]);
965       }
966     }
967
968   }
969
970   /**
971    * When positional annotation and a sequence reference is present, clears and
972    * resizes the annotations array to the current alignment width, and adds
973    * annotation according to aligned positions of the sequenceRef given by
974    * sequenceMapping.
975    */
976   public void adjustForAlignment()
977   {
978     if (sequenceRef == null)
979     {
980       return;
981     }
982
983     if (annotations == null)
984     {
985       return;
986     }
987
988     int a = 0, aSize = sequenceRef.getLength();
989
990     if (aSize == 0)
991     {
992       // Its been deleted
993       return;
994     }
995
996     int position;
997     Annotation[] temp = new Annotation[aSize];
998     Integer index;
999     if (sequenceMapping != null)
1000     {
1001       for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
1002       {
1003         index = new Integer(a);
1004         Annotation annot = sequenceMapping.get(index);
1005         if (annot != null)
1006         {
1007           position = sequenceRef.findIndex(a) - 1;
1008
1009           temp[position] = annot;
1010         }
1011       }
1012     }
1013     annotations = temp;
1014   }
1015
1016   /**
1017    * remove any null entries in annotation row and return the number of non-null
1018    * annotation elements.
1019    * 
1020    * @return
1021    */
1022   public int compactAnnotationArray()
1023   {
1024     int i = 0, iSize = annotations.length;
1025     while (i < iSize)
1026     {
1027       if (annotations[i] == null)
1028       {
1029         if (i + 1 < iSize)
1030         {
1031           System.arraycopy(annotations, i + 1, annotations, i, iSize - i
1032                   - 1);
1033         }
1034         iSize--;
1035       }
1036       else
1037       {
1038         i++;
1039       }
1040     }
1041     Annotation[] ann = annotations;
1042     annotations = new Annotation[i];
1043     System.arraycopy(ann, 0, annotations, 0, i);
1044     ann = null;
1045     return iSize;
1046   }
1047
1048   /**
1049    * Associate this annotation with the aligned residues of a particular
1050    * sequence. sequenceMapping will be updated in the following way: null
1051    * sequenceI - existing mapping will be discarded but annotations left in
1052    * mapped positions. valid sequenceI not equal to current sequenceRef: mapping
1053    * is discarded and rebuilt assuming 1:1 correspondence TODO: overload with
1054    * parameter to specify correspondence between current and new sequenceRef
1055    * 
1056    * @param sequenceI
1057    */
1058   public void setSequenceRef(SequenceI sequenceI)
1059   {
1060     if (sequenceI != null)
1061     {
1062       if (sequenceRef != null)
1063       {
1064         boolean rIsDs = sequenceRef.getDatasetSequence() == null, tIsDs = sequenceI
1065                 .getDatasetSequence() == null;
1066         if (sequenceRef != sequenceI
1067                 && (rIsDs && !tIsDs && sequenceRef != sequenceI
1068                         .getDatasetSequence())
1069                 && (!rIsDs && tIsDs && sequenceRef.getDatasetSequence() != sequenceI)
1070                 && (!rIsDs && !tIsDs && sequenceRef.getDatasetSequence() != sequenceI
1071                         .getDatasetSequence())
1072                 && !sequenceRef.equals(sequenceI))
1073         {
1074           // if sequenceRef isn't intersecting with sequenceI
1075           // throw away old mapping and reconstruct.
1076           sequenceRef = null;
1077           if (sequenceMapping != null)
1078           {
1079             sequenceMapping = null;
1080             // compactAnnotationArray();
1081           }
1082           createSequenceMapping(sequenceI, 1, true);
1083           adjustForAlignment();
1084         }
1085         else
1086         {
1087           // Mapping carried over
1088           sequenceRef = sequenceI;
1089         }
1090       }
1091       else
1092       {
1093         // No mapping exists
1094         createSequenceMapping(sequenceI, 1, true);
1095         adjustForAlignment();
1096       }
1097     }
1098     else
1099     {
1100       // throw away the mapping without compacting.
1101       sequenceMapping = null;
1102       sequenceRef = null;
1103     }
1104   }
1105
1106   /**
1107    * @return the score
1108    */
1109   public double getScore()
1110   {
1111     return score;
1112   }
1113
1114   /**
1115    * @param score
1116    *          the score to set
1117    */
1118   public void setScore(double score)
1119   {
1120     hasScore = true;
1121     this.score = score;
1122   }
1123
1124   /**
1125    * 
1126    * @return true if annotation has an associated score
1127    */
1128   public boolean hasScore()
1129   {
1130     return hasScore || !Double.isNaN(score);
1131   }
1132
1133   /**
1134    * Score only annotation
1135    * 
1136    * @param label
1137    * @param description
1138    * @param score
1139    */
1140   public AlignmentAnnotation(String label, String description, double score)
1141   {
1142     this(label, description, null);
1143     setScore(score);
1144   }
1145
1146   /**
1147    * copy constructor with edit based on the hidden columns marked in colSel
1148    * 
1149    * @param alignmentAnnotation
1150    * @param colSel
1151    */
1152   public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
1153           HiddenColumns hidden)
1154   {
1155     this(alignmentAnnotation);
1156     if (annotations == null)
1157     {
1158       return;
1159     }
1160     hidden.makeVisibleAnnotation(this);
1161   }
1162
1163   public void setPadGaps(boolean padgaps, char gapchar)
1164   {
1165     this.padGaps = padgaps;
1166     if (padgaps)
1167     {
1168       hasText = true;
1169       for (int i = 0; i < annotations.length; i++)
1170       {
1171         if (annotations[i] == null)
1172         {
1173           annotations[i] = new Annotation(String.valueOf(gapchar), null,
1174                   ' ', 0f, null);
1175         }
1176         else if (annotations[i].displayCharacter == null
1177                 || annotations[i].displayCharacter.equals(" "))
1178         {
1179           annotations[i].displayCharacter = String.valueOf(gapchar);
1180         }
1181       }
1182     }
1183   }
1184
1185   /**
1186    * format description string for display
1187    * 
1188    * @param seqname
1189    * @return Get the annotation description string optionally prefixed by
1190    *         associated sequence name (if any)
1191    */
1192   public String getDescription(boolean seqname)
1193   {
1194     if (seqname && this.sequenceRef != null)
1195     {
1196       int i = description.toLowerCase().indexOf("<html>");
1197       if (i > -1)
1198       {
1199         // move the html tag to before the sequence reference.
1200         return "<html>" + sequenceRef.getName() + " : "
1201                 + description.substring(i + 6);
1202       }
1203       return sequenceRef.getName() + " : " + description;
1204     }
1205     return description;
1206   }
1207
1208   public boolean isValidStruc()
1209   {
1210     return invalidrnastruc == -1;
1211   }
1212
1213   public long getInvalidStrucPos()
1214   {
1215     return invalidrnastruc;
1216   }
1217
1218   /**
1219    * machine readable ID string indicating what generated this annotation
1220    */
1221   protected String calcId = "";
1222
1223   /**
1224    * properties associated with the calcId
1225    */
1226   protected Map<String, String> properties = new HashMap<String, String>();
1227
1228   /**
1229    * base colour for line graphs. If null, will be set automatically by
1230    * searching the alignment annotation
1231    */
1232   public java.awt.Color _linecolour;
1233
1234   public String getCalcId()
1235   {
1236     return calcId;
1237   }
1238
1239   public void setCalcId(String calcId)
1240   {
1241     this.calcId = calcId;
1242   }
1243
1244   public boolean isRNA()
1245   {
1246     return isrna;
1247   }
1248
1249   /**
1250    * transfer annotation to the given sequence using the given mapping from the
1251    * current positions or an existing sequence mapping
1252    * 
1253    * @param sq
1254    * @param sp2sq
1255    *          map involving sq as To or From
1256    */
1257   public void liftOver(SequenceI sq, Mapping sp2sq)
1258   {
1259     if (sp2sq.getMappedWidth() != sp2sq.getWidth())
1260     {
1261       // TODO: employ getWord/MappedWord to transfer annotation between cDNA and
1262       // Protein reference frames
1263       throw new Error(
1264               "liftOver currently not implemented for transfer of annotation between different types of seqeunce");
1265     }
1266     boolean mapIsTo = (sp2sq != null) ? (sp2sq.getTo() == sq || sp2sq
1267             .getTo() == sq.getDatasetSequence()) : false;
1268
1269     // TODO build a better annotation element map and get rid of annotations[]
1270     Map<Integer, Annotation> mapForsq = new HashMap<Integer, Annotation>();
1271     if (sequenceMapping != null)
1272     {
1273       if (sp2sq != null)
1274       {
1275         for (Entry<Integer, Annotation> ie : sequenceMapping.entrySet())
1276         {
1277           Integer mpos = Integer.valueOf(mapIsTo ? sp2sq
1278                   .getMappedPosition(ie.getKey()) : sp2sq.getPosition(ie
1279                   .getKey()));
1280           if (mpos >= sq.getStart() && mpos <= sq.getEnd())
1281           {
1282             mapForsq.put(mpos, ie.getValue());
1283           }
1284         }
1285         sequenceMapping = mapForsq;
1286         sequenceRef = sq;
1287         adjustForAlignment();
1288       }
1289       else
1290       {
1291         // trim positions
1292       }
1293     }
1294   }
1295
1296   /**
1297    * like liftOver but more general.
1298    * 
1299    * Takes an array of int pairs that will be used to update the internal
1300    * sequenceMapping and so shuffle the annotated positions
1301    * 
1302    * @param newref
1303    *          - new sequence reference for the annotation row - if null,
1304    *          sequenceRef is left unchanged
1305    * @param mapping
1306    *          array of ints containing corresponding positions
1307    * @param from
1308    *          - column for current coordinate system (-1 for index+1)
1309    * @param to
1310    *          - column for destination coordinate system (-1 for index+1)
1311    * @param idxoffset
1312    *          - offset added to index when referencing either coordinate system
1313    * @note no checks are made as to whether from and/or to are sensible
1314    * @note caller should add the remapped annotation to newref if they have not
1315    *       already
1316    */
1317   public void remap(SequenceI newref, HashMap<Integer, int[]> mapping,
1318           int from, int to, int idxoffset)
1319   {
1320     if (mapping != null)
1321     {
1322       Map<Integer, Annotation> old = sequenceMapping;
1323       Map<Integer, Annotation> remap = new HashMap<Integer, Annotation>();
1324       int index = -1;
1325       for (int mp[] : mapping.values())
1326       {
1327         if (index++ < 0)
1328         {
1329           continue;
1330         }
1331         Annotation ann = null;
1332         if (from == -1)
1333         {
1334           ann = sequenceMapping.get(Integer.valueOf(idxoffset + index));
1335         }
1336         else
1337         {
1338           if (mp != null && mp.length > from)
1339           {
1340             ann = sequenceMapping.get(Integer.valueOf(mp[from]));
1341           }
1342         }
1343         if (ann != null)
1344         {
1345           if (to == -1)
1346           {
1347             remap.put(Integer.valueOf(idxoffset + index), ann);
1348           }
1349           else
1350           {
1351             if (to > -1 && to < mp.length)
1352             {
1353               remap.put(Integer.valueOf(mp[to]), ann);
1354             }
1355           }
1356         }
1357       }
1358       sequenceMapping = remap;
1359       old.clear();
1360       if (newref != null)
1361       {
1362         sequenceRef = newref;
1363       }
1364       adjustForAlignment();
1365     }
1366   }
1367
1368   public String getProperty(String property)
1369   {
1370     if (properties == null)
1371     {
1372       return null;
1373     }
1374     return properties.get(property);
1375   }
1376
1377   public void setProperty(String property, String value)
1378   {
1379     if (properties == null)
1380     {
1381       properties = new HashMap<String, String>();
1382     }
1383     properties.put(property, value);
1384   }
1385
1386   public boolean hasProperties()
1387   {
1388     return properties != null && properties.size() > 0;
1389   }
1390
1391   public Collection<String> getProperties()
1392   {
1393     if (properties == null)
1394     {
1395       return Collections.emptyList();
1396     }
1397     return properties.keySet();
1398   }
1399
1400   /**
1401    * Returns the Annotation for the given sequence position (base 1) if any,
1402    * else null
1403    * 
1404    * @param position
1405    * @return
1406    */
1407   public Annotation getAnnotationForPosition(int position)
1408   {
1409     return sequenceMapping == null ? null : sequenceMapping.get(position);
1410
1411   }
1412
1413   /**
1414    * Set the id to "ann" followed by a counter that increments so as to be
1415    * unique for the lifetime of the JVM
1416    */
1417   protected final void setAnnotationId()
1418   {
1419     this.annotationId = ANNOTATION_ID_PREFIX + Long.toString(nextId());
1420   }
1421
1422   /**
1423    * Returns the match for the last unmatched opening RNA helix pair symbol
1424    * preceding the given column, or '(' if nothing found to match.
1425    * 
1426    * @param column
1427    * @return
1428    */
1429   public String getDefaultRnaHelixSymbol(int column)
1430   {
1431     String result = "(";
1432     if (annotations == null)
1433     {
1434       return result;
1435     }
1436
1437     /*
1438      * for each preceding column, if it contains an open bracket, 
1439      * count whether it is still unmatched at column, if so return its pair
1440      * (likely faster than the fancy alternative using stacks)
1441      */
1442     for (int col = column - 1; col >= 0; col--)
1443     {
1444       Annotation annotation = annotations[col];
1445       if (annotation == null)
1446       {
1447         continue;
1448       }
1449       String displayed = annotation.displayCharacter;
1450       if (displayed == null || displayed.length() != 1)
1451       {
1452         continue;
1453       }
1454       char symbol = displayed.charAt(0);
1455       if (!Rna.isOpeningParenthesis(symbol))
1456       {
1457         continue;
1458       }
1459
1460       /*
1461        * found an opening bracket symbol
1462        * count (closing-opening) symbols of this type that follow it,
1463        * up to and excluding the target column; if the count is less
1464        * than 1, the opening bracket is unmatched, so return its match
1465        */
1466       String closer = String.valueOf(Rna
1467               .getMatchingClosingParenthesis(symbol));
1468       String opener = String.valueOf(symbol);
1469       int count = 0;
1470       for (int j = col + 1; j < column; j++)
1471       {
1472         if (annotations[j] != null)
1473         {
1474           String s = annotations[j].displayCharacter;
1475           if (closer.equals(s))
1476           {
1477             count++;
1478           }
1479           else if (opener.equals(s))
1480           {
1481             count--;
1482           }
1483         }
1484       }
1485       if (count < 1)
1486       {
1487         return closer;
1488       }
1489     }
1490     return result;
1491   }
1492
1493   protected static synchronized long nextId()
1494   {
1495     return counter++;
1496   }
1497 }