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