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