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