0a569b5b7ec569ff24e5e7577b8d180103912e56
[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    *          - annotation are at aligned columns
929    */
930   public void createSequenceMapping(SequenceI seqRef, int startRes,
931           boolean alreadyMapped)
932   {
933
934     if (seqRef == null)
935     {
936       return;
937     }
938     sequenceRef = seqRef;
939     if (annotations == null)
940     {
941       return;
942     }
943     sequenceMapping = new HashMap<>();
944
945     int seqPos;
946
947     for (int i = 0; i < annotations.length; i++)
948     {
949       if (annotations[i] != null)
950       {
951         if (alreadyMapped)
952         {
953           seqPos = seqRef.findPosition(i);
954         }
955         else
956         {
957           seqPos = i + startRes;
958         }
959
960         sequenceMapping.put(new Integer(seqPos), annotations[i]);
961       }
962     }
963
964   }
965
966   /**
967    * When positional annotation and a sequence reference is present, clears and
968    * resizes the annotations array to the current alignment width, and adds
969    * annotation according to aligned positions of the sequenceRef given by
970    * sequenceMapping.
971    */
972   public void adjustForAlignment()
973   {
974     if (sequenceRef == null)
975     {
976       return;
977     }
978
979     if (annotations == null)
980     {
981       return;
982     }
983
984     int a = 0, aSize = sequenceRef.getLength();
985
986     if (aSize == 0)
987     {
988       // Its been deleted
989       return;
990     }
991
992     int position;
993     Annotation[] temp = new Annotation[aSize];
994     Integer index;
995     if (sequenceMapping != null)
996     {
997       for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
998       {
999         index = new Integer(a);
1000         Annotation annot = sequenceMapping.get(index);
1001         if (annot != null)
1002         {
1003           position = sequenceRef.findIndex(a) - 1;
1004
1005           temp[position] = annot;
1006         }
1007       }
1008     }
1009     annotations = temp;
1010   }
1011
1012   /**
1013    * remove any null entries in annotation row and return the number of non-null
1014    * annotation elements.
1015    * 
1016    * @return
1017    */
1018   public int compactAnnotationArray()
1019   {
1020     int i = 0, iSize = annotations.length;
1021     while (i < iSize)
1022     {
1023       if (annotations[i] == null)
1024       {
1025         if (i + 1 < iSize)
1026         {
1027           System.arraycopy(annotations, i + 1, annotations, i,
1028                   iSize - i - 1);
1029         }
1030         iSize--;
1031       }
1032       else
1033       {
1034         i++;
1035       }
1036     }
1037     Annotation[] ann = annotations;
1038     annotations = new Annotation[i];
1039     System.arraycopy(ann, 0, annotations, 0, i);
1040     ann = null;
1041     return iSize;
1042   }
1043
1044   /**
1045    * Associate this annotation with the aligned residues of a particular
1046    * sequence. sequenceMapping will be updated in the following way: null
1047    * sequenceI - existing mapping will be discarded but annotations left in
1048    * mapped positions. valid sequenceI not equal to current sequenceRef: mapping
1049    * is discarded and rebuilt assuming 1:1 correspondence TODO: overload with
1050    * parameter to specify correspondence between current and new sequenceRef
1051    * 
1052    * @param sequenceI
1053    */
1054   public void setSequenceRef(SequenceI sequenceI)
1055   {
1056     if (sequenceI != null)
1057     {
1058       if (sequenceRef != null)
1059       {
1060         boolean rIsDs = sequenceRef.getDatasetSequence() == null,
1061                 tIsDs = sequenceI.getDatasetSequence() == null;
1062         if (sequenceRef != sequenceI
1063                 && (rIsDs && !tIsDs
1064                         && sequenceRef != sequenceI.getDatasetSequence())
1065                 && (!rIsDs && tIsDs
1066                         && sequenceRef.getDatasetSequence() != sequenceI)
1067                 && (!rIsDs && !tIsDs
1068                         && sequenceRef.getDatasetSequence() != sequenceI
1069                                 .getDatasetSequence())
1070                 && !sequenceRef.equals(sequenceI))
1071         {
1072           // if sequenceRef isn't intersecting with sequenceI
1073           // throw away old mapping and reconstruct.
1074           sequenceRef = null;
1075           if (sequenceMapping != null)
1076           {
1077             sequenceMapping = null;
1078             // compactAnnotationArray();
1079           }
1080           createSequenceMapping(sequenceI, 1, true);
1081           adjustForAlignment();
1082         }
1083         else
1084         {
1085           // Mapping carried over
1086           sequenceRef = sequenceI;
1087         }
1088       }
1089       else
1090       {
1091         // No mapping exists
1092         createSequenceMapping(sequenceI, 1, true);
1093         adjustForAlignment();
1094       }
1095     }
1096     else
1097     {
1098       // throw away the mapping without compacting.
1099       sequenceMapping = null;
1100       sequenceRef = null;
1101     }
1102   }
1103
1104   /**
1105    * @return the score
1106    */
1107   public double getScore()
1108   {
1109     return score;
1110   }
1111
1112   /**
1113    * @param score
1114    *          the score to set
1115    */
1116   public void setScore(double score)
1117   {
1118     hasScore = true;
1119     this.score = score;
1120   }
1121
1122   /**
1123    * 
1124    * @return true if annotation has an associated score
1125    */
1126   public boolean hasScore()
1127   {
1128     return hasScore || !Double.isNaN(score);
1129   }
1130
1131   public void setPadGaps(boolean padgaps, char gapchar)
1132   {
1133     this.padGaps = padgaps;
1134     if (padgaps)
1135     {
1136       hasText = true;
1137       for (int i = 0; i < annotations.length; i++)
1138       {
1139         if (annotations[i] == null)
1140         {
1141           annotations[i] = new Annotation(String.valueOf(gapchar), null,
1142                   ' ', 0f, null);
1143         }
1144         else if (annotations[i].displayCharacter == null
1145                 || annotations[i].displayCharacter.equals(" "))
1146         {
1147           annotations[i].displayCharacter = String.valueOf(gapchar);
1148         }
1149       }
1150     }
1151   }
1152
1153   /**
1154    * format description string for display
1155    * 
1156    * @param seqname
1157    * @return Get the annotation description string optionally prefixed by
1158    *         associated sequence name (if any)
1159    */
1160   public String getDescription(boolean seqname)
1161   {
1162     if (seqname && this.sequenceRef != null)
1163     {
1164       int i = description.toLowerCase().indexOf("<html>");
1165       if (i > -1)
1166       {
1167         // move the html tag to before the sequence reference.
1168         return "<html>" + sequenceRef.getName() + " : "
1169                 + description.substring(i + 6);
1170       }
1171       return sequenceRef.getName() + " : " + description;
1172     }
1173     return description;
1174   }
1175
1176   public boolean isValidStruc()
1177   {
1178     return invalidrnastruc == -1;
1179   }
1180
1181   public long getInvalidStrucPos()
1182   {
1183     return invalidrnastruc;
1184   }
1185
1186   /**
1187    * machine readable ID string indicating what generated this annotation
1188    */
1189   private String calcId = "";
1190
1191   /**
1192    * properties associated with the calcId
1193    */
1194   protected Map<String, String> properties = new HashMap<>();
1195
1196   /**
1197    * base colour for line graphs. If null, will be set automatically by
1198    * searching the alignment annotation
1199    */
1200   public java.awt.Color _linecolour;
1201
1202   public String getCalcId()
1203   {
1204     return calcId;
1205   }
1206
1207   public void setCalcId(String calcId)
1208   {
1209     this.calcId = calcId;
1210   }
1211
1212   public boolean isRNA()
1213   {
1214     return isrna;
1215   }
1216
1217   /**
1218    * transfer annotation to the given sequence using the given mapping from the
1219    * current positions or an existing sequence mapping
1220    * 
1221    * @param sq
1222    * @param sp2sq
1223    *          map involving sq as To or From
1224    */
1225   public void liftOver(SequenceI sq, Mapping sp2sq)
1226   {
1227     if (sp2sq.getMappedWidth() != sp2sq.getWidth())
1228     {
1229       // TODO: employ getWord/MappedWord to transfer annotation between cDNA and
1230       // Protein reference frames
1231       throw new Error(
1232               "liftOver currently not implemented for transfer of annotation between different types of seqeunce");
1233     }
1234     boolean mapIsTo = (sp2sq != null)
1235             ? (sp2sq.getTo() == sq
1236                     || sp2sq.getTo() == sq.getDatasetSequence())
1237             : false;
1238
1239     // TODO build a better annotation element map and get rid of annotations[]
1240     Map<Integer, Annotation> mapForsq = new HashMap<>();
1241     if (sequenceMapping != null)
1242     {
1243       if (sp2sq != null)
1244       {
1245         for (Entry<Integer, Annotation> ie : sequenceMapping.entrySet())
1246         {
1247           Integer mpos = Integer
1248                   .valueOf(mapIsTo ? sp2sq.getMappedPosition(ie.getKey())
1249                           : sp2sq.getPosition(ie.getKey()));
1250           if (mpos >= sq.getStart() && mpos <= sq.getEnd())
1251           {
1252             mapForsq.put(mpos, ie.getValue());
1253           }
1254         }
1255         sequenceMapping = mapForsq;
1256         sequenceRef = sq;
1257         adjustForAlignment();
1258       }
1259       else
1260       {
1261         // trim positions
1262       }
1263     }
1264   }
1265
1266   /**
1267    * like liftOver but more general.
1268    * 
1269    * Takes an array of int pairs that will be used to update the internal
1270    * sequenceMapping and so shuffle the annotated positions
1271    * 
1272    * @param newref
1273    *          - new sequence reference for the annotation row - if null,
1274    *          sequenceRef is left unchanged
1275    * @param mapping
1276    *          array of ints containing corresponding positions
1277    * @param from
1278    *          - column for current coordinate system (-1 for index+1)
1279    * @param to
1280    *          - column for destination coordinate system (-1 for index+1)
1281    * @param idxoffset
1282    *          - offset added to index when referencing either coordinate system
1283    * @note no checks are made as to whether from and/or to are sensible
1284    * @note caller should add the remapped annotation to newref if they have not
1285    *       already
1286    */
1287   public void remap(SequenceI newref, HashMap<Integer, int[]> mapping,
1288           int from, int to, int idxoffset)
1289   {
1290     if (mapping != null)
1291     {
1292       Map<Integer, Annotation> old = sequenceMapping;
1293       Map<Integer, Annotation> remap = new HashMap<>();
1294       int index = -1;
1295       for (int mp[] : mapping.values())
1296       {
1297         if (index++ < 0)
1298         {
1299           continue;
1300         }
1301         Annotation ann = null;
1302         if (from == -1)
1303         {
1304           ann = sequenceMapping.get(Integer.valueOf(idxoffset + index));
1305         }
1306         else
1307         {
1308           if (mp != null && mp.length > from)
1309           {
1310             ann = sequenceMapping.get(Integer.valueOf(mp[from]));
1311           }
1312         }
1313         if (ann != null)
1314         {
1315           if (to == -1)
1316           {
1317             remap.put(Integer.valueOf(idxoffset + index), ann);
1318           }
1319           else
1320           {
1321             if (to > -1 && to < mp.length)
1322             {
1323               remap.put(Integer.valueOf(mp[to]), ann);
1324             }
1325           }
1326         }
1327       }
1328       sequenceMapping = remap;
1329       old.clear();
1330       if (newref != null)
1331       {
1332         sequenceRef = newref;
1333       }
1334       adjustForAlignment();
1335     }
1336   }
1337
1338   public String getProperty(String property)
1339   {
1340     if (properties == null)
1341     {
1342       return null;
1343     }
1344     return properties.get(property);
1345   }
1346
1347   public void setProperty(String property, String value)
1348   {
1349     if (properties == null)
1350     {
1351       properties = new HashMap<>();
1352     }
1353     properties.put(property, value);
1354   }
1355
1356   public boolean hasProperties()
1357   {
1358     return properties != null && properties.size() > 0;
1359   }
1360
1361   public Collection<String> getProperties()
1362   {
1363     if (properties == null)
1364     {
1365       return Collections.emptyList();
1366     }
1367     return properties.keySet();
1368   }
1369
1370   /**
1371    * Returns the Annotation for the given sequence position (base 1) if any,
1372    * else null
1373    * 
1374    * @param position
1375    * @return
1376    */
1377   public Annotation getAnnotationForPosition(int position)
1378   {
1379     return sequenceMapping == null ? null : sequenceMapping.get(position);
1380
1381   }
1382
1383   /**
1384    * Set the id to "ann" followed by a counter that increments so as to be
1385    * unique for the lifetime of the JVM
1386    */
1387   protected final void setAnnotationId()
1388   {
1389     this.annotationId = ANNOTATION_ID_PREFIX + Long.toString(nextId());
1390   }
1391
1392   /**
1393    * Returns the match for the last unmatched opening RNA helix pair symbol
1394    * preceding the given column, or '(' if nothing found to match.
1395    * 
1396    * @param column
1397    * @return
1398    */
1399   public String getDefaultRnaHelixSymbol(int column)
1400   {
1401     String result = "(";
1402     if (annotations == null)
1403     {
1404       return result;
1405     }
1406
1407     /*
1408      * for each preceding column, if it contains an open bracket, 
1409      * count whether it is still unmatched at column, if so return its pair
1410      * (likely faster than the fancy alternative using stacks)
1411      */
1412     for (int col = column - 1; col >= 0; col--)
1413     {
1414       Annotation annotation = annotations[col];
1415       if (annotation == null)
1416       {
1417         continue;
1418       }
1419       String displayed = annotation.displayCharacter;
1420       if (displayed == null || displayed.length() != 1)
1421       {
1422         continue;
1423       }
1424       char symbol = displayed.charAt(0);
1425       if (!Rna.isOpeningParenthesis(symbol))
1426       {
1427         continue;
1428       }
1429
1430       /*
1431        * found an opening bracket symbol
1432        * count (closing-opening) symbols of this type that follow it,
1433        * up to and excluding the target column; if the count is less
1434        * than 1, the opening bracket is unmatched, so return its match
1435        */
1436       String closer = String
1437               .valueOf(Rna.getMatchingClosingParenthesis(symbol));
1438       String opener = String.valueOf(symbol);
1439       int count = 0;
1440       for (int j = col + 1; j < column; j++)
1441       {
1442         if (annotations[j] != null)
1443         {
1444           String s = annotations[j].displayCharacter;
1445           if (closer.equals(s))
1446           {
1447             count++;
1448           }
1449           else if (opener.equals(s))
1450           {
1451             count--;
1452           }
1453         }
1454       }
1455       if (count < 1)
1456       {
1457         return closer;
1458       }
1459     }
1460     return result;
1461   }
1462
1463   protected static synchronized long nextId()
1464   {
1465     return counter++;
1466   }
1467
1468   /**
1469    * 
1470    * @return true for rows that have a range of values in their annotation set
1471    */
1472   public boolean isQuantitative()
1473   {
1474     return graphMin < graphMax;
1475   }
1476
1477   /**
1478    * delete any columns in alignmentAnnotation that are hidden (including
1479    * sequence associated annotation).
1480    * 
1481    * @param hiddenColumns
1482    *          the set of hidden columns
1483    */
1484   public void makeVisibleAnnotation(HiddenColumns hiddenColumns)
1485   {
1486     if (annotations != null)
1487     {
1488       makeVisibleAnnotation(0, annotations.length, hiddenColumns);
1489     }
1490   }
1491
1492   /**
1493    * delete any columns in alignmentAnnotation that are hidden (including
1494    * sequence associated annotation).
1495    * 
1496    * @param start
1497    *          remove any annotation to the right of this column
1498    * @param end
1499    *          remove any annotation to the left of this column
1500    * @param hiddenColumns
1501    *          the set of hidden columns
1502    */
1503   public void makeVisibleAnnotation(int start, int end,
1504           HiddenColumns hiddenColumns)
1505   {
1506     if (annotations != null)
1507     {
1508       if (hiddenColumns.hasHiddenColumns())
1509       {
1510         removeHiddenAnnotation(start, end, hiddenColumns);
1511       }
1512       else
1513       {
1514         restrict(start, end);
1515       }
1516     }
1517   }
1518
1519   /**
1520    * The actual implementation of deleting hidden annotation columns
1521    * 
1522    * @param start
1523    *          remove any annotation to the right of this column
1524    * @param end
1525    *          remove any annotation to the left of this column
1526    * @param hiddenColumns
1527    *          the set of hidden columns
1528    */
1529   private void removeHiddenAnnotation(int start, int end,
1530           HiddenColumns hiddenColumns)
1531   {
1532     // mangle the alignmentAnnotation annotation array
1533     ArrayList<Annotation[]> annels = new ArrayList<>();
1534     Annotation[] els = null;
1535
1536     int w = 0;
1537
1538     Iterator<int[]> blocks = hiddenColumns.getVisContigsIterator(start,
1539             end + 1, false);
1540
1541     int copylength;
1542     int annotationLength;
1543     while (blocks.hasNext())
1544     {
1545       int[] block = blocks.next();
1546       annotationLength = block[1] - block[0] + 1;
1547
1548       if (blocks.hasNext())
1549       {
1550         // copy just the visible segment of the annotation row
1551         copylength = annotationLength;
1552       }
1553       else
1554       {
1555         if (annotationLength + block[0] <= annotations.length)
1556         {
1557           // copy just the visible segment of the annotation row
1558           copylength = annotationLength;
1559         }
1560         else
1561         {
1562           // copy to the end of the annotation row
1563           copylength = annotations.length - block[0];
1564         }
1565       }
1566
1567       els = new Annotation[annotationLength];
1568       annels.add(els);
1569       System.arraycopy(annotations, block[0], els, 0, copylength);
1570       w += annotationLength;
1571     }
1572
1573     if (w != 0)
1574     {
1575       annotations = new Annotation[w];
1576
1577       w = 0;
1578       for (Annotation[] chnk : annels)
1579       {
1580         System.arraycopy(chnk, 0, annotations, w, chnk.length);
1581         w += chnk.length;
1582       }
1583     }
1584   }
1585
1586   public static Iterable<AlignmentAnnotation> findAnnotations(
1587           Iterable<AlignmentAnnotation> list, SequenceI seq, String calcId,
1588           String label)
1589   {
1590     List<AlignmentAnnotation> aa = new ArrayList<>();
1591     for (AlignmentAnnotation ann : list)
1592     {
1593       if ((calcId == null || (ann.getCalcId() != null
1594               && ann.getCalcId().equals(calcId)))
1595               && (seq == null || (ann.sequenceRef != null
1596                       && ann.sequenceRef == seq))
1597               && (label == null
1598                       || (ann.label != null && ann.label.equals(label))))
1599       {
1600         aa.add(ann);
1601       }
1602     }
1603     return aa;
1604   }
1605
1606   /**
1607    * Answer true if any annotation matches the calcId passed in (if not null).
1608    * 
1609    * @param list
1610    *          annotation to search
1611    * @param calcId
1612    * @return
1613    */
1614   public static boolean hasAnnotation(List<AlignmentAnnotation> list,
1615           String calcId)
1616   {
1617
1618     if (calcId != null && !"".equals(calcId))
1619     {
1620       for (AlignmentAnnotation a : list)
1621       {
1622         if (a.getCalcId() == calcId)
1623         {
1624           return true;
1625         }
1626       }
1627     }
1628     return false;
1629   }
1630
1631   public static Iterable<AlignmentAnnotation> findAnnotation(
1632           List<AlignmentAnnotation> list, String calcId)
1633   {
1634     List<AlignmentAnnotation> aa = new ArrayList<>();
1635     if (calcId == null)
1636     {
1637       return aa;
1638     }
1639     for (AlignmentAnnotation a : list)
1640     {
1641
1642       if (a.getCalcId() == calcId || (a.getCalcId() != null
1643               && calcId != null && a.getCalcId().equals(calcId)))
1644       {
1645         aa.add(a);
1646       }
1647     }
1648     return aa;
1649   }
1650 }