JAL-2843 help text for colour by attribute, feature filters
[jalview.git] / src / jalview / schemes / FeatureColour.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.FeatureColourI;
24 import jalview.datamodel.SequenceFeature;
25 import jalview.datamodel.features.FeatureMatcher;
26 import jalview.util.ColorUtils;
27 import jalview.util.Format;
28
29 import java.awt.Color;
30 import java.util.StringTokenizer;
31
32 /**
33  * A class that represents a colour scheme for a feature type. Options supported
34  * are currently
35  * <ul>
36  * <li>a simple colour e.g. Red</li>
37  * <li>colour by label - a colour is generated from the feature description</li>
38  * <li>graduated colour by feature score</li>
39  * <ul>
40  * <li>minimum and maximum score range must be provided</li>
41  * <li>minimum and maximum value colours should be specified</li>
42  * <li>a colour for 'no value' may optionally be provided</li>
43  * <li>colours for intermediate scores are interpolated RGB values</li>
44  * <li>there is an optional threshold above/below which to colour values</li>
45  * <li>the range may be the full value range, or may be limited by the threshold
46  * value</li>
47  * </ul>
48  * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
49  * (numeric) value of a named attribute</li> </ul>
50  */
51 public class FeatureColour implements FeatureColourI
52 {
53   private static final String ABSOLUTE = "abso";
54
55   private static final String ABOVE = "above";
56
57   private static final String BELOW = "below";
58
59   /*
60    * constants used to read or write a Jalview Features file
61    */
62   private static final String LABEL = "label";
63
64   private static final String SCORE = "score";
65
66   private static final String ATTRIBUTE = "attribute";
67
68   private static final String NO_VALUE_MIN = "noValueMin";
69
70   private static final String NO_VALUE_MAX = "noValueMax";
71
72   private static final String NO_VALUE_NONE = "noValueNone";
73
74   static final Color DEFAULT_NO_COLOUR = null;
75
76   private static final String BAR = "|";
77
78   final private Color colour;
79
80   final private Color minColour;
81
82   final private Color maxColour;
83
84   /*
85    * colour to use for colour by attribute when the 
86    * attribute value is absent
87    */
88   final private Color noColour;
89
90   /*
91    * if true, then colour has a gradient based on a numerical 
92    * range (either feature score, or an attribute value)
93    */
94   private boolean graduatedColour;
95
96   /*
97    * if true, colour values are generated from a text string,
98    * either feature description, or an attribute value
99    */
100   private boolean colourByLabel;
101
102   /*
103    * if not null, the value of [attribute, [sub-attribute] ...]
104    *  is used for colourByLabel or graduatedColour
105    */
106   private String[] attributeName;
107
108   private float threshold;
109
110   private float base;
111
112   private float range;
113
114   private boolean belowThreshold;
115
116   private boolean aboveThreshold;
117
118   private boolean isHighToLow;
119
120   private boolean autoScaled;
121
122   final private float minRed;
123
124   final private float minGreen;
125
126   final private float minBlue;
127
128   final private float deltaRed;
129
130   final private float deltaGreen;
131
132   final private float deltaBlue;
133
134   /**
135    * Parses a Jalview features file format colour descriptor
136    * <p>
137    * <code>
138    * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
139    * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
140    * <p>
141    * 'Score' is optional (default) for a graduated colour. An attribute with
142    * sub-attribute should be written as (for example) CSQ:Consequence.
143    * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
144    * with default noValueMin.
145    * <p>
146    * Examples:
147    * <ul>
148    * <li>red</li>
149    * <li>a28bbb</li>
150    * <li>25,125,213</li>
151    * <li>label</li>
152    * <li>attribute|CSQ:PolyPhen</li>
153    * <li>label|||0.0|0.0|above|12.5</li>
154    * <li>label|||0.0|0.0|below|12.5</li>
155    * <li>red|green|12.0|26.0|none</li>
156    * <li>score|red|green|12.0|26.0|none</li>
157    * <li>attribute|AF|red|green|12.0|26.0|none</li>
158    * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
159    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
160    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
161    * </ul>
162    * 
163    * @param descriptor
164    * @return
165    * @throws IllegalArgumentException
166    *           if not parseable
167    */
168   public static FeatureColourI parseJalviewFeatureColour(String descriptor)
169   {
170     StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
171     float min = Float.MIN_VALUE;
172     float max = Float.MAX_VALUE;
173     boolean byLabel = false;
174     boolean byAttribute = false;
175     String attName = null;
176     String mincol = null;
177     String maxcol = null;
178
179     /*
180      * first token should be 'label', or 'score', or an
181      * attribute name, or simple colour, or minimum colour
182      */
183     String nextToken = gcol.nextToken();
184     if (nextToken == BAR)
185     {
186       throw new IllegalArgumentException(
187               "Expected either 'label' or a colour specification in the line: "
188                       + descriptor);
189     }
190     if (nextToken.toLowerCase().startsWith(LABEL))
191     {
192       byLabel = true;
193       // get the token after the next delimiter:
194       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
195       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
196     }
197     else if (nextToken.toLowerCase().startsWith(SCORE))
198     {
199       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
200       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
201     }
202     else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
203     {
204       byAttribute = true;
205       attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
206       attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
207       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
208       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
209     }
210     else
211     {
212       mincol = nextToken;
213     }
214
215     /*
216      * if only one token, it can validly be label, attributeName,
217      * or a plain colour value
218      */
219     if (!gcol.hasMoreTokens())
220     {
221       if (byLabel || byAttribute)
222       {
223         FeatureColourI fc = new FeatureColour();
224         fc.setColourByLabel(true);
225         if (byAttribute)
226         {
227           fc.setAttributeName(
228                   FeatureMatcher.fromAttributeDisplayName(attName));
229         }
230         return fc;
231       }
232
233       Color colour = ColorUtils.parseColourString(descriptor);
234       if (colour == null)
235       {
236         throw new IllegalArgumentException(
237                 "Invalid colour descriptor: " + descriptor);
238       }
239       return new FeatureColour(colour);
240     }
241
242     /*
243      * continue parsing for min/max/no colour (if graduated)
244      * and for threshold (colour by text or graduated)
245      */
246
247     /*
248      * autoScaled == true: colours range over actual score range
249      * autoScaled == false ('abso'): colours range over min/max range
250      */
251     boolean autoScaled = true;
252     String tok = null, minval, maxval;
253     String noValueColour = NO_VALUE_MIN;
254
255     if (mincol != null)
256     {
257       // at least four more tokens
258       if (mincol.equals(BAR))
259       {
260         mincol = null;
261       }
262       else
263       {
264         gcol.nextToken(); // skip next '|'
265       }
266       maxcol = gcol.nextToken();
267       if (maxcol.equals(BAR))
268       {
269         maxcol = null;
270       }
271       else
272       {
273         gcol.nextToken(); // skip next '|'
274       }
275       tok = gcol.nextToken();
276
277       /*
278        * check for specifier for colour for no attribute value
279        * (new in 2.11, defaults to minColour if not specified)
280        */
281       if (tok.equalsIgnoreCase(NO_VALUE_MIN))
282       {
283         tok = gcol.nextToken();
284         tok = gcol.nextToken();
285       }
286       else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
287       {
288         noValueColour = NO_VALUE_MAX;
289         tok = gcol.nextToken();
290         tok = gcol.nextToken();
291       }
292       else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
293       {
294         noValueColour = NO_VALUE_NONE;
295         tok = gcol.nextToken();
296         tok = gcol.nextToken();
297       }
298
299       gcol.nextToken(); // skip next '|'
300       if (tok.toLowerCase().startsWith(ABSOLUTE))
301       {
302         minval = gcol.nextToken();
303         gcol.nextToken(); // skip next '|'
304         autoScaled = false;
305       }
306       else
307       {
308         minval = tok;
309       }
310       maxval = gcol.nextToken();
311       if (gcol.hasMoreTokens())
312       {
313         gcol.nextToken(); // skip next '|'
314       }
315       try
316       {
317         if (minval.length() > 0)
318         {
319           min = new Float(minval).floatValue();
320         }
321       } catch (Exception e)
322       {
323         throw new IllegalArgumentException(
324                 "Couldn't parse the minimum value for graduated colour ('"
325                         + minval + "')");
326       }
327       try
328       {
329         if (maxval.length() > 0)
330         {
331           max = new Float(maxval).floatValue();
332         }
333       } catch (Exception e)
334       {
335         throw new IllegalArgumentException(
336                 "Couldn't parse the maximum value for graduated colour ("
337                         + descriptor + ")");
338       }
339     }
340     else
341     {
342       /*
343        * dummy min/max colours for colour by text
344        * (label or attribute value)
345        */
346       mincol = "white";
347       maxcol = "black";
348       byLabel = true;
349     }
350
351     /*
352      * construct the FeatureColour!
353      */
354     FeatureColour featureColour;
355     try
356     {
357       Color minColour = ColorUtils.parseColourString(mincol);
358       Color maxColour = ColorUtils.parseColourString(maxcol);
359       Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
360               : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
361       featureColour = new FeatureColour(minColour, maxColour, noColour, min,
362               max);
363       featureColour.setColourByLabel(minColour == null);
364       featureColour.setAutoScaled(autoScaled);
365       if (byAttribute)
366       {
367         featureColour.setAttributeName(
368                 FeatureMatcher.fromAttributeDisplayName(attName));
369       }
370       // add in any additional parameters
371       String ttype = null, tval = null;
372       if (gcol.hasMoreTokens())
373       {
374         // threshold type and possibly a threshold value
375         ttype = gcol.nextToken();
376         if (ttype.toLowerCase().startsWith(BELOW))
377         {
378           featureColour.setBelowThreshold(true);
379         }
380         else if (ttype.toLowerCase().startsWith(ABOVE))
381         {
382           featureColour.setAboveThreshold(true);
383         }
384         else
385         {
386           if (!ttype.toLowerCase().startsWith("no"))
387           {
388             System.err.println(
389                     "Ignoring unrecognised threshold type : " + ttype);
390           }
391         }
392       }
393       if (featureColour.hasThreshold())
394       {
395         try
396         {
397           gcol.nextToken();
398           tval = gcol.nextToken();
399           featureColour.setThreshold(new Float(tval).floatValue());
400         } catch (Exception e)
401         {
402           System.err.println("Couldn't parse threshold value as a float: ("
403                   + tval + ")");
404         }
405       }
406       if (gcol.hasMoreTokens())
407       {
408         System.err.println(
409                 "Ignoring additional tokens in parameters in graduated colour specification\n");
410         while (gcol.hasMoreTokens())
411         {
412           System.err.println(BAR + gcol.nextToken());
413         }
414         System.err.println("\n");
415       }
416       return featureColour;
417     } catch (Exception e)
418     {
419       throw new IllegalArgumentException(e.getMessage());
420     }
421   }
422
423   /**
424    * Default constructor
425    */
426   public FeatureColour()
427   {
428     this((Color) null);
429   }
430
431   /**
432    * Constructor given a simple colour
433    * 
434    * @param c
435    */
436   public FeatureColour(Color c)
437   {
438     minColour = Color.WHITE;
439     maxColour = Color.BLACK;
440     noColour = DEFAULT_NO_COLOUR;
441     minRed = 0f;
442     minGreen = 0f;
443     minBlue = 0f;
444     deltaRed = 0f;
445     deltaGreen = 0f;
446     deltaBlue = 0f;
447     colour = c;
448   }
449
450   /**
451    * Constructor given a colour range and a score range, defaulting 'no value
452    * colour' to be the same as minimum colour
453    * 
454    * @param low
455    * @param high
456    * @param min
457    * @param max
458    */
459   public FeatureColour(Color low, Color high, float min, float max)
460   {
461     this(low, high, low, min, max);
462   }
463
464   /**
465    * Copy constructor
466    * 
467    * @param fc
468    */
469   public FeatureColour(FeatureColour fc)
470   {
471     graduatedColour = fc.graduatedColour;
472     colour = fc.colour;
473     minColour = fc.minColour;
474     maxColour = fc.maxColour;
475     noColour = fc.noColour;
476     minRed = fc.minRed;
477     minGreen = fc.minGreen;
478     minBlue = fc.minBlue;
479     deltaRed = fc.deltaRed;
480     deltaGreen = fc.deltaGreen;
481     deltaBlue = fc.deltaBlue;
482     base = fc.base;
483     range = fc.range;
484     isHighToLow = fc.isHighToLow;
485     attributeName = fc.attributeName;
486     setAboveThreshold(fc.isAboveThreshold());
487     setBelowThreshold(fc.isBelowThreshold());
488     setThreshold(fc.getThreshold());
489     setAutoScaled(fc.isAutoScaled());
490     setColourByLabel(fc.isColourByLabel());
491   }
492
493   /**
494    * Copy constructor with new min/max ranges
495    * 
496    * @param fc
497    * @param min
498    * @param max
499    */
500   public FeatureColour(FeatureColour fc, float min, float max)
501   {
502     this(fc);
503     updateBounds(min, max);
504   }
505
506   /**
507    * Constructor for a graduated colour
508    * 
509    * @param low
510    * @param high
511    * @param noValueColour
512    * @param min
513    * @param max
514    */
515   public FeatureColour(Color low, Color high, Color noValueColour,
516           float min, float max)
517   {
518     if (low == null)
519     {
520       low = Color.white;
521     }
522     if (high == null)
523     {
524       high = Color.black;
525     }
526     graduatedColour = true;
527     colour = null;
528     minColour = low;
529     maxColour = high;
530     noColour = noValueColour;
531     threshold = Float.NaN;
532     isHighToLow = min >= max;
533     minRed = low.getRed() / 255f;
534     minGreen = low.getGreen() / 255f;
535     minBlue = low.getBlue() / 255f;
536     deltaRed = (high.getRed() / 255f) - minRed;
537     deltaGreen = (high.getGreen() / 255f) - minGreen;
538     deltaBlue = (high.getBlue() / 255f) - minBlue;
539     if (isHighToLow)
540     {
541       base = max;
542       range = min - max;
543     }
544     else
545     {
546       base = min;
547       range = max - min;
548     }
549   }
550
551   @Override
552   public boolean isGraduatedColour()
553   {
554     return graduatedColour;
555   }
556
557   /**
558    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
559    * false.
560    */
561   void setGraduatedColour(boolean b)
562   {
563     graduatedColour = b;
564     if (b)
565     {
566       setColourByLabel(false);
567     }
568   }
569
570   @Override
571   public Color getColour()
572   {
573     return colour;
574   }
575
576   @Override
577   public Color getMinColour()
578   {
579     return minColour;
580   }
581
582   @Override
583   public Color getMaxColour()
584   {
585     return maxColour;
586   }
587
588   @Override
589   public Color getNoColour()
590   {
591     return noColour;
592   }
593
594   @Override
595   public boolean isColourByLabel()
596   {
597     return colourByLabel;
598   }
599
600   /**
601    * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
602    * false.
603    */
604   @Override
605   public void setColourByLabel(boolean b)
606   {
607     colourByLabel = b;
608     if (b)
609     {
610       setGraduatedColour(false);
611     }
612   }
613
614   @Override
615   public boolean isBelowThreshold()
616   {
617     return belowThreshold;
618   }
619
620   @Override
621   public void setBelowThreshold(boolean b)
622   {
623     belowThreshold = b;
624     if (b)
625     {
626       setAboveThreshold(false);
627     }
628   }
629
630   @Override
631   public boolean isAboveThreshold()
632   {
633     return aboveThreshold;
634   }
635
636   @Override
637   public void setAboveThreshold(boolean b)
638   {
639     aboveThreshold = b;
640     if (b)
641     {
642       setBelowThreshold(false);
643     }
644   }
645
646   @Override
647   public float getThreshold()
648   {
649     return threshold;
650   }
651
652   @Override
653   public void setThreshold(float f)
654   {
655     threshold = f;
656   }
657
658   @Override
659   public boolean isAutoScaled()
660   {
661     return autoScaled;
662   }
663
664   @Override
665   public void setAutoScaled(boolean b)
666   {
667     this.autoScaled = b;
668   }
669
670   /**
671    * {@inheritDoc}
672    */
673   @Override
674   public void updateBounds(float min, float max)
675   {
676     if (max < min)
677     {
678       base = max;
679       range = min - max;
680       isHighToLow = true;
681     }
682     else
683     {
684       base = min;
685       range = max - min;
686       isHighToLow = false;
687     }
688   }
689
690   /**
691    * Returns the colour for the given instance of the feature. This may be a
692    * simple colour, a colour generated from the feature description (if
693    * isColourByLabel()), or a colour derived from the feature score (if
694    * isGraduatedColour()).
695    * 
696    * @param feature
697    * @return
698    */
699   @Override
700   public Color getColor(SequenceFeature feature)
701   {
702     if (isColourByLabel())
703     {
704       String label = attributeName == null ? feature.getDescription()
705               : feature.getValueAsString(attributeName);
706       return label == null ? noColour : ColorUtils
707               .createColourFromName(label);
708     }
709
710     if (!isGraduatedColour())
711     {
712       return getColour();
713     }
714
715     /*
716      * graduated colour case, optionally with threshold
717      * may be based on feature score on an attribute value
718      * Float.NaN, or no value, is assigned the 'no value' colour
719      */
720     float scr = feature.getScore();
721     if (attributeName != null)
722     {
723       try
724       {
725         String attVal = feature.getValueAsString(attributeName);
726         scr = Float.valueOf(attVal);
727       } catch (Throwable e)
728       {
729         scr = Float.NaN;
730       }
731     }
732     if (Float.isNaN(scr))
733     {
734       return noColour;
735     }
736
737     if (isAboveThreshold() && scr <= threshold)
738     {
739       return null;
740     }
741
742     if (isBelowThreshold() && scr >= threshold)
743     {
744       return null;
745     }
746     if (range == 0.0)
747     {
748       return getMaxColour();
749     }
750     float scl = (scr - base) / range;
751     if (isHighToLow)
752     {
753       scl = -scl;
754     }
755     if (scl < 0f)
756     {
757       scl = 0f;
758     }
759     if (scl > 1f)
760     {
761       scl = 1f;
762     }
763     return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
764             minBlue + scl * deltaBlue);
765   }
766
767   /**
768    * Returns the maximum score of the graduated colour range
769    * 
770    * @return
771    */
772   @Override
773   public float getMax()
774   {
775     // regenerate the original values passed in to the constructor
776     return (isHighToLow) ? base : (base + range);
777   }
778
779   /**
780    * Returns the minimum score of the graduated colour range
781    * 
782    * @return
783    */
784   @Override
785   public float getMin()
786   {
787     // regenerate the original value passed in to the constructor
788     return (isHighToLow) ? (base + range) : base;
789   }
790
791   @Override
792   public boolean isSimpleColour()
793   {
794     return (!isColourByLabel() && !isGraduatedColour());
795   }
796
797   @Override
798   public boolean hasThreshold()
799   {
800     return isAboveThreshold() || isBelowThreshold();
801   }
802
803   @Override
804   public String toJalviewFormat(String featureType)
805   {
806     String colourString = null;
807     if (isSimpleColour())
808     {
809       colourString = Format.getHexString(getColour());
810     }
811     else
812     {
813       StringBuilder sb = new StringBuilder(32);
814       if (isColourByAttribute())
815       {
816         sb.append(ATTRIBUTE).append(BAR);
817         sb.append(
818                 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
819       }
820       else if (isColourByLabel())
821       {
822         sb.append(LABEL);
823       }
824       else
825       {
826         sb.append(SCORE);
827       }
828       if (isGraduatedColour())
829       {
830         sb.append(BAR).append(Format.getHexString(getMinColour()))
831                 .append(BAR);
832         sb.append(Format.getHexString(getMaxColour())).append(BAR);
833         String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
834                 : (maxColour.equals(noColour) ? NO_VALUE_MAX
835                         : NO_VALUE_NONE);
836         sb.append(noValue).append(BAR);
837         if (!isAutoScaled())
838         {
839           sb.append(ABSOLUTE).append(BAR);
840         }
841       }
842       else
843       {
844         /*
845          * colour by text with score threshold: empty fields for
846          * minColour and maxColour (not used)
847          */
848         if (hasThreshold())
849         {
850           sb.append(BAR).append(BAR).append(BAR);
851         }
852       }
853       if (hasThreshold() || isGraduatedColour())
854       {
855         sb.append(getMin()).append(BAR);
856         sb.append(getMax()).append(BAR);
857         if (isBelowThreshold())
858         {
859           sb.append(BELOW).append(BAR).append(getThreshold());
860         }
861         else if (isAboveThreshold())
862         {
863           sb.append(ABOVE).append(BAR).append(getThreshold());
864         }
865         else
866         {
867           sb.append("none");
868         }
869       }
870       colourString = sb.toString();
871     }
872     return String.format("%s\t%s", featureType, colourString);
873   }
874
875   @Override
876   public boolean isColourByAttribute()
877   {
878     return attributeName != null;
879   }
880
881   @Override
882   public String[] getAttributeName()
883   {
884     return attributeName;
885   }
886
887   @Override
888   public void setAttributeName(String... name)
889   {
890     attributeName = name;
891   }
892
893 }