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