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