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