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