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