/*
* 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 represents a colour scheme for a feature type. Options supported
* are currently
*
* - a simple colour e.g. Red
* - colour by label - a colour is generated from the feature description
* - graduated colour by feature score
*
* - minimum and maximum score range must be provided
* - minimum and maximum value colours should be specified
* - a colour for 'no value' may optionally be provided
* - colours for intermediate scores are interpolated RGB values
* - there is an optional threshold above/below which to colour values
* - the range may be the full value range, or may be limited by the threshold
* value
*
* - colour by (text) value of a named attribute
- graduated colour by
* (numeric) value of a named attribute
*/
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;
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:
*
* - red
* - a28bbb
* - 25,125,213
* - label
* - attribute|CSQ:PolyPhen
* - label|||0.0|0.0|above|12.5
* - label|||0.0|0.0|below|12.5
* - red|green|12.0|26.0|none
* - score|red|green|12.0|26.0|none
* - attribute|AF|red|green|12.0|26.0|none
* - attribute|AF|red|green|noValueNone|12.0|26.0|none
* - a28bbb|3eb555|12.0|26.0|above|12.5
* - a28bbb|3eb555|abso|12.0|26.0|below|12.5
*
*
* @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 = new Float(minval).floatValue();
}
} catch (Exception e)
{
throw new IllegalArgumentException(
"Couldn't parse the minimum value for graduated colour ("
+ descriptor + ")");
}
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(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());
}
}
/**
* Default constructor
*/
public FeatureColour()
{
this((Color) null);
}
/**
* Constructor given a simple colour
*
* @param c
*/
public FeatureColour(Color c)
{
minColour = Color.WHITE;
maxColour = Color.BLACK;
noColour = DEFAULT_NO_COLOUR;
minRed = 0f;
minGreen = 0f;
minBlue = 0f;
deltaRed = 0f;
deltaGreen = 0f;
deltaBlue = 0f;
colour = c;
}
/**
* Constructor given a colour range and a score range, defaulting 'no value
* colour' to be the same as minimum colour
*
* @param low
* @param high
* @param min
* @param max
*/
public FeatureColour(Color low, Color high, float min, float max)
{
this(low, high, low, min, max);
}
/**
* 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());
}
/**
* Copy constructor with new min/max ranges
*
* @param fc
* @param min
* @param max
*/
public FeatureColour(FeatureColour fc, float min, float max)
{
this(fc);
updateBounds(min, max);
}
/**
* Constructor for a graduated colour
*
* @param low
* @param high
* @param noValueColour
* @param min
* @param max
*/
public FeatureColour(Color low, Color high, Color noValueColour,
float min, float max)
{
if (low == null)
{
low = Color.white;
}
if (high == null)
{
high = Color.black;
}
graduatedColour = true;
colour = null;
minColour = low;
maxColour = high;
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.
*/
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 (if
* isColourByLabel()), or a colour derived from the feature score (if
* isGraduatedColour()).
*
* @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);
String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
: (maxColour.equals(noColour) ? NO_VALUE_MAX
: 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;
}
}