JAL-3438 spotless for 2.11.2.0
[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             System.err.println(
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           System.err.println("Couldn't parse threshold value as a float: ("
413                   + tval + ")");
414         }
415       }
416       if (gcol.hasMoreTokens())
417       {
418         System.err.println(
419                 "Ignoring additional tokens in parameters in graduated colour specification\n");
420         while (gcol.hasMoreTokens())
421         {
422           System.err.println(BAR + gcol.nextToken());
423         }
424         System.err.println("\n");
425       }
426       return featureColour;
427     } catch (Exception e)
428     {
429       throw new IllegalArgumentException(e.getMessage());
430     }
431   }
432
433   /**
434    * Default constructor
435    */
436   public FeatureColour()
437   {
438     this((Color) null);
439   }
440
441   /**
442    * Constructor given a simple colour. This also 'primes' a graduated colour
443    * range, where the maximum colour is the given simple colour, and the minimum
444    * colour a paler shade of it. This is for convenience when switching from a
445    * simple colour to a graduated colour scheme.
446    * 
447    * @param c
448    */
449   public FeatureColour(Color c)
450   {
451     /*
452      * set max colour to the simple colour, min colour to a paler shade of it
453      */
454     this(c, c == null ? Color.white : ColorUtils.bleachColour(c, 0.9f),
455             c == null ? Color.black : c, DEFAULT_NO_COLOUR, 0, 0);
456
457     /*
458      * but enforce simple colour for now!
459      */
460     setGraduatedColour(false);
461   }
462
463   /**
464    * Copy constructor
465    * 
466    * @param fc
467    */
468   public FeatureColour(FeatureColour fc)
469   {
470     graduatedColour = fc.graduatedColour;
471     colour = fc.colour;
472     minColour = fc.minColour;
473     maxColour = fc.maxColour;
474     noColour = fc.noColour;
475     minRed = fc.minRed;
476     minGreen = fc.minGreen;
477     minBlue = fc.minBlue;
478     deltaRed = fc.deltaRed;
479     deltaGreen = fc.deltaGreen;
480     deltaBlue = fc.deltaBlue;
481     base = fc.base;
482     range = fc.range;
483     isHighToLow = fc.isHighToLow;
484     attributeName = fc.attributeName;
485     setAboveThreshold(fc.isAboveThreshold());
486     setBelowThreshold(fc.isBelowThreshold());
487     setThreshold(fc.getThreshold());
488     setAutoScaled(fc.isAutoScaled());
489     setColourByLabel(fc.isColourByLabel());
490   }
491
492   /**
493    * Constructor that sets both simple and graduated colour values. This allows
494    * alternative colour schemes to be 'preserved' while switching between them
495    * to explore their effects on the visualisation.
496    * <p>
497    * This sets the colour scheme to 'graduated' by default. Override this if
498    * wanted by calling <code>setGraduatedColour(false)</code> for a simple
499    * colour, or <code>setColourByLabel(true)</code> for colour by label.
500    * 
501    * @param myColour
502    * @param low
503    * @param high
504    * @param noValueColour
505    * @param min
506    * @param max
507    */
508   public FeatureColour(Color myColour, Color low, Color high,
509           Color noValueColour, float min, float max)
510   {
511     if (low == null)
512     {
513       low = Color.white;
514     }
515     if (high == null)
516     {
517       high = Color.black;
518     }
519     colour = myColour;
520     minColour = low;
521     maxColour = high;
522     setGraduatedColour(true);
523     noColour = noValueColour;
524     threshold = Float.NaN;
525     isHighToLow = min >= max;
526     minRed = low.getRed() / 255f;
527     minGreen = low.getGreen() / 255f;
528     minBlue = low.getBlue() / 255f;
529     deltaRed = (high.getRed() / 255f) - minRed;
530     deltaGreen = (high.getGreen() / 255f) - minGreen;
531     deltaBlue = (high.getBlue() / 255f) - minBlue;
532     if (isHighToLow)
533     {
534       base = max;
535       range = min - max;
536     }
537     else
538     {
539       base = min;
540       range = max - min;
541     }
542   }
543
544   @Override
545   public boolean isGraduatedColour()
546   {
547     return graduatedColour;
548   }
549
550   /**
551    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
552    * false.
553    */
554   public void setGraduatedColour(boolean b)
555   {
556     graduatedColour = b;
557     if (b)
558     {
559       setColourByLabel(false);
560     }
561   }
562
563   @Override
564   public Color getColour()
565   {
566     return colour;
567   }
568
569   @Override
570   public Color getMinColour()
571   {
572     return minColour;
573   }
574
575   @Override
576   public Color getMaxColour()
577   {
578     return maxColour;
579   }
580
581   @Override
582   public Color getNoColour()
583   {
584     return noColour;
585   }
586
587   @Override
588   public boolean isColourByLabel()
589   {
590     return colourByLabel;
591   }
592
593   /**
594    * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
595    * false.
596    */
597   @Override
598   public void setColourByLabel(boolean b)
599   {
600     colourByLabel = b;
601     if (b)
602     {
603       setGraduatedColour(false);
604     }
605   }
606
607   @Override
608   public boolean isBelowThreshold()
609   {
610     return belowThreshold;
611   }
612
613   @Override
614   public void setBelowThreshold(boolean b)
615   {
616     belowThreshold = b;
617     if (b)
618     {
619       setAboveThreshold(false);
620     }
621   }
622
623   @Override
624   public boolean isAboveThreshold()
625   {
626     return aboveThreshold;
627   }
628
629   @Override
630   public void setAboveThreshold(boolean b)
631   {
632     aboveThreshold = b;
633     if (b)
634     {
635       setBelowThreshold(false);
636     }
637   }
638
639   @Override
640   public float getThreshold()
641   {
642     return threshold;
643   }
644
645   @Override
646   public void setThreshold(float f)
647   {
648     threshold = f;
649   }
650
651   @Override
652   public boolean isAutoScaled()
653   {
654     return autoScaled;
655   }
656
657   @Override
658   public void setAutoScaled(boolean b)
659   {
660     this.autoScaled = b;
661   }
662
663   /**
664    * {@inheritDoc}
665    */
666   @Override
667   public void updateBounds(float min, float max)
668   {
669     if (max < min)
670     {
671       base = max;
672       range = min - max;
673       isHighToLow = true;
674     }
675     else
676     {
677       base = min;
678       range = max - min;
679       isHighToLow = false;
680     }
681   }
682
683   /**
684    * Returns the colour for the given instance of the feature. This may be a
685    * simple colour, a colour generated from the feature description or other
686    * attribute (if isColourByLabel()), or a colour derived from the feature
687    * score or other attribute (if isGraduatedColour()).
688    * <p>
689    * Answers null if feature score (or attribute) value lies outside a
690    * configured threshold.
691    * 
692    * @param feature
693    * @return
694    */
695   @Override
696   public Color getColor(SequenceFeature feature)
697   {
698     if (isColourByLabel())
699     {
700       String label = attributeName == null ? feature.getDescription()
701               : feature.getValueAsString(attributeName);
702       return label == null ? noColour
703               : ColorUtils.createColourFromName(label);
704     }
705
706     if (!isGraduatedColour())
707     {
708       return getColour();
709     }
710
711     /*
712      * graduated colour case, optionally with threshold
713      * may be based on feature score on an attribute value
714      * Float.NaN, or no value, is assigned the 'no value' colour
715      */
716     float scr = feature.getScore();
717     if (attributeName != null)
718     {
719       try
720       {
721         String attVal = feature.getValueAsString(attributeName);
722         scr = Float.valueOf(attVal);
723       } catch (Throwable e)
724       {
725         scr = Float.NaN;
726       }
727     }
728     if (Float.isNaN(scr))
729     {
730       return noColour;
731     }
732
733     if (isAboveThreshold() && scr <= threshold)
734     {
735       return null;
736     }
737
738     if (isBelowThreshold() && scr >= threshold)
739     {
740       return null;
741     }
742     if (range == 0.0)
743     {
744       return getMaxColour();
745     }
746     float scl = (scr - base) / range;
747     if (isHighToLow)
748     {
749       scl = -scl;
750     }
751     if (scl < 0f)
752     {
753       scl = 0f;
754     }
755     if (scl > 1f)
756     {
757       scl = 1f;
758     }
759     return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
760             minBlue + scl * deltaBlue);
761   }
762
763   /**
764    * Returns the maximum score of the graduated colour range
765    * 
766    * @return
767    */
768   @Override
769   public float getMax()
770   {
771     // regenerate the original values passed in to the constructor
772     return (isHighToLow) ? base : (base + range);
773   }
774
775   /**
776    * Returns the minimum score of the graduated colour range
777    * 
778    * @return
779    */
780   @Override
781   public float getMin()
782   {
783     // regenerate the original value passed in to the constructor
784     return (isHighToLow) ? (base + range) : base;
785   }
786
787   @Override
788   public boolean isSimpleColour()
789   {
790     return (!isColourByLabel() && !isGraduatedColour());
791   }
792
793   @Override
794   public boolean hasThreshold()
795   {
796     return isAboveThreshold() || isBelowThreshold();
797   }
798
799   @Override
800   public String toJalviewFormat(String featureType)
801   {
802     String colourString = null;
803     if (isSimpleColour())
804     {
805       colourString = Format.getHexString(getColour());
806     }
807     else
808     {
809       StringBuilder sb = new StringBuilder(32);
810       if (isColourByAttribute())
811       {
812         sb.append(ATTRIBUTE).append(BAR);
813         sb.append(
814                 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
815       }
816       else if (isColourByLabel())
817       {
818         sb.append(LABEL);
819       }
820       else
821       {
822         sb.append(SCORE);
823       }
824       if (isGraduatedColour())
825       {
826         sb.append(BAR).append(Format.getHexString(getMinColour()))
827                 .append(BAR);
828         sb.append(Format.getHexString(getMaxColour())).append(BAR);
829
830         /*
831          * 'no value' colour should be null, min or max colour;
832          * if none of these, coerce to minColour
833          */
834         String noValue = NO_VALUE_MIN;
835         if (maxColour.equals(noColour))
836         {
837           noValue = NO_VALUE_MAX;
838         }
839         if (noColour == null)
840         {
841           noValue = NO_VALUE_NONE;
842         }
843         sb.append(noValue).append(BAR);
844         if (!isAutoScaled())
845         {
846           sb.append(ABSOLUTE).append(BAR);
847         }
848       }
849       else
850       {
851         /*
852          * colour by text with score threshold: empty fields for
853          * minColour and maxColour (not used)
854          */
855         if (hasThreshold())
856         {
857           sb.append(BAR).append(BAR).append(BAR);
858         }
859       }
860       if (hasThreshold() || isGraduatedColour())
861       {
862         sb.append(getMin()).append(BAR);
863         sb.append(getMax()).append(BAR);
864         if (isBelowThreshold())
865         {
866           sb.append(BELOW).append(BAR).append(getThreshold());
867         }
868         else if (isAboveThreshold())
869         {
870           sb.append(ABOVE).append(BAR).append(getThreshold());
871         }
872         else
873         {
874           sb.append("none");
875         }
876       }
877       colourString = sb.toString();
878     }
879     return String.format("%s\t%s", featureType, colourString);
880   }
881
882   @Override
883   public boolean isColourByAttribute()
884   {
885     return attributeName != null;
886   }
887
888   @Override
889   public String[] getAttributeName()
890   {
891     return attributeName;
892   }
893
894   @Override
895   public void setAttributeName(String... name)
896   {
897     attributeName = name;
898   }
899
900   @Override
901   public boolean isOutwithThreshold(SequenceFeature feature)
902   {
903     if (!isGraduatedColour())
904     {
905       return false;
906     }
907     float scr = feature.getScore();
908     if (attributeName != null)
909     {
910       try
911       {
912         String attVal = feature.getValueAsString(attributeName);
913         scr = Float.valueOf(attVal);
914       } catch (Throwable e)
915       {
916         scr = Float.NaN;
917       }
918     }
919     if (Float.isNaN(scr))
920     {
921       return false;
922     }
923
924     return ((isAboveThreshold() && scr <= threshold)
925             || (isBelowThreshold() && scr >= threshold));
926   }
927
928   @Override
929   public String getDescription()
930   {
931     if (isSimpleColour())
932     {
933       return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b="
934               + colour.getBlue();
935     }
936     StringBuilder tt = new StringBuilder();
937     String by = null;
938
939     if (getAttributeName() != null)
940     {
941       by = FeatureMatcher.toAttributeDisplayName(getAttributeName());
942     }
943     else if (isColourByLabel())
944     {
945       by = I18N_LABEL;
946     }
947     else
948     {
949       by = I18N_SCORE;
950     }
951     tt.append(MessageManager.formatMessage("action.by_title_param", by));
952
953     /*
954      * add threshold if any
955      */
956     if (isAboveThreshold() || isBelowThreshold())
957     {
958       tt.append(" (");
959       if (isColourByLabel())
960       {
961         /*
962          * Jalview features file supports the combination of 
963          * colour by label or attribute text with score threshold
964          */
965         tt.append(I18N_SCORE).append(" ");
966       }
967       tt.append(isAboveThreshold() ? "> " : "< ");
968       tt.append(getThreshold()).append(")");
969     }
970
971     return tt.toString();
972   }
973
974 }