/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.schemes; import jalview.api.FeatureColourI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.features.FeatureMatcher; import jalview.util.ColorUtils; import jalview.util.Format; import jalview.util.MessageManager; import java.awt.Color; import java.util.StringTokenizer; /** * A class that represents a colour scheme for a feature type. Options supported * are currently * */ public class FeatureColour implements FeatureColourI { private static final String I18N_LABEL = MessageManager .getString("label.label"); private static final String I18N_SCORE = MessageManager .getString("label.score"); private static final String ABSOLUTE = "abso"; private static final String ABOVE = "above"; private static final String BELOW = "below"; /* * constants used to read or write a Jalview Features file */ private static final String LABEL = "label"; private static final String SCORE = "score"; private static final String ATTRIBUTE = "attribute"; private static final String NO_VALUE_MIN = "noValueMin"; private static final String NO_VALUE_MAX = "noValueMax"; private static final String NO_VALUE_NONE = "noValueNone"; static final Color DEFAULT_NO_COLOUR = null; private static final String BAR = "|"; final private Color colour; final private Color minColour; final private Color maxColour; /* * colour to use for colour by attribute when the * attribute value is absent */ final private Color noColour; /* * if true, then colour has a gradient based on a numerical * range (either feature score, or an attribute value) */ private boolean graduatedColour; /* * if true, colour values are generated from a text string, * either feature description, or an attribute value */ private boolean colourByLabel; /* * if not null, the value of [attribute, [sub-attribute] ...] * is used for colourByLabel or graduatedColour */ private String[] attributeName; private float threshold; private float base; private float range; private boolean belowThreshold; private boolean aboveThreshold; private boolean isHighToLow; private boolean autoScaled; final private float minRed; final private float minGreen; final private float minBlue; final private float deltaRed; final private float deltaGreen; final private float deltaBlue; /** * Parses a Jalview features file format colour descriptor *

* * [label|score|[attribute|attributeName]|][mincolour|maxcolour| * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue] *

* 'Score' is optional (default) for a graduated colour. An attribute with * sub-attribute should be written as (for example) CSQ:Consequence. * noValueOption is one of noValueMin, noValueMax, noValueNone * with default noValueMin. *

* Examples: *

* * @param descriptor * @return * @throws IllegalArgumentException * if not parseable */ public static FeatureColourI parseJalviewFeatureColour(String descriptor) { StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true); float min = Float.MIN_VALUE; float max = Float.MAX_VALUE; boolean byLabel = false; boolean byAttribute = false; String attName = null; String mincol = null; String maxcol = null; /* * first token should be 'label', or 'score', or an * attribute name, or simple colour, or minimum colour */ String nextToken = gcol.nextToken(); if (nextToken == BAR) { throw new IllegalArgumentException( "Expected either 'label' or a colour specification in the line: " + descriptor); } if (nextToken.toLowerCase().startsWith(LABEL)) { byLabel = true; // get the token after the next delimiter: mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); } else if (nextToken.toLowerCase().startsWith(SCORE)) { mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); } else if (nextToken.toLowerCase().startsWith(ATTRIBUTE)) { byAttribute = true; attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); } else { mincol = nextToken; } /* * if only one token, it can validly be label, attributeName, * or a plain colour value */ if (!gcol.hasMoreTokens()) { if (byLabel || byAttribute) { FeatureColourI fc = new FeatureColour(); fc.setColourByLabel(true); if (byAttribute) { fc.setAttributeName( FeatureMatcher.fromAttributeDisplayName(attName)); } return fc; } Color colour = ColorUtils.parseColourString(descriptor); if (colour == null) { throw new IllegalArgumentException( "Invalid colour descriptor: " + descriptor); } return new FeatureColour(colour); } /* * continue parsing for min/max/no colour (if graduated) * and for threshold (colour by text or graduated) */ /* * autoScaled == true: colours range over actual score range * autoScaled == false ('abso'): colours range over min/max range */ boolean autoScaled = true; String tok = null, minval, maxval; String noValueColour = NO_VALUE_MIN; if (mincol != null) { // at least four more tokens if (mincol.equals(BAR)) { mincol = null; } else { gcol.nextToken(); // skip next '|' } maxcol = gcol.nextToken(); if (maxcol.equals(BAR)) { maxcol = null; } else { gcol.nextToken(); // skip next '|' } tok = gcol.nextToken(); /* * check for specifier for colour for no attribute value * (new in 2.11, defaults to minColour if not specified) */ if (tok.equalsIgnoreCase(NO_VALUE_MIN)) { tok = gcol.nextToken(); tok = gcol.nextToken(); } else if (tok.equalsIgnoreCase(NO_VALUE_MAX)) { noValueColour = NO_VALUE_MAX; tok = gcol.nextToken(); tok = gcol.nextToken(); } else if (tok.equalsIgnoreCase(NO_VALUE_NONE)) { noValueColour = NO_VALUE_NONE; tok = gcol.nextToken(); tok = gcol.nextToken(); } gcol.nextToken(); // skip next '|' if (tok.toLowerCase().startsWith(ABSOLUTE)) { minval = gcol.nextToken(); gcol.nextToken(); // skip next '|' autoScaled = false; } else { minval = tok; } maxval = gcol.nextToken(); if (gcol.hasMoreTokens()) { gcol.nextToken(); // skip next '|' } try { if (minval.length() > 0) { min = Float.valueOf(minval).floatValue(); } } catch (Exception e) { throw new IllegalArgumentException( "Couldn't parse the minimum value for graduated colour ('" + minval + "')"); } try { if (maxval.length() > 0) { max = Float.valueOf(maxval).floatValue(); } } catch (Exception e) { throw new IllegalArgumentException( "Couldn't parse the maximum value for graduated colour (" + descriptor + ")"); } } else { /* * dummy min/max colours for colour by text * (label or attribute value) */ mincol = "white"; maxcol = "black"; byLabel = true; } /* * construct the FeatureColour! */ FeatureColour featureColour; try { Color minColour = ColorUtils.parseColourString(mincol); Color maxColour = ColorUtils.parseColourString(maxcol); Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour); featureColour = new FeatureColour(maxColour, minColour, maxColour, noColour, min, max); featureColour.setColourByLabel(minColour == null); featureColour.setAutoScaled(autoScaled); if (byAttribute) { featureColour.setAttributeName( FeatureMatcher.fromAttributeDisplayName(attName)); } // add in any additional parameters String ttype = null, tval = null; if (gcol.hasMoreTokens()) { // threshold type and possibly a threshold value ttype = gcol.nextToken(); if (ttype.toLowerCase().startsWith(BELOW)) { featureColour.setBelowThreshold(true); } else if (ttype.toLowerCase().startsWith(ABOVE)) { featureColour.setAboveThreshold(true); } else { if (!ttype.toLowerCase().startsWith("no")) { System.err.println( "Ignoring unrecognised threshold type : " + ttype); } } } if (featureColour.hasThreshold()) { try { gcol.nextToken(); tval = gcol.nextToken(); featureColour.setThreshold(Float.valueOf(tval).floatValue()); } catch (Exception e) { System.err.println("Couldn't parse threshold value as a float: (" + tval + ")"); } } if (gcol.hasMoreTokens()) { System.err.println( "Ignoring additional tokens in parameters in graduated colour specification\n"); while (gcol.hasMoreTokens()) { System.err.println(BAR + gcol.nextToken()); } System.err.println("\n"); } return featureColour; } catch (Exception e) { throw new IllegalArgumentException(e.getMessage()); } } /** * Default constructor */ public FeatureColour() { this((Color) null); } /** * Constructor given a simple colour. This also 'primes' a graduated colour * range, where the maximum colour is the given simple colour, and the minimum * colour a paler shade of it. This is for convenience when switching from a * simple colour to a graduated colour scheme. * * @param c */ public FeatureColour(Color c) { /* * set max colour to the simple colour, min colour to a paler shade of it */ this(c, c == null ? Color.white : ColorUtils.bleachColour(c, 0.9f), c == null ? Color.black : c, DEFAULT_NO_COLOUR, 0, 0); /* * but enforce simple colour for now! */ setGraduatedColour(false); } /** * Copy constructor * * @param fc */ public FeatureColour(FeatureColour fc) { graduatedColour = fc.graduatedColour; colour = fc.colour; minColour = fc.minColour; maxColour = fc.maxColour; noColour = fc.noColour; minRed = fc.minRed; minGreen = fc.minGreen; minBlue = fc.minBlue; deltaRed = fc.deltaRed; deltaGreen = fc.deltaGreen; deltaBlue = fc.deltaBlue; base = fc.base; range = fc.range; isHighToLow = fc.isHighToLow; attributeName = fc.attributeName; setAboveThreshold(fc.isAboveThreshold()); setBelowThreshold(fc.isBelowThreshold()); setThreshold(fc.getThreshold()); setAutoScaled(fc.isAutoScaled()); setColourByLabel(fc.isColourByLabel()); } /** * Constructor that sets both simple and graduated colour values. This allows * alternative colour schemes to be 'preserved' while switching between them * to explore their effects on the visualisation. *

* This sets the colour scheme to 'graduated' by default. Override this if * wanted by calling setGraduatedColour(false) for a simple * colour, or setColourByLabel(true) for colour by label. * * @param myColour * @param low * @param high * @param noValueColour * @param min * @param max */ public FeatureColour(Color myColour, Color low, Color high, Color noValueColour, float min, float max) { if (low == null) { low = Color.white; } if (high == null) { high = Color.black; } colour = myColour; minColour = low; maxColour = high; setGraduatedColour(true); noColour = noValueColour; threshold = Float.NaN; isHighToLow = min >= max; minRed = low.getRed() / 255f; minGreen = low.getGreen() / 255f; minBlue = low.getBlue() / 255f; deltaRed = (high.getRed() / 255f) - minRed; deltaGreen = (high.getGreen() / 255f) - minGreen; deltaBlue = (high.getBlue() / 255f) - minBlue; if (isHighToLow) { base = max; range = min - max; } else { base = min; range = max - min; } } @Override public boolean isGraduatedColour() { return graduatedColour; } /** * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to * false. */ public void setGraduatedColour(boolean b) { graduatedColour = b; if (b) { setColourByLabel(false); } } @Override public Color getColour() { return colour; } @Override public Color getMinColour() { return minColour; } @Override public Color getMaxColour() { return maxColour; } @Override public Color getNoColour() { return noColour; } @Override public boolean isColourByLabel() { return colourByLabel; } /** * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to * false. */ @Override public void setColourByLabel(boolean b) { colourByLabel = b; if (b) { setGraduatedColour(false); } } @Override public boolean isBelowThreshold() { return belowThreshold; } @Override public void setBelowThreshold(boolean b) { belowThreshold = b; if (b) { setAboveThreshold(false); } } @Override public boolean isAboveThreshold() { return aboveThreshold; } @Override public void setAboveThreshold(boolean b) { aboveThreshold = b; if (b) { setBelowThreshold(false); } } @Override public float getThreshold() { return threshold; } @Override public void setThreshold(float f) { threshold = f; } @Override public boolean isAutoScaled() { return autoScaled; } @Override public void setAutoScaled(boolean b) { this.autoScaled = b; } /** * {@inheritDoc} */ @Override public void updateBounds(float min, float max) { if (max < min) { base = max; range = min - max; isHighToLow = true; } else { base = min; range = max - min; isHighToLow = false; } } /** * Returns the colour for the given instance of the feature. This may be a * simple colour, a colour generated from the feature description or other * attribute (if isColourByLabel()), or a colour derived from the feature * score or other attribute (if isGraduatedColour()). *

* Answers null if feature score (or attribute) value lies outside a * configured threshold. * * @param feature * @return */ @Override public Color getColor(SequenceFeature feature) { if (isColourByLabel()) { String label = attributeName == null ? feature.getDescription() : feature.getValueAsString(attributeName); return label == null ? noColour : ColorUtils .createColourFromName(label); } if (!isGraduatedColour()) { return getColour(); } /* * graduated colour case, optionally with threshold * may be based on feature score on an attribute value * Float.NaN, or no value, is assigned the 'no value' colour */ float scr = feature.getScore(); if (attributeName != null) { try { String attVal = feature.getValueAsString(attributeName); scr = Float.valueOf(attVal); } catch (Throwable e) { scr = Float.NaN; } } if (Float.isNaN(scr)) { return noColour; } if (isAboveThreshold() && scr <= threshold) { return null; } if (isBelowThreshold() && scr >= threshold) { return null; } if (range == 0.0) { return getMaxColour(); } float scl = (scr - base) / range; if (isHighToLow) { scl = -scl; } if (scl < 0f) { scl = 0f; } if (scl > 1f) { scl = 1f; } return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, minBlue + scl * deltaBlue); } /** * Returns the maximum score of the graduated colour range * * @return */ @Override public float getMax() { // regenerate the original values passed in to the constructor return (isHighToLow) ? base : (base + range); } /** * Returns the minimum score of the graduated colour range * * @return */ @Override public float getMin() { // regenerate the original value passed in to the constructor return (isHighToLow) ? (base + range) : base; } @Override public boolean isSimpleColour() { return (!isColourByLabel() && !isGraduatedColour()); } @Override public boolean hasThreshold() { return isAboveThreshold() || isBelowThreshold(); } @Override public String toJalviewFormat(String featureType) { String colourString = null; if (isSimpleColour()) { colourString = Format.getHexString(getColour()); } else { StringBuilder sb = new StringBuilder(32); if (isColourByAttribute()) { sb.append(ATTRIBUTE).append(BAR); sb.append( FeatureMatcher.toAttributeDisplayName(getAttributeName())); } else if (isColourByLabel()) { sb.append(LABEL); } else { sb.append(SCORE); } if (isGraduatedColour()) { sb.append(BAR).append(Format.getHexString(getMinColour())) .append(BAR); sb.append(Format.getHexString(getMaxColour())).append(BAR); /* * 'no value' colour should be null, min or max colour; * if none of these, coerce to minColour */ String noValue = NO_VALUE_MIN; if (maxColour.equals(noColour)) { noValue = NO_VALUE_MAX; } if (noColour == null) { noValue = NO_VALUE_NONE; } sb.append(noValue).append(BAR); if (!isAutoScaled()) { sb.append(ABSOLUTE).append(BAR); } } else { /* * colour by text with score threshold: empty fields for * minColour and maxColour (not used) */ if (hasThreshold()) { sb.append(BAR).append(BAR).append(BAR); } } if (hasThreshold() || isGraduatedColour()) { sb.append(getMin()).append(BAR); sb.append(getMax()).append(BAR); if (isBelowThreshold()) { sb.append(BELOW).append(BAR).append(getThreshold()); } else if (isAboveThreshold()) { sb.append(ABOVE).append(BAR).append(getThreshold()); } else { sb.append("none"); } } colourString = sb.toString(); } return String.format("%s\t%s", featureType, colourString); } @Override public boolean isColourByAttribute() { return attributeName != null; } @Override public String[] getAttributeName() { return attributeName; } @Override public void setAttributeName(String... name) { attributeName = name; } @Override public boolean isOutwithThreshold(SequenceFeature feature) { if (!isGraduatedColour()) { return false; } float scr = feature.getScore(); if (attributeName != null) { try { String attVal = feature.getValueAsString(attributeName); scr = Float.valueOf(attVal); } catch (Throwable e) { scr = Float.NaN; } } if (Float.isNaN(scr)) { return false; } return ((isAboveThreshold() && scr <= threshold) || (isBelowThreshold() && scr >= threshold)); } @Override public String getDescription() { if (isSimpleColour()) { return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b=" + colour.getBlue(); } StringBuilder tt = new StringBuilder(); String by = null; if (getAttributeName() != null) { by = FeatureMatcher.toAttributeDisplayName(getAttributeName()); } else if (isColourByLabel()) { by = I18N_LABEL; } else { by = I18N_SCORE; } tt.append(MessageManager.formatMessage("action.by_title_param", by)); /* * add threshold if any */ if (isAboveThreshold() || isBelowThreshold()) { tt.append(" ("); if (isColourByLabel()) { /* * Jalview features file supports the combination of * colour by label or attribute text with score threshold */ tt.append(I18N_SCORE).append(" "); } tt.append(isAboveThreshold() ? "> " : "< "); tt.append(getThreshold()).append(")"); } return tt.toString(); } }