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