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