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