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