fix for unicode char labels causing arrayOutOfBoundsException bug spotted by cameron...
[jalview.git] / src / jalview / datamodel / AlignmentAnnotation.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer
3  * Copyright (C) 2007 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18  */
19 package jalview.datamodel;
20
21 import java.util.Enumeration;
22 import java.util.Hashtable;
23
24 /**
25  * DOCUMENT ME!
26  *
27  * @author $author$
28  * @version $Revision$
29  */
30 public class AlignmentAnnotation
31 {
32   /** If true, this annotations is calculated every edit,
33    * eg consensus, quality or conservation graphs */
34   public boolean autoCalculated = false;
35
36   public String annotationId;
37
38   public SequenceI sequenceRef;
39
40   /** DOCUMENT ME!! */
41   public String label;
42
43   /** DOCUMENT ME!! */
44   public String description;
45
46   /** DOCUMENT ME!! */
47   public Annotation[] annotations;
48
49   public java.util.Hashtable sequenceMapping;
50
51   /** DOCUMENT ME!! */
52   public float graphMin;
53
54   /** DOCUMENT ME!! */
55   public float graphMax;
56
57   /**
58    * Score associated with label and description.
59    */
60   public double score= Double.NaN;
61   /**
62    * flag indicating if annotation has a score.
63    */
64   public boolean hasScore=false;
65
66   public GraphLine threshold;
67
68   // Graphical hints and tips
69
70   /** DOCUMENT ME!! */
71   public boolean editable = false;
72
73   /** DOCUMENT ME!! */
74   public boolean hasIcons; //
75
76   /** DOCUMENT ME!! */
77   public boolean hasText;
78
79   /** DOCUMENT ME!! */
80   public boolean visible = true;
81
82   public int graphGroup = -1;
83
84   /** DOCUMENT ME!! */
85   public int height = 0;
86
87   public int graph = 0;
88
89   public int graphHeight = 40;
90
91   public boolean padGaps = false;
92
93   public static final int NO_GRAPH = 0;
94
95   public static final int BAR_GRAPH = 1;
96
97   public static final int LINE_GRAPH = 2;
98
99   public boolean belowAlignment = true;
100
101
102   public static int getGraphValueFromString(String string)
103   {
104     if (string.equalsIgnoreCase("BAR_GRAPH"))
105     {
106       return BAR_GRAPH;
107     }
108     else if (string.equalsIgnoreCase("LINE_GRAPH"))
109     {
110       return LINE_GRAPH;
111     }
112     else
113     {
114       return NO_GRAPH;
115     }
116   }
117
118   /**
119    * Creates a new AlignmentAnnotation object.
120    *
121    * @param label short label shown under sequence labels
122    * @param description text displayed on mouseover
123    * @param annotations set of positional annotation elements
124    */
125   public AlignmentAnnotation(String label, String description,
126                              Annotation[] annotations)
127   {
128     // always editable?
129     editable = true;
130     this.label = label;
131     this.description = description;
132     this.annotations = annotations;
133
134      validateRangeAndDisplay();
135   }
136
137   void areLabelsSecondaryStructure()
138   {
139     boolean nonSSLabel = false;
140     char firstChar=0;
141     for (int i = 0; i < annotations.length; i++)
142     {
143       if (annotations[i] == null)
144       {
145         continue;
146       }
147       if (annotations[i].secondaryStructure == 'H' ||
148           annotations[i].secondaryStructure == 'E')
149       {
150           hasIcons = true;
151       }
152
153       if(annotations[i].displayCharacter==null)
154       {
155         continue;
156       }
157       if (annotations[i].displayCharacter.length() == 1)
158       {      
159         firstChar = annotations[i].displayCharacter.charAt(0);
160         // check to see if it looks like a sequence or is secondary structure labelling.
161         if (
162                 // Uncomment to only catch case where displayCharacter==secondary Structure 
163               // to correctly redisplay SS annotation imported from Stockholm, exported to JalviewXML and read back in again.
164           // && annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
165           firstChar!='H'
166           && firstChar!='E'
167           && firstChar!='-'
168           && firstChar!='-' && firstChar<jalview.schemes.ResidueProperties.aaIndex.length)
169         {
170           if (jalview.schemes.ResidueProperties.aaIndex
171                   [firstChar] < 23)
172           {
173             nonSSLabel = true;
174           }
175         }
176       }
177
178       if (annotations[i].displayCharacter.length() > 0)
179       {
180         hasText = true;
181       }
182     }
183
184     if (nonSSLabel)
185     {
186       hasIcons = false;
187       for (int j = 0; j < annotations.length; j++)
188       {
189         if (annotations[j] != null && annotations[j].secondaryStructure != ' ')
190         {
191           annotations[j].displayCharacter
192               = String.valueOf(annotations[j].secondaryStructure);
193           annotations[j].secondaryStructure = ' ';
194         }
195
196       }
197     }
198
199     annotationId = this.hashCode() + "";
200   }
201   /**
202    * Creates a new AlignmentAnnotation object.
203    *
204    * @param label DOCUMENT ME!
205    * @param description DOCUMENT ME!
206    * @param annotations DOCUMENT ME!
207    * @param min DOCUMENT ME!
208    * @param max DOCUMENT ME!
209    * @param winLength DOCUMENT ME!
210    */
211   public AlignmentAnnotation(String label, String description,
212                              Annotation[] annotations, float min, float max,
213                              int graphType)
214   {
215     // graphs are not editable
216     editable = graphType==0;
217
218     this.label = label;
219     this.description = description;
220     this.annotations = annotations;
221     graph = graphType;
222     graphMin = min;
223     graphMax = max;
224     validateRangeAndDisplay();
225   }
226   /**
227    * checks graphMin and graphMax,
228    * secondary structure symbols,
229    * sets graphType appropriately,
230    * sets null labels to the empty string
231    * if appropriate.
232    */
233   private void validateRangeAndDisplay() {
234
235     if (annotations==null)
236     {
237       visible=false; // try to prevent renderer from displaying.
238       return; // this is a non-annotation row annotation - ie a sequence score.
239     }
240
241     int graphType = graph;
242     float min = graphMin;
243     float max = graphMax;
244     boolean drawValues = true;
245
246     if (min == max)
247     {
248       min = 999999999;
249       for (int i = 0; i < annotations.length; i++)
250       {
251         if (annotations[i] == null)
252         {
253           continue;
254         }
255
256         if (drawValues
257             && annotations[i].displayCharacter!=null
258             && annotations[i].displayCharacter.length() > 1)
259         {
260           drawValues = false;
261         }
262
263         if (annotations[i].value > max)
264         {
265           max = annotations[i].value;
266         }
267
268         if (annotations[i].value < min)
269         {
270           min = annotations[i].value;
271         }
272       }
273     }
274
275     graphMin = min;
276     graphMax = max;
277
278     areLabelsSecondaryStructure();
279
280     if (!drawValues && graphType != NO_GRAPH)
281     {
282       for (int i = 0; i < annotations.length; i++)
283       {
284         if (annotations[i] != null)
285         {
286           annotations[i].displayCharacter = "";
287         }
288       }
289     }
290   }
291
292   /**
293    * Copy constructor
294    * creates a new independent annotation row with the same associated sequenceRef
295    * @param annotation
296    */
297   public AlignmentAnnotation(AlignmentAnnotation annotation)
298   {
299     this.label = new String(annotation.label);
300     if (annotation.description != null)
301       this.description = new String(annotation.description);
302     this.graphMin = annotation.graphMin;
303     this.graphMax = annotation.graphMax;
304     this.graph = annotation.graph;
305     this.graphHeight = annotation.graphHeight;
306     this.graphGroup = annotation.graphGroup;
307     this.editable = annotation.editable;
308     this.autoCalculated = annotation.autoCalculated;
309     this.hasIcons = annotation.hasIcons;
310     this.hasText = annotation.hasText;
311     this.height = annotation.height;
312     this.label = annotation.label;
313     this.padGaps = annotation.padGaps;
314     this.visible = annotation.visible;
315     if (this.hasScore = annotation.hasScore)
316     {
317       this.score = annotation.score;
318     }
319     if (threshold!=null) {
320       threshold = new GraphLine(annotation.threshold);
321     }
322     if (annotation.annotations!=null) {
323       Annotation[] ann = annotation.annotations;
324       this.annotations = new Annotation[ann.length];
325       for (int i=0; i<ann.length; i++) {
326         annotations[i] = new Annotation(ann[i]);
327       };
328       if (annotation.sequenceRef!=null) {
329         this.sequenceRef = annotation.sequenceRef;
330         if (annotation.sequenceMapping!=null)
331         {
332           Integer p=null;
333           sequenceMapping = new Hashtable();
334           Enumeration pos=annotation.sequenceMapping.keys();
335           while (pos.hasMoreElements()) {
336             // could optimise this!
337             p = (Integer) pos.nextElement();
338             Annotation a = (Annotation) annotation.sequenceMapping.get(p);
339             if (a==null)
340             {
341               continue;
342             }
343             for (int i=0; i<ann.length; i++)
344             {
345               if (ann[i]==a)
346               {
347                 sequenceMapping.put(p, annotations[i]);
348               }
349             }
350           }
351         } else {
352           this.sequenceMapping = null;
353         }
354       }
355     }
356     validateRangeAndDisplay(); // construct hashcodes, etc.
357   }
358
359   /**
360    * clip the annotation to the columns given by startRes and endRes (inclusive)
361    * and prune any existing sequenceMapping to just those columns.
362    * @param startRes
363    * @param endRes
364    */
365   public void restrict(int startRes, int endRes)
366   {
367     if (annotations==null)
368     {
369       // non-positional
370       return;
371     }
372     if (startRes<0)
373       startRes=0;
374     if (startRes>=annotations.length)
375       startRes = annotations.length-1;
376     if (endRes>=annotations.length)
377       endRes = annotations.length-1;
378     if (annotations==null)
379       return;
380     Annotation[] temp = new Annotation[endRes-startRes+1];
381     if (startRes<annotations.length)
382     {
383       System.arraycopy(annotations, startRes, temp, 0, endRes-startRes+1);
384     }
385     if (sequenceRef!=null) {
386       // Clip the mapping, if it exists.
387       int spos = sequenceRef.findPosition(startRes);
388       int epos = sequenceRef.findPosition(endRes);
389       if (sequenceMapping!=null)
390       {
391         Hashtable newmapping = new Hashtable();
392         Enumeration e = sequenceMapping.keys();
393         while (e.hasMoreElements())
394         {
395           Integer pos = (Integer) e.nextElement();
396           if (pos.intValue()>=spos && pos.intValue()<=epos)
397           {
398             newmapping.put(pos, sequenceMapping.get(pos));
399           }
400         }
401         sequenceMapping.clear();
402         sequenceMapping = newmapping;
403       }
404     }
405     annotations=temp;
406   }
407   /**
408    * set the annotation row to be at least length Annotations
409    * @param length minimum number of columns required in the annotation row
410    * @return false if the annotation row is greater than length
411    */
412   public boolean padAnnotation(int length) {
413     if (annotations==null)
414     {
415       return true; // annotation row is correct - null == not visible and undefined length
416     }
417     if (annotations.length<length)
418     {
419       Annotation[] na = new Annotation[length];
420       System.arraycopy(annotations, 0, na, 0, annotations.length);
421       annotations = na;
422       return true;
423     }
424     return annotations.length>length;
425
426   }
427
428   /**
429    * DOCUMENT ME!
430    *
431    * @return DOCUMENT ME!
432    */
433   public String toString()
434   {
435     StringBuffer buffer = new StringBuffer();
436
437     for (int i = 0; i < annotations.length; i++)
438     {
439       if (annotations[i] != null)
440       {
441         if (graph != 0)
442         {
443           buffer.append(annotations[i].value);
444         }
445         else if (hasIcons)
446         {
447           buffer.append(annotations[i].secondaryStructure);
448         }
449         else
450         {
451           buffer.append(annotations[i].displayCharacter);
452         }
453       }
454
455       buffer.append(", ");
456     }
457
458     if (label.equals("Consensus"))
459     {
460       buffer.append("\n");
461
462       for (int i = 0; i < annotations.length; i++)
463       {
464         if (annotations[i] != null)
465         {
466           buffer.append(annotations[i].description);
467         }
468
469         buffer.append(", ");
470       }
471     }
472
473     return buffer.toString();
474   }
475
476   public void setThreshold(GraphLine line)
477   {
478     threshold = line;
479   }
480
481   public GraphLine getThreshold()
482   {
483     return threshold;
484   }
485
486   /**
487    * Attach the annotation to seqRef, starting from startRes position. If alreadyMapped is true then the indices of the annotation[] array are sequence positions rather than alignment column positions.
488    * @param seqRef
489    * @param startRes
490    * @param alreadyMapped
491    */
492   public void createSequenceMapping(SequenceI seqRef,
493                                     int startRes,
494                                     boolean alreadyMapped)
495   {
496
497     if (seqRef == null)
498     {
499       return;
500     }
501     sequenceRef=seqRef;
502     if (annotations==null)
503     {
504       return;
505     }
506     sequenceMapping = new java.util.Hashtable();
507
508     int seqPos;
509
510     for (int i = 0; i < annotations.length; i++)
511     {
512       if (annotations[i] != null)
513       {
514         if (alreadyMapped)
515         {
516           seqPos = seqRef.findPosition(i);
517         }
518         else
519         {
520           seqPos = i + startRes;
521         }
522
523         sequenceMapping.put(new Integer(seqPos), annotations[i]);
524       }
525     }
526
527   }
528
529   public void adjustForAlignment()
530   {
531     if (sequenceRef==null)
532       return;
533
534     if (annotations==null)
535     {
536       return;
537     }
538
539     int a = 0, aSize = sequenceRef.getLength();
540
541     if (aSize == 0)
542     {
543       //Its been deleted
544       return;
545     }
546
547     int position;
548     Annotation[] temp = new Annotation[aSize];
549     Integer index;
550
551     for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
552     {
553       index = new Integer(a);
554       if (sequenceMapping.containsKey(index))
555       {
556         position = sequenceRef.findIndex(a) - 1;
557
558         temp[position] = (Annotation) sequenceMapping.get(index);
559       }
560     }
561
562     annotations = temp;
563   }
564   /**
565    * remove any null entries in annotation row and return the
566    * number of non-null annotation elements.
567    * @return
568    */
569   public int compactAnnotationArray() {
570     int i=0,iSize=annotations.length;
571     while (i<iSize)
572     {
573       if (annotations[i]==null) {
574         if (i+1<iSize)
575           System.arraycopy(annotations, i+1, annotations, i, iSize-i-1);
576         iSize--;
577       } else {
578         i++;
579       }
580     }
581     Annotation[] ann = annotations;
582     annotations = new Annotation[i];
583     System.arraycopy(ann, 0, annotations, 0, i);
584     ann = null;
585     return iSize;
586   }
587
588   /**
589    * Associate this annotion with the aligned residues of a particular sequence.
590    * sequenceMapping will be updated in the following way:
591    *   null sequenceI - existing mapping will be discarded but annotations left in mapped positions.
592    *   valid sequenceI not equal to current sequenceRef: mapping is discarded and rebuilt assuming 1:1 correspondence
593    *   TODO: overload with parameter to specify correspondence between current and new sequenceRef
594    * @param sequenceI
595    */
596   public void setSequenceRef(SequenceI sequenceI)
597   {
598     if (sequenceI != null)
599     {
600       if (sequenceRef != null)
601       {
602         if (sequenceRef != sequenceI && !sequenceRef.equals(sequenceI) && sequenceRef.getDatasetSequence()!=sequenceI.getDatasetSequence())
603         {
604           // if sequenceRef isn't intersecting with sequenceI
605           // throw away old mapping and reconstruct.
606           sequenceRef = null;
607           if (sequenceMapping != null)
608           {
609             sequenceMapping = null;
610             // compactAnnotationArray();
611           }
612           createSequenceMapping(sequenceI, 1, true);
613           adjustForAlignment();
614         }
615         else
616         {
617           // Mapping carried over
618           sequenceRef = sequenceI;
619         }
620       }
621       else
622       {
623         // No mapping exists
624         createSequenceMapping(sequenceI, 1, true);
625         adjustForAlignment();
626       }
627     }
628     else
629     {
630       // throw away the mapping without compacting.
631       sequenceMapping = null;
632       sequenceRef = null;
633     }
634   }
635
636   /**
637    * @return the score
638    */
639   public double getScore()
640   {
641     return score;
642   }
643
644   /**
645    * @param score the score to set
646    */
647   public void setScore(double score)
648   {
649     hasScore=true;
650     this.score = score;
651   }
652   /**
653    *
654    * @return true if annotation has an associated score
655    */
656   public boolean hasScore()
657   {
658     return hasScore || !Double.isNaN(score);
659   }
660   /**
661    * Score only annotation
662    * @param label
663    * @param description
664    * @param score
665    */
666   public AlignmentAnnotation(String label, String description, double score)
667   {
668     this(label, description, null);
669     setScore(score);
670   }
671   /**
672    * copy constructor with edit based on the hidden columns marked in colSel
673    * @param alignmentAnnotation
674    * @param colSel
675    */
676   public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
677           ColumnSelection colSel)
678   {
679     this(alignmentAnnotation);
680     if (annotations==null)
681     {
682       return;
683     }
684     colSel.makeVisibleAnnotation(this);
685   }
686
687   public void setPadGaps(boolean padgaps, char gapchar)
688   {
689     this.padGaps = padgaps;
690     if(padgaps)
691     {
692       hasText = true;
693       for(int i=0; i<annotations.length; i++)
694       {
695         if(annotations[i]==null)
696           annotations[i] = new Annotation(String.valueOf(gapchar),null,' ',0f);
697         else if(annotations[i].displayCharacter==null ||annotations[i].displayCharacter.equals(" "))
698           annotations[i].displayCharacter=String.valueOf(gapchar);
699       }
700     }
701   }
702 }