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