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