javadoc
[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   /** Can this row be edited by the user ? */
74   public boolean editable = false;
75
76   /** Indicates if annotation has a graphical symbol track */
77   public boolean hasIcons; //
78
79   /** Indicates if annotation has a text character label  */
80   public boolean hasText;
81
82   /** is the row visible */
83   public boolean visible = true;
84
85   public int graphGroup = -1;
86
87   /** Displayed height of row in pixels */
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 (annotations[i].secondaryStructure!=' ' && !hasIcons &&
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 != ' '
202                 && firstChar != 'H'
203                 && firstChar != 'E'
204                 && firstChar != '-'
205                 && firstChar < jalview.schemes.ResidueProperties.aaIndex.length)
206         {
207           if (jalview.schemes.ResidueProperties.aaIndex[firstChar] < 23) // TODO: parameterise to gap symbol number
208           {
209             nonSSLabel = true;
210           }
211         }
212       }
213
214       if (annotations[i].displayCharacter.length() > 0)
215       {
216         hasText = true;
217       }
218     }
219
220     if (nonSSLabel)
221     {
222       hasIcons = false;
223       for (int j = 0; j < annotations.length; j++)
224       {
225         if (annotations[j] != null
226                 && annotations[j].secondaryStructure != ' ')
227         {
228           annotations[j].displayCharacter = String
229                   .valueOf(annotations[j].secondaryStructure);
230           annotations[j].secondaryStructure = ' ';
231         }
232
233       }
234     }
235
236     annotationId = this.hashCode() + "";
237   }
238
239   /**
240    * Creates a new AlignmentAnnotation object.
241    * 
242    * @param label
243    *                DOCUMENT ME!
244    * @param description
245    *                DOCUMENT ME!
246    * @param annotations
247    *                DOCUMENT ME!
248    * @param min
249    *                DOCUMENT ME!
250    * @param max
251    *                DOCUMENT ME!
252    * @param winLength
253    *                DOCUMENT ME!
254    */
255   public AlignmentAnnotation(String label, String description,
256           Annotation[] annotations, float min, float max, int graphType)
257   {
258     // graphs are not editable
259     editable = graphType == 0;
260
261     this.label = label;
262     this.description = description;
263     this.annotations = annotations;
264     graph = graphType;
265     graphMin = min;
266     graphMax = max;
267     validateRangeAndDisplay();
268   }
269
270   /**
271    * checks graphMin and graphMax, secondary structure symbols, sets graphType
272    * appropriately, sets null labels to the empty string if appropriate.
273    */
274   private void validateRangeAndDisplay()
275   {
276
277     if (annotations == null)
278     {
279       visible = false; // try to prevent renderer from displaying.
280       return; // this is a non-annotation row annotation - ie a sequence score.
281     }
282
283     int graphType = graph;
284     float min = graphMin;
285     float max = graphMax;
286     boolean drawValues = true;
287
288     if (min == max)
289     {
290       min = 999999999;
291       for (int i = 0; i < annotations.length; i++)
292       {
293         if (annotations[i] == null)
294         {
295           continue;
296         }
297
298         if (drawValues && annotations[i].displayCharacter != null
299                 && annotations[i].displayCharacter.length() > 1)
300         {
301           drawValues = false;
302         }
303
304         if (annotations[i].value > max)
305         {
306           max = annotations[i].value;
307         }
308
309         if (annotations[i].value < min)
310         {
311           min = annotations[i].value;
312         }
313       }
314       // ensure zero is origin for min/max ranges on only one side of zero
315       if (min>0) {
316         min = 0;
317       } else {
318         if (max<0)
319         {
320           max = 0;
321         }
322         }
323     }
324
325     graphMin = min;
326     graphMax = max;
327
328     areLabelsSecondaryStructure();
329
330     if (!drawValues && graphType != NO_GRAPH)
331     {
332       for (int i = 0; i < annotations.length; i++)
333       {
334         if (annotations[i] != null)
335         {
336           annotations[i].displayCharacter = "";
337         }
338       }
339     }
340   }
341
342   /**
343    * Copy constructor creates a new independent annotation row with the same
344    * associated sequenceRef
345    * 
346    * @param annotation
347    */
348   public AlignmentAnnotation(AlignmentAnnotation annotation)
349   {
350     this.label = new String(annotation.label);
351     if (annotation.description != null)
352       this.description = new String(annotation.description);
353     this.graphMin = annotation.graphMin;
354     this.graphMax = annotation.graphMax;
355     this.graph = annotation.graph;
356     this.graphHeight = annotation.graphHeight;
357     this.graphGroup = annotation.graphGroup;
358     this.groupRef = annotation.groupRef;
359     this.editable = annotation.editable;
360     this.autoCalculated = annotation.autoCalculated;
361     this.hasIcons = annotation.hasIcons;
362     this.hasText = annotation.hasText;
363     this.height = annotation.height;
364     this.label = annotation.label;
365     this.padGaps = annotation.padGaps;
366     this.visible = annotation.visible;
367     if (this.hasScore = annotation.hasScore)
368     {
369       this.score = annotation.score;
370     }
371     if (threshold != null)
372     {
373       threshold = new GraphLine(annotation.threshold);
374     }
375     if (annotation.annotations != null)
376     {
377       Annotation[] ann = annotation.annotations;
378       this.annotations = new Annotation[ann.length];
379       for (int i = 0; i < ann.length; i++)
380       {
381         annotations[i] = new Annotation(ann[i]);
382       }
383       ;
384       if (annotation.sequenceRef != null)
385       {
386         this.sequenceRef = annotation.sequenceRef;
387         if (annotation.sequenceMapping != null)
388         {
389           Integer p = null;
390           sequenceMapping = new Hashtable();
391           Enumeration pos = annotation.sequenceMapping.keys();
392           while (pos.hasMoreElements())
393           {
394             // could optimise this!
395             p = (Integer) pos.nextElement();
396             Annotation a = (Annotation) annotation.sequenceMapping.get(p);
397             if (a == null)
398             {
399               continue;
400             }
401             for (int i = 0; i < ann.length; i++)
402             {
403               if (ann[i] == a)
404               {
405                 sequenceMapping.put(p, annotations[i]);
406               }
407             }
408           }
409         }
410         else
411         {
412           this.sequenceMapping = null;
413         }
414       }
415     }
416     validateRangeAndDisplay(); // construct hashcodes, etc.
417   }
418
419   /**
420    * clip the annotation to the columns given by startRes and endRes (inclusive)
421    * and prune any existing sequenceMapping to just those columns.
422    * 
423    * @param startRes
424    * @param endRes
425    */
426   public void restrict(int startRes, int endRes)
427   {
428     if (annotations == null)
429     {
430       // non-positional
431       return;
432     }
433     if (startRes < 0)
434       startRes = 0;
435     if (startRes >= annotations.length)
436       startRes = annotations.length - 1;
437     if (endRes >= annotations.length)
438       endRes = annotations.length - 1;
439     if (annotations == null)
440       return;
441     Annotation[] temp = new Annotation[endRes - startRes + 1];
442     if (startRes < annotations.length)
443     {
444       System.arraycopy(annotations, startRes, temp, 0, endRes - startRes
445               + 1);
446     }
447     if (sequenceRef != null)
448     {
449       // Clip the mapping, if it exists.
450       int spos = sequenceRef.findPosition(startRes);
451       int epos = sequenceRef.findPosition(endRes);
452       if (sequenceMapping != null)
453       {
454         Hashtable newmapping = new Hashtable();
455         Enumeration e = sequenceMapping.keys();
456         while (e.hasMoreElements())
457         {
458           Integer pos = (Integer) e.nextElement();
459           if (pos.intValue() >= spos && pos.intValue() <= epos)
460           {
461             newmapping.put(pos, sequenceMapping.get(pos));
462           }
463         }
464         sequenceMapping.clear();
465         sequenceMapping = newmapping;
466       }
467     }
468     annotations = temp;
469   }
470
471   /**
472    * set the annotation row to be at least length Annotations
473    * 
474    * @param length
475    *                minimum number of columns required in the annotation row
476    * @return false if the annotation row is greater than length
477    */
478   public boolean padAnnotation(int length)
479   {
480     if (annotations == null)
481     {
482       return true; // annotation row is correct - null == not visible and
483                     // undefined length
484     }
485     if (annotations.length < length)
486     {
487       Annotation[] na = new Annotation[length];
488       System.arraycopy(annotations, 0, na, 0, annotations.length);
489       annotations = na;
490       return true;
491     }
492     return annotations.length > length;
493
494   }
495
496   /**
497    * DOCUMENT ME!
498    * 
499    * @return DOCUMENT ME!
500    */
501   public String toString()
502   {
503     StringBuffer buffer = new StringBuffer();
504
505     for (int i = 0; i < annotations.length; i++)
506     {
507       if (annotations[i] != null)
508       {
509         if (graph != 0)
510         {
511           buffer.append(annotations[i].value);
512         }
513         else if (hasIcons)
514         {
515           buffer.append(annotations[i].secondaryStructure);
516         }
517         else
518         {
519           buffer.append(annotations[i].displayCharacter);
520         }
521       }
522
523       buffer.append(", ");
524     }
525     // TODO: remove disgusting hack for 'special' treatment of consensus line.
526     if (label.indexOf("Consensus")==0)
527     {
528       buffer.append("\n");
529
530       for (int i = 0; i < annotations.length; i++)
531       {
532         if (annotations[i] != null)
533         {
534           buffer.append(annotations[i].description);
535         }
536
537         buffer.append(", ");
538       }
539     }
540
541     return buffer.toString();
542   }
543
544   public void setThreshold(GraphLine line)
545   {
546     threshold = line;
547   }
548
549   public GraphLine getThreshold()
550   {
551     return threshold;
552   }
553
554   /**
555    * Attach the annotation to seqRef, starting from startRes position. If
556    * alreadyMapped is true then the indices of the annotation[] array are
557    * sequence positions rather than alignment column positions.
558    * 
559    * @param seqRef
560    * @param startRes
561    * @param alreadyMapped
562    */
563   public void createSequenceMapping(SequenceI seqRef, int startRes,
564           boolean alreadyMapped)
565   {
566
567     if (seqRef == null)
568     {
569       return;
570     }
571     sequenceRef = seqRef;
572     if (annotations == null)
573     {
574       return;
575     }
576     sequenceMapping = new java.util.Hashtable();
577
578     int seqPos;
579
580     for (int i = 0; i < annotations.length; i++)
581     {
582       if (annotations[i] != null)
583       {
584         if (alreadyMapped)
585         {
586           seqPos = seqRef.findPosition(i);
587         }
588         else
589         {
590           seqPos = i + startRes;
591         }
592
593         sequenceMapping.put(new Integer(seqPos), annotations[i]);
594       }
595     }
596
597   }
598
599   public void adjustForAlignment()
600   {
601     if (sequenceRef == null)
602       return;
603
604     if (annotations == null)
605     {
606       return;
607     }
608
609     int a = 0, aSize = sequenceRef.getLength();
610
611     if (aSize == 0)
612     {
613       // Its been deleted
614       return;
615     }
616
617     int position;
618     Annotation[] temp = new Annotation[aSize];
619     Integer index;
620
621     for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
622     {
623       index = new Integer(a);
624       if (sequenceMapping.containsKey(index))
625       {
626         position = sequenceRef.findIndex(a) - 1;
627
628         temp[position] = (Annotation) sequenceMapping.get(index);
629       }
630     }
631
632     annotations = temp;
633   }
634
635   /**
636    * remove any null entries in annotation row and return the number of non-null
637    * annotation elements.
638    * 
639    * @return
640    */
641   public int compactAnnotationArray()
642   {
643     int i = 0, iSize = annotations.length;
644     while (i < iSize)
645     {
646       if (annotations[i] == null)
647       {
648         if (i + 1 < iSize)
649           System.arraycopy(annotations, i + 1, annotations, i, iSize - i
650                   - 1);
651         iSize--;
652       }
653       else
654       {
655         i++;
656       }
657     }
658     Annotation[] ann = annotations;
659     annotations = new Annotation[i];
660     System.arraycopy(ann, 0, annotations, 0, i);
661     ann = null;
662     return iSize;
663   }
664
665   /**
666    * Associate this annotion with the aligned residues of a particular sequence.
667    * sequenceMapping will be updated in the following way: null sequenceI -
668    * existing mapping will be discarded but annotations left in mapped
669    * positions. valid sequenceI not equal to current sequenceRef: mapping is
670    * discarded and rebuilt assuming 1:1 correspondence TODO: overload with
671    * parameter to specify correspondence between current and new sequenceRef
672    * 
673    * @param sequenceI
674    */
675   public void setSequenceRef(SequenceI sequenceI)
676   {
677     if (sequenceI != null)
678     {
679       if (sequenceRef != null)
680       {
681         if (sequenceRef != sequenceI
682                 && !sequenceRef.equals(sequenceI)
683                 && sequenceRef.getDatasetSequence() != sequenceI
684                         .getDatasetSequence())
685         {
686           // if sequenceRef isn't intersecting with sequenceI
687           // throw away old mapping and reconstruct.
688           sequenceRef = null;
689           if (sequenceMapping != null)
690           {
691             sequenceMapping = null;
692             // compactAnnotationArray();
693           }
694           createSequenceMapping(sequenceI, 1, true);
695           adjustForAlignment();
696         }
697         else
698         {
699           // Mapping carried over
700           sequenceRef = sequenceI;
701         }
702       }
703       else
704       {
705         // No mapping exists
706         createSequenceMapping(sequenceI, 1, true);
707         adjustForAlignment();
708       }
709     }
710     else
711     {
712       // throw away the mapping without compacting.
713       sequenceMapping = null;
714       sequenceRef = null;
715     }
716   }
717
718   /**
719    * @return the score
720    */
721   public double getScore()
722   {
723     return score;
724   }
725
726   /**
727    * @param score
728    *                the score to set
729    */
730   public void setScore(double score)
731   {
732     hasScore = true;
733     this.score = score;
734   }
735
736   /**
737    * 
738    * @return true if annotation has an associated score
739    */
740   public boolean hasScore()
741   {
742     return hasScore || !Double.isNaN(score);
743   }
744
745   /**
746    * Score only annotation
747    * 
748    * @param label
749    * @param description
750    * @param score
751    */
752   public AlignmentAnnotation(String label, String description, double score)
753   {
754     this(label, description, null);
755     setScore(score);
756   }
757
758   /**
759    * copy constructor with edit based on the hidden columns marked in colSel
760    * 
761    * @param alignmentAnnotation
762    * @param colSel
763    */
764   public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
765           ColumnSelection colSel)
766   {
767     this(alignmentAnnotation);
768     if (annotations == null)
769     {
770       return;
771     }
772     colSel.makeVisibleAnnotation(this);
773   }
774
775   public void setPadGaps(boolean padgaps, char gapchar)
776   {
777     this.padGaps = padgaps;
778     if (padgaps)
779     {
780       hasText = true;
781       for (int i = 0; i < annotations.length; i++)
782       {
783         if (annotations[i] == null)
784           annotations[i] = new Annotation(String.valueOf(gapchar), null,
785                   ' ', 0f);
786         else if (annotations[i].displayCharacter == null
787                 || annotations[i].displayCharacter.equals(" "))
788           annotations[i].displayCharacter = String.valueOf(gapchar);
789       }
790     }
791   }
792
793   /**
794    * format description string for display
795    * 
796    * @param seqname
797    * @return Get the annotation description string optionally prefixed by
798    *         associated sequence name (if any)
799    */
800   public String getDescription(boolean seqname)
801   {
802     if (seqname && this.sequenceRef != null)
803     {
804       return sequenceRef.getName() + " : " + description;
805     }
806     return description;
807   }
808 }