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