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