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