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