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