JAL-3853 JAL-1870 patch tests for annotation colour gradient without transparency...
[jalview.git] / src / jalview / schemes / AnnotationColourGradient.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.schemes;
22
23 import jalview.api.AlignViewportI;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.AnnotatedCollectionI;
27 import jalview.datamodel.Annotation;
28 import jalview.datamodel.GraphLine;
29 import jalview.datamodel.SequenceCollectionI;
30 import jalview.datamodel.SequenceI;
31 import jalview.renderer.AnnotationRenderer;
32 import jalview.util.Comparison;
33
34 import java.awt.Color;
35 import java.util.IdentityHashMap;
36 import java.util.Map;
37
38 public class AnnotationColourGradient extends FollowerColourScheme
39 {
40   /**
41    * map positional scores to transparency rather than colour
42    */
43   boolean positionToTransparency = false;
44
45   /**
46    * compute shade based on annotation row score
47    */
48   boolean perLineScore = false;
49
50   public static final int NO_THRESHOLD = -1;
51
52   public static final int BELOW_THRESHOLD = 0;
53
54   public static final int ABOVE_THRESHOLD = 1;
55
56   private final AlignmentAnnotation annotation;
57
58   private final int aboveAnnotationThreshold;
59
60   public boolean thresholdIsMinMax = false;
61
62   private GraphLine annotationThreshold;
63
64   private int redMin;
65
66   private int greenMin;
67
68   private int blueMin;
69
70   private int redRange;
71
72   private int greenRange;
73
74   private int blueRange;
75
76   private boolean predefinedColours = false;
77
78   private boolean seqAssociated = false;
79
80   /**
81    * false if the scheme was constructed without a minColour and maxColour used
82    * to decide if existing colours should be taken from annotation elements when
83    * they exist
84    */
85   private boolean noGradient = false;
86
87   private IdentityHashMap<SequenceI, AlignmentAnnotation> seqannot = null;
88
89   @Override
90   public ColourSchemeI getInstance(AlignViewportI view,
91           AnnotatedCollectionI sg)
92   {
93     AnnotationColourGradient acg = new AnnotationColourGradient(annotation,
94             getColourScheme(), aboveAnnotationThreshold);
95     acg.thresholdIsMinMax = thresholdIsMinMax;
96     acg.annotationThreshold = (annotationThreshold == null) ? null
97             : new GraphLine(annotationThreshold);
98     acg.redMin = redMin;
99     acg.greenMin = greenMin;
100     acg.blueMin = blueMin;
101     acg.redRange = redRange;
102     acg.greenRange = greenRange;
103     acg.blueRange = blueRange;
104     acg.predefinedColours = predefinedColours;
105     acg.seqAssociated = seqAssociated;
106     acg.noGradient = noGradient;
107     acg.positionToTransparency = positionToTransparency;
108     acg.perLineScore = perLineScore;
109     return acg;
110   }
111
112   /**
113    * Creates a new AnnotationColourGradient object.
114    */
115   public AnnotationColourGradient(AlignmentAnnotation annotation,
116           ColourSchemeI originalColour, int aboveThreshold)
117   {
118     if (originalColour instanceof AnnotationColourGradient)
119     {
120       setColourScheme(((AnnotationColourGradient) originalColour)
121               .getColourScheme());
122     }
123     else
124     {
125       setColourScheme(originalColour);
126     }
127
128     this.annotation = annotation;
129
130     aboveAnnotationThreshold = aboveThreshold;
131
132     if (aboveThreshold != NO_THRESHOLD && annotation.threshold != null)
133     {
134       annotationThreshold = annotation.threshold;
135     }
136     // clear values so we don't get weird black bands...
137     redMin = 254;
138     greenMin = 254;
139     blueMin = 254;
140     redRange = 0;
141     greenRange = 0;
142     blueRange = 0;
143
144     noGradient = true;
145     checkLimits();
146   }
147
148   /**
149    * Creates a new AnnotationColourGradient object.
150    */
151   public AnnotationColourGradient(AlignmentAnnotation annotation,
152           Color minColour, Color maxColour, int aboveThreshold)
153   {
154     this.annotation = annotation;
155
156     aboveAnnotationThreshold = aboveThreshold;
157
158     if (aboveThreshold != NO_THRESHOLD && annotation.threshold != null)
159     {
160       annotationThreshold = annotation.threshold;
161     }
162
163     redMin = minColour.getRed();
164     greenMin = minColour.getGreen();
165     blueMin = minColour.getBlue();
166
167     redRange = maxColour.getRed() - redMin;
168     greenRange = maxColour.getGreen() - greenMin;
169     blueRange = maxColour.getBlue() - blueMin;
170
171     noGradient = false;
172     checkLimits();
173   }
174
175   private void checkLimits()
176   {
177     aamax = annotation.graphMax;
178     aamin = annotation.graphMin;
179     if (annotation.isRNA())
180     {
181       // reset colour palette
182       ColourSchemeProperty.resetRnaHelicesShading();
183       ColourSchemeProperty.initRnaHelicesShading(1 + (int) aamax);
184     }
185   }
186
187   @Override
188   public void alignmentChanged(AnnotatedCollectionI alignment,
189           Map<SequenceI, SequenceCollectionI> hiddenReps)
190   {
191     super.alignmentChanged(alignment, hiddenReps);
192
193     if (seqAssociated && annotation.getCalcId() != null)
194     {
195       if (seqannot != null)
196       {
197         seqannot.clear();
198       }
199       else
200       {
201         seqannot = new IdentityHashMap<>();
202       }
203       // resolve the context containing all the annotation for the sequence
204       AnnotatedCollectionI alcontext = alignment instanceof AlignmentI
205               ? alignment
206               : alignment.getContext();
207       boolean f = true, sf = true, rna = false;
208       long plcount = 0, ancount = 0;
209       for (AlignmentAnnotation alan : alcontext.findAnnotation(annotation
210               .getCalcId()))
211       {
212         if (alan.sequenceRef != null
213                 && (alan.label != null && annotation != null
214                         && alan.label.equals(annotation.label)))
215         {
216           ancount++;
217           if (!rna && alan.isRNA())
218           {
219             rna = true;
220           }
221           seqannot.put(alan.sequenceRef, alan);
222           if (f || alan.graphMax > aamax)
223           {
224             aamax = alan.graphMax;
225           }
226           if (f || alan.graphMin < aamin)
227           {
228             aamin = alan.graphMin;
229           }
230           f = false;
231           if (alan.score == alan.score)
232           {
233             if (sf || alan.score < plmin)
234             {
235               plmin = alan.score;
236             }
237             if (sf || alan.score > plmax)
238             {
239               plmax = alan.score;
240             }
241             sf = false;
242             plcount++;
243           }
244         }
245       }
246       if (plcount > 0 && plcount == ancount)
247       {
248         perLineScore = plmax!=plmin;
249         aamax=plmax;
250       }
251       else
252       {
253         perLineScore = false;
254       }
255       if (rna)
256       {
257         ColourSchemeProperty.initRnaHelicesShading(1 + (int) aamax);
258       }
259     }
260   }
261
262   /**
263    * positional annotation max/min
264    */
265   double aamin = 0.0, aamax = 0.0;
266
267   /**
268    * per line score max/min
269    */
270   double plmin = Double.NaN, plmax = Double.NaN;
271
272   public AlignmentAnnotation getAnnotation()
273   {
274     return annotation;
275   }
276
277   public int getAboveThreshold()
278   {
279     return aboveAnnotationThreshold;
280   }
281
282   public float getAnnotationThreshold()
283   {
284     if (annotationThreshold == null)
285     {
286       return 0;
287     }
288     else
289     {
290       return annotationThreshold.value;
291     }
292   }
293
294   public Color getMinColour()
295   {
296     return new Color(redMin, greenMin, blueMin);
297   }
298
299   public Color getMaxColour()
300   {
301     return new Color(redMin + redRange, greenMin + greenRange,
302             blueMin + blueRange);
303   }
304
305   /**
306    * DOCUMENT ME!
307    * 
308    * @param n
309    *          DOCUMENT ME!
310    * 
311    * @return DOCUMENT ME!
312    */
313   @Override
314   public Color findColour(char c)
315   {
316     return Color.red;
317   }
318
319   /**
320    * Returns the colour for a given character and position in a sequence
321    * 
322    * @param c
323    *          the residue character
324    * @param j
325    *          the aligned position
326    * @param seq
327    *          the sequence
328    * @return
329    */
330   @Override
331   public Color findColour(char c, int j, SequenceI seq)
332   {
333     /*
334      * locate the annotation we are configured to colour by
335      */
336     AlignmentAnnotation ann = (seqAssociated && seqannot != null
337             ? seqannot.get(seq)
338             : this.annotation);
339
340     /*
341      * if gap or no annotation at position, no colour (White)
342      */
343     if (ann == null || ann.annotations == null
344             || j >= ann.annotations.length || ann.annotations[j] == null
345             || Comparison.isGap(c))
346     {
347       return Color.white;
348     }
349
350     Annotation aj = ann.annotations[j];
351     // 'use original colours' => colourScheme != null
352     // -> look up colour to be used
353     // predefined colours => preconfigured shading
354     // -> only use original colours reference if thresholding enabled &
355     // minmax exists
356     // annotation.hasIcons => null or black colours replaced with glyph
357     // colours
358     // -> reuse original colours if present
359     // -> if thresholding enabled then return colour on non-whitespace glyph
360
361     /*
362      * if threshold applies, and annotation fails the test - no colour (white)
363      */
364     if (annotationThreshold != null)
365     {
366       if ((aboveAnnotationThreshold == ABOVE_THRESHOLD
367               && aj.value <= annotationThreshold.value)
368               || (aboveAnnotationThreshold == BELOW_THRESHOLD
369                       && aj.value >= annotationThreshold.value))
370       {
371         return Color.white;
372       }
373     }
374
375     /*
376      * If 'use original colours' then return the colour of the annotation
377      * at the aligned position - computed using the background colour scheme
378      */
379     if (predefinedColours && aj.colour != null
380             && !aj.colour.equals(Color.black))
381     {
382       return aj.colour;
383     }
384
385     Color result = Color.white;
386     if (ann.hasIcons && ann.graph == AlignmentAnnotation.NO_GRAPH)
387     {
388       /*
389        * secondary structure symbol colouring
390        */
391       if (aj.secondaryStructure > ' ' && aj.secondaryStructure != '.'
392               && aj.secondaryStructure != '-')
393       {
394         if (getColourScheme() != null)
395         {
396           result = getColourScheme().findColour(c, j, seq, null, 0f);
397         }
398         else
399         {
400           if (ann.isRNA())
401           {
402             result = ColourSchemeProperty.rnaHelices[(int) aj.value];
403           }
404           else
405           {
406             result = ann.annotations[j].secondaryStructure == 'H'
407                     ? AnnotationRenderer.HELIX_COLOUR
408                     : ann.annotations[j].secondaryStructure == 'E'
409                             ? AnnotationRenderer.SHEET_COLOUR
410                             : AnnotationRenderer.STEM_COLOUR;
411           }
412         }
413       }
414       else
415       {
416         return Color.white;
417       }
418     }
419     else if (noGradient)
420     {
421       if (getColourScheme() != null)
422       {
423         result = getColourScheme().findColour(c, j, seq, null, 0f);
424       }
425       else
426       {
427         if (aj.colour != null)
428         {
429           result = aj.colour;
430         }
431       }
432     }
433     else
434     {
435       result = shadeCalculation(ann, j);
436     }
437
438     return result;
439   }
440
441   /**
442    * Returns a graduated colour for the annotation at the given column. If there
443    * is a threshold value, and it is used as the top/bottom of the colour range,
444    * and the value satisfies the threshold condition, then a colour
445    * proportionate to the range from the threshold is calculated. For all other
446    * cases, a colour proportionate to the annotation's min-max range is
447    * calulated. Note that thresholding is _not_ done here (a colour is computed
448    * even if threshold is not passed).
449    * 
450    * @param ann
451    * @param col
452    * @return
453    */
454   Color shadeCalculation(AlignmentAnnotation ann, int col)
455   {
456     float range = 1f;
457     float value = ann.annotations[col].value;
458     if (thresholdIsMinMax && ann.threshold != null
459             && aboveAnnotationThreshold == ABOVE_THRESHOLD
460             && value >= ann.threshold.value)
461     {
462       range = ann.graphMax == ann.threshold.value ? 1f
463               : (value - ann.threshold.value)
464               / (ann.graphMax - ann.threshold.value);
465     }
466     else if (thresholdIsMinMax && ann.threshold != null
467             && aboveAnnotationThreshold == BELOW_THRESHOLD
468             && value <= ann.threshold.value)
469     {
470       range = ann.graphMin == ann.threshold.value ? 0f
471               : (value - ann.graphMin)
472                       / (ann.threshold.value - ann.graphMin);
473     }
474     else
475     {
476       if (ann.graphMax != ann.graphMin)
477       {
478         range = (value - ann.graphMin) / (ann.graphMax - ann.graphMin);
479       }
480       else
481       {
482         range = 0f;
483       }
484     }
485
486     // midtr sets the ceiling for bleaching out the shading
487     int trans = 0, midtr = 239;
488     if (perLineScore)
489     {
490       trans = (int) ((1f - range) * midtr);
491       range = (float) ((ann.score - plmin) / (plmax - aamin));
492     }
493     int dr = (int) (redRange * range + redMin),
494             dg = (int) (greenRange * range + greenMin),
495             db = (int) (blueRange * range + blueMin);
496     if (ann.score == ann.score && positionToTransparency)
497     {
498       return new Color(Math.min(dr + trans, midtr), Math.min(dg
499               + trans, midtr), Math.min(db + trans, midtr));
500     }
501     else
502     {
503       return new Color(dr, dg, db);
504     }
505   }
506
507   public boolean isPredefinedColours()
508   {
509     return predefinedColours;
510   }
511
512   public void setPredefinedColours(boolean predefinedColours)
513   {
514     this.predefinedColours = predefinedColours;
515   }
516
517   public boolean isSeqAssociated()
518   {
519     return seqAssociated;
520   }
521
522   public void setSeqAssociated(boolean sassoc)
523   {
524     seqAssociated = sassoc;
525   }
526
527   public boolean isThresholdIsMinMax()
528   {
529     return thresholdIsMinMax;
530   }
531
532   public void setThresholdIsMinMax(boolean minMax)
533   {
534     this.thresholdIsMinMax = minMax;
535   }
536
537   @Override
538   public String getSchemeName()
539   {
540     return ANNOTATION_COLOUR;
541   }
542
543   @Override
544   public boolean isSimple()
545   {
546     return false;
547   }
548
549   public boolean isPositionToTransparency()
550   {
551     return positionToTransparency;
552   }
553
554   public void setPositionToTransparency(boolean positionToTransparency)
555   {
556     this.positionToTransparency = positionToTransparency;
557   }
558 }