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