X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fschemes%2FFeatureColour.java;h=c73e32bdfea0551c4c267a5e6fe6978e64f2f47b;hb=3c8a25936a2d805e7e3d7ab82f83b13135406d18;hp=ce382c3af84267a67a7884241a3020e8beecd530;hpb=0b1c761dfaa8242f122cf868e8897a06ec6eb727;p=jalview.git diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index ce382c3..c73e32b 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -1,25 +1,110 @@ +/* + * 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 java.awt.Color; +import java.util.StringTokenizer; /** - * A class that wraps either a simple colour or a graduated colour + * A class that represents a colour scheme for a feature type. Options supported + * are currently + * */ public class FeatureColour implements FeatureColourI { + 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; @@ -30,8 +115,6 @@ public class FeatureColour implements FeatureColourI private boolean aboveThreshold; - private boolean thresholdIsMinOrMax; - private boolean isHighToLow; private boolean autoScaled; @@ -49,63 +132,322 @@ public class FeatureColour implements FeatureColourI final private float deltaBlue; /** - * Default constructor + * 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 FeatureColour() + public static FeatureColourI parseJalviewFeatureColour(String descriptor) { - this((Color) null); + 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 = new Float(minval).floatValue(); + } + } catch (Exception e) + { + throw new IllegalArgumentException( + "Couldn't parse the minimum value for graduated colour ('" + + minval + "')"); + } + try + { + if (maxval.length() > 0) + { + max = new Float(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(new Float(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()); + } } /** - * Constructor given a simple colour - * - * @param c + * Default constructor */ - public FeatureColour(Color c) + public FeatureColour() { - minColour = null; - maxColour = null; - minRed = 0f; - minGreen = 0f; - minBlue = 0f; - deltaRed = 0f; - deltaGreen = 0f; - deltaBlue = 0f; - colour = c; + this((Color) null); } /** - * Constructor given a colour range and a score range + * 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 low - * @param high - * @param min - * @param max + * @param c */ - public FeatureColour(Color low, Color high, float min, float max) + public FeatureColour(Color c) { - graduatedColour = true; - colour = null; - minColour = low; - maxColour = high; - 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; - } + /* + * 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); } /** @@ -115,9 +457,11 @@ public class FeatureColour implements FeatureColourI */ 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; @@ -127,24 +471,64 @@ public class FeatureColour implements FeatureColourI 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()); } - + /** - * Copy constructor with new min/max ranges - * @param fc + * 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(FeatureColour fc, float min, float max) + public FeatureColour(Color myColour, Color low, Color high, + Color noValueColour, float min, float max) { - this(fc); - graduatedColour = true; - updateBounds(min, 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 @@ -157,7 +541,6 @@ public class FeatureColour implements FeatureColourI * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to * false. */ - @Override public void setGraduatedColour(boolean b) { graduatedColour = b; @@ -186,6 +569,12 @@ public class FeatureColour implements FeatureColourI } @Override + public Color getNoColour() + { + return noColour; + } + + @Override public boolean isColourByLabel() { return colourByLabel; @@ -204,6 +593,7 @@ public class FeatureColour implements FeatureColourI setGraduatedColour(false); } } + @Override public boolean isBelowThreshold() { @@ -237,18 +627,6 @@ public class FeatureColour implements FeatureColourI } @Override - public boolean isThresholdMinMax() - { - return thresholdIsMinOrMax; - } - - @Override - public void setThresholdMinMax(boolean b) - { - thresholdIsMinOrMax = b; - } - - @Override public float getThreshold() { return threshold; @@ -273,10 +651,7 @@ public class FeatureColour implements FeatureColourI } /** - * Updates the base and range appropriately for the given minmax range - * - * @param min - * @param max + * {@inheritDoc} */ @Override public void updateBounds(float min, float max) @@ -297,9 +672,12 @@ public class FeatureColour implements FeatureColourI /** * Returns the colour for the given instance of the feature. This may be a - * simple colour, a colour generated from the feature description (if - * isColourByLabel()), or a colour derived from the feature score (if - * isGraduatedColour()). + * 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 @@ -309,8 +687,10 @@ public class FeatureColour implements FeatureColourI { if (isColourByLabel()) { - return UserColourScheme - .createColourFromName(feature.getDescription()); + String label = attributeName == null ? feature.getDescription() + : feature.getValueAsString(attributeName); + return label == null ? noColour : ColorUtils + .createColourFromName(label); } if (!isGraduatedColour()) @@ -318,15 +698,40 @@ public class FeatureColour implements FeatureColourI return getColour(); } - // todo should we check for above/below threshold here? - if (range == 0.0) + /* + * 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) { - return getMaxColour(); + try + { + String attVal = feature.getValueAsString(attributeName); + scr = Float.valueOf(attVal); + } catch (Throwable e) + { + scr = Float.NaN; + } } - float scr = feature.getScore(); if (Float.isNaN(scr)) { - return getMinColour(); + 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) @@ -341,7 +746,8 @@ public class FeatureColour implements FeatureColourI { scl = 1f; } - return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, minBlue + scl * deltaBlue); + return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, + minBlue + scl * deltaBlue); } /** @@ -368,54 +774,145 @@ public class FeatureColour implements FeatureColourI return (isHighToLow) ? (base + range) : base; } - /** - * Answers true if the feature has a simple colour, or is coloured by label, - * or has a graduated colour and the score of this feature instance is within - * the range to render (if any), i.e. does not lie below or above any - * threshold set. - * - * @param feature - * @return - */ @Override - public boolean isColored(SequenceFeature feature) + public boolean isSimpleColour() { - if (isColourByLabel() || !isGraduatedColour()) - { - return true; - } + return (!isColourByLabel() && !isGraduatedColour()); + } - float val = feature.getScore(); - if (Float.isNaN(val)) - { - return true; - } - if (Float.isNaN(this.threshold)) - { - return true; - } + @Override + public boolean hasThreshold() + { + return isAboveThreshold() || isBelowThreshold(); + } - if (isAboveThreshold() && val <= threshold) + @Override + public String toJalviewFormat(String featureType) + { + String colourString = null; + if (isSimpleColour()) { - return false; + colourString = Format.getHexString(getColour()); } - if (isBelowThreshold() && val >= threshold) + else { - return false; + 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 true; + return String.format("%s\t%s", featureType, colourString); } @Override - public boolean isSimpleColour() + public boolean isColourByAttribute() { - return (!isColourByLabel() && !isGraduatedColour()); + return attributeName != null; } @Override - public boolean hasThreshold() + public String[] getAttributeName() { - return isAboveThreshold() || isBelowThreshold(); + 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)); } }