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