2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.schemes;
23 import java.util.Locale;
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;
32 import java.awt.Color;
33 import java.util.StringTokenizer;
36 * A class that represents a colour scheme for a feature type. Options supported
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>
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
51 * <li>colour by (text) value of a named attribute</li>
52 * <li>graduated colour by (numeric) value of a named attribute</li>
55 public class FeatureColour implements FeatureColourI
57 private static final String I18N_LABEL = MessageManager
58 .getString("label.label");
60 private static final String I18N_SCORE = MessageManager
61 .getString("label.score");
63 private static final String ABSOLUTE = "abso";
65 private static final String ABOVE = "above";
67 private static final String BELOW = "below";
70 * constants used to read or write a Jalview Features file
72 private static final String LABEL = "label";
74 private static final String SCORE = "score";
76 private static final String ATTRIBUTE = "attribute";
78 private static final String NO_VALUE_MIN = "noValueMin";
80 private static final String NO_VALUE_MAX = "noValueMax";
82 private static final String NO_VALUE_NONE = "noValueNone";
84 static final Color DEFAULT_NO_COLOUR = null;
86 private static final String BAR = "|";
88 final private Color colour;
90 final private Color minColour;
92 final private Color maxColour;
95 * colour to use for colour by attribute when the
96 * attribute value is absent
98 final private Color noColour;
101 * if true, then colour has a gradient based on a numerical
102 * range (either feature score, or an attribute value)
104 private boolean graduatedColour;
107 * if true, colour values are generated from a text string,
108 * either feature description, or an attribute value
110 private boolean colourByLabel;
113 * if not null, the value of [attribute, [sub-attribute] ...]
114 * is used for colourByLabel or graduatedColour
116 private String[] attributeName;
118 private float threshold;
124 private boolean belowThreshold;
126 private boolean aboveThreshold;
128 private boolean isHighToLow;
130 private boolean autoScaled;
132 final private float minRed;
134 final private float minGreen;
136 final private float minBlue;
138 final private float deltaRed;
140 final private float deltaGreen;
142 final private float deltaBlue;
145 * Parses a Jalview features file format colour descriptor
148 * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
149 * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
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.
160 * <li>25,125,213</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>
175 * @throws IllegalArgumentException
178 public static FeatureColourI parseJalviewFeatureColour(String descriptor)
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;
190 * first token should be 'label', or 'score', or an
191 * attribute name, or simple colour, or minimum colour
193 String nextToken = gcol.nextToken();
194 if (nextToken == BAR)
196 throw new IllegalArgumentException(
197 "Expected either 'label' or a colour specification in the line: "
200 if (nextToken.toLowerCase(Locale.ROOT).startsWith(LABEL))
203 // get the token after the next delimiter:
204 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
205 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
207 else if (nextToken.toLowerCase(Locale.ROOT).startsWith(SCORE))
209 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
210 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
212 else if (nextToken.toLowerCase(Locale.ROOT).startsWith(ATTRIBUTE))
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);
226 * if only one token, it can validly be label, attributeName,
227 * or a plain colour value
229 if (!gcol.hasMoreTokens())
231 if (byLabel || byAttribute)
233 FeatureColourI fc = new FeatureColour();
234 fc.setColourByLabel(true);
238 FeatureMatcher.fromAttributeDisplayName(attName));
243 Color colour = ColorUtils.parseColourString(descriptor);
246 throw new IllegalArgumentException(
247 "Invalid colour descriptor: " + descriptor);
249 return new FeatureColour(colour);
253 * continue parsing for min/max/no colour (if graduated)
254 * and for threshold (colour by text or graduated)
258 * autoScaled == true: colours range over actual score range
259 * autoScaled == false ('abso'): colours range over min/max range
261 boolean autoScaled = true;
262 String tok = null, minval, maxval;
263 String noValueColour = NO_VALUE_MIN;
267 // at least four more tokens
268 if (mincol.equals(BAR))
274 gcol.nextToken(); // skip next '|'
276 maxcol = gcol.nextToken();
277 if (maxcol.equals(BAR))
283 gcol.nextToken(); // skip next '|'
285 tok = gcol.nextToken();
288 * check for specifier for colour for no attribute value
289 * (new in 2.11, defaults to minColour if not specified)
291 if (tok.equalsIgnoreCase(NO_VALUE_MIN))
293 tok = gcol.nextToken();
294 tok = gcol.nextToken();
296 else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
298 noValueColour = NO_VALUE_MAX;
299 tok = gcol.nextToken();
300 tok = gcol.nextToken();
302 else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
304 noValueColour = NO_VALUE_NONE;
305 tok = gcol.nextToken();
306 tok = gcol.nextToken();
309 gcol.nextToken(); // skip next '|'
310 if (tok.toLowerCase(Locale.ROOT).startsWith(ABSOLUTE))
312 minval = gcol.nextToken();
313 gcol.nextToken(); // skip next '|'
320 maxval = gcol.nextToken();
321 if (gcol.hasMoreTokens())
323 gcol.nextToken(); // skip next '|'
327 if (minval.length() > 0)
329 min = Float.valueOf(minval).floatValue();
331 } catch (Exception e)
333 throw new IllegalArgumentException(
334 "Couldn't parse the minimum value for graduated colour ('"
339 if (maxval.length() > 0)
341 max = Float.valueOf(maxval).floatValue();
343 } catch (Exception e)
345 throw new IllegalArgumentException(
346 "Couldn't parse the maximum value for graduated colour ("
353 * dummy min/max colours for colour by text
354 * (label or attribute value)
362 * construct the FeatureColour!
364 FeatureColour featureColour;
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,
373 featureColour.setColourByLabel(minColour == null);
374 featureColour.setAutoScaled(autoScaled);
377 featureColour.setAttributeName(
378 FeatureMatcher.fromAttributeDisplayName(attName));
380 // add in any additional parameters
381 String ttype = null, tval = null;
382 if (gcol.hasMoreTokens())
384 // threshold type and possibly a threshold value
385 ttype = gcol.nextToken();
386 if (ttype.toLowerCase(Locale.ROOT).startsWith(BELOW))
388 featureColour.setBelowThreshold(true);
390 else if (ttype.toLowerCase(Locale.ROOT).startsWith(ABOVE))
392 featureColour.setAboveThreshold(true);
396 if (!ttype.toLowerCase(Locale.ROOT).startsWith("no"))
398 jalview.bin.Console.errPrintln(
399 "Ignoring unrecognised threshold type : " + ttype);
403 if (featureColour.hasThreshold())
408 tval = gcol.nextToken();
409 featureColour.setThreshold(Float.valueOf(tval).floatValue());
410 } catch (Exception e)
412 jalview.bin.Console.errPrintln("Couldn't parse threshold value as a float: ("
416 if (gcol.hasMoreTokens())
418 jalview.bin.Console.errPrintln(
419 "Ignoring additional tokens in parameters in graduated colour specification\n");
420 while (gcol.hasMoreTokens())
422 jalview.bin.Console.errPrintln(BAR + gcol.nextToken());
424 jalview.bin.Console.errPrintln("\n");
426 return featureColour;
427 } catch (Exception e)
429 throw new IllegalArgumentException(e.getMessage());
434 * Default constructor
436 public FeatureColour()
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.
449 public FeatureColour(Color c)
452 * set max colour to the simple colour, min colour to a paler shade of it
454 this(c, c == null ? Color.white : ColorUtils.bleachColour(c, 0.9f),
455 c == null ? Color.black : c, DEFAULT_NO_COLOUR, 0, 0);
458 * but enforce simple colour for now!
460 setGraduatedColour(false);
468 public FeatureColour(FeatureColour fc)
470 graduatedColour = fc.graduatedColour;
472 minColour = fc.minColour;
473 maxColour = fc.maxColour;
474 noColour = fc.noColour;
476 minGreen = fc.minGreen;
477 minBlue = fc.minBlue;
478 deltaRed = fc.deltaRed;
479 deltaGreen = fc.deltaGreen;
480 deltaBlue = fc.deltaBlue;
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());
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.
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.
504 * @param noValueColour
508 public FeatureColour(Color myColour, Color low, Color high,
509 Color noValueColour, float min, float max)
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;
545 public boolean isGraduatedColour()
547 return graduatedColour;
551 * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
554 public void setGraduatedColour(boolean b)
559 setColourByLabel(false);
564 public Color getColour()
570 public Color getMinColour()
576 public Color getMaxColour()
582 public Color getNoColour()
588 public boolean isColourByLabel()
590 return colourByLabel;
594 * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
598 public void setColourByLabel(boolean b)
603 setGraduatedColour(false);
608 public boolean isBelowThreshold()
610 return belowThreshold;
614 public void setBelowThreshold(boolean b)
619 setAboveThreshold(false);
624 public boolean isAboveThreshold()
626 return aboveThreshold;
630 public void setAboveThreshold(boolean b)
635 setBelowThreshold(false);
640 public float getThreshold()
646 public void setThreshold(float f)
652 public boolean isAutoScaled()
658 public void setAutoScaled(boolean b)
667 public void updateBounds(float min, float max)
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()).
689 * Answers null if feature score (or attribute) value lies outside a
690 * configured threshold.
696 public Color getColor(SequenceFeature feature)
698 if (isColourByLabel())
700 String label = attributeName == null ? feature.getDescription()
701 : feature.getValueAsString(attributeName);
702 return label == null ? noColour
703 : ColorUtils.createColourFromName(label);
706 if (!isGraduatedColour())
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
716 float scr = feature.getScore();
717 if (attributeName != null)
721 String attVal = feature.getValueAsString(attributeName);
722 scr = Float.valueOf(attVal);
723 } catch (Throwable e)
728 if (Float.isNaN(scr))
733 if (isAboveThreshold() && scr <= threshold)
738 if (isBelowThreshold() && scr >= threshold)
744 return getMaxColour();
746 float scl = (scr - base) / range;
759 return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
760 minBlue + scl * deltaBlue);
764 * Returns the maximum score of the graduated colour range
769 public float getMax()
771 // regenerate the original values passed in to the constructor
772 return (isHighToLow) ? base : (base + range);
776 * Returns the minimum score of the graduated colour range
781 public float getMin()
783 // regenerate the original value passed in to the constructor
784 return (isHighToLow) ? (base + range) : base;
788 public boolean isSimpleColour()
790 return (!isColourByLabel() && !isGraduatedColour());
794 public boolean hasThreshold()
796 return isAboveThreshold() || isBelowThreshold();
800 public String toJalviewFormat(String featureType)
802 String colourString = null;
803 if (isSimpleColour())
805 colourString = Format.getHexString(getColour());
809 StringBuilder sb = new StringBuilder(32);
810 if (isColourByAttribute())
812 sb.append(ATTRIBUTE).append(BAR);
814 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
816 else if (isColourByLabel())
824 if (isGraduatedColour())
826 sb.append(BAR).append(Format.getHexString(getMinColour()))
828 sb.append(Format.getHexString(getMaxColour())).append(BAR);
831 * 'no value' colour should be null, min or max colour;
832 * if none of these, coerce to minColour
834 String noValue = NO_VALUE_MIN;
835 if (maxColour.equals(noColour))
837 noValue = NO_VALUE_MAX;
839 if (noColour == null)
841 noValue = NO_VALUE_NONE;
843 sb.append(noValue).append(BAR);
846 sb.append(ABSOLUTE).append(BAR);
852 * colour by text with score threshold: empty fields for
853 * minColour and maxColour (not used)
857 sb.append(BAR).append(BAR).append(BAR);
860 if (hasThreshold() || isGraduatedColour())
862 sb.append(getMin()).append(BAR);
863 sb.append(getMax()).append(BAR);
864 if (isBelowThreshold())
866 sb.append(BELOW).append(BAR).append(getThreshold());
868 else if (isAboveThreshold())
870 sb.append(ABOVE).append(BAR).append(getThreshold());
877 colourString = sb.toString();
879 return String.format("%s\t%s", featureType, colourString);
883 public boolean isColourByAttribute()
885 return attributeName != null;
889 public String[] getAttributeName()
891 return attributeName;
895 public void setAttributeName(String... name)
897 attributeName = name;
901 public boolean isOutwithThreshold(SequenceFeature feature)
903 if (!isGraduatedColour())
907 float scr = feature.getScore();
908 if (attributeName != null)
912 String attVal = feature.getValueAsString(attributeName);
913 scr = Float.valueOf(attVal);
914 } catch (Throwable e)
919 if (Float.isNaN(scr))
924 return ((isAboveThreshold() && scr <= threshold)
925 || (isBelowThreshold() && scr >= threshold));
929 public String getDescription()
931 if (isSimpleColour())
933 return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b="
936 StringBuilder tt = new StringBuilder();
939 if (getAttributeName() != null)
941 by = FeatureMatcher.toAttributeDisplayName(getAttributeName());
943 else if (isColourByLabel())
951 tt.append(MessageManager.formatMessage("action.by_title_param", by));
954 * add threshold if any
956 if (isAboveThreshold() || isBelowThreshold())
959 if (isColourByLabel())
962 * Jalview features file supports the combination of
963 * colour by label or attribute text with score threshold
965 tt.append(I18N_SCORE).append(" ");
967 tt.append(isAboveThreshold() ? "> " : "< ");
968 tt.append(getThreshold()).append(")");
971 return tt.toString();