JAL-2069 add attributeName and noValueColour to FeatureColour
[jalview.git] / src / jalview / schemes / FeatureColour.java
index ce382c3..df4ea39 100644 (file)
@@ -1,25 +1,88 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+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
+ * <ul>
+ * <li>a simple colour e.g. Red</li>
+ * <li>colour by label - a colour is generated from the feature description</li>
+ * <li>graduated colour by feature score</li>
+ * <ul>
+ * <li>minimum and maximum score range must be provided</li>
+ * <li>minimum and maximum value colours should be specified</li>
+ * <li>a colour for 'no value' may optionally be provided</li>
+ * <li>colours for intermediate scores are interpolated RGB values</li>
+ * <li>there is an optional threshold above/below which to colour values</li>
+ * <li>the range may be the full value range, or may be limited by the threshold
+ * value</li>
+ * </ul>
+ * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
+ * (numeric) value of a named attribute</li> </ul>
  */
 public class FeatureColour implements FeatureColourI
 {
+  static final Color DEFAULT_NO_COLOUR = Color.LIGHT_GRAY;
+
+  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 this named attribute is used for
+   * colourByLabel or graduatedColour
+   */
+  private String byAttributeName;
+
   private float threshold;
 
   private float base;
@@ -49,6 +112,204 @@ public class FeatureColour implements FeatureColourI
   final private float deltaBlue;
 
   /**
+   * Parses a Jalview features file format colour descriptor
+   * [label|][mincolour|maxcolour
+   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
+   * <ul>
+   * <li>red</li>
+   * <li>a28bbb</li>
+   * <li>25,125,213</li>
+   * <li>label</li>
+   * <li>label|||0.0|0.0|above|12.5</li>
+   * <li>label|||0.0|0.0|below|12.5</li>
+   * <li>red|green|12.0|26.0|none</li>
+   * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
+   * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
+   * </ul>
+   * 
+   * @param descriptor
+   * @return
+   * @throws IllegalArgumentException
+   *           if not parseable
+   */
+  public static FeatureColour parseJalviewFeatureColour(String descriptor)
+  {
+    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
+    float min = Float.MIN_VALUE;
+    float max = Float.MAX_VALUE;
+    boolean labelColour = false;
+
+    String mincol = gcol.nextToken();
+    if (mincol == "|")
+    {
+      throw new IllegalArgumentException(
+              "Expected either 'label' or a colour specification in the line: "
+                      + descriptor);
+    }
+    String maxcol = null;
+    if (mincol.toLowerCase().indexOf("label") == 0)
+    {
+      labelColour = true;
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      // skip '|'
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+
+    if (!labelColour && !gcol.hasMoreTokens())
+    {
+      /*
+       * only a simple colour specification - parse it
+       */
+      Color colour = ColorUtils.parseColourString(descriptor);
+      if (colour == null)
+      {
+        throw new IllegalArgumentException(
+                "Invalid colour descriptor: " + descriptor);
+      }
+      return new FeatureColour(colour);
+    }
+
+    /*
+     * 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;
+    if (mincol != null)
+    {
+      // at least four more tokens
+      if (mincol.equals("|"))
+      {
+        mincol = "";
+      }
+      else
+      {
+        gcol.nextToken(); // skip next '|'
+      }
+      maxcol = gcol.nextToken();
+      if (maxcol.equals("|"))
+      {
+        maxcol = "";
+      }
+      else
+      {
+        gcol.nextToken(); // skip next '|'
+      }
+      tok = gcol.nextToken();
+      gcol.nextToken(); // skip next '|'
+      if (tok.toLowerCase().startsWith("abso"))
+      {
+        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
+    {
+      // add in some dummy min/max colours for the label-only
+      // colourscheme.
+      mincol = "FFFFFF";
+      maxcol = "000000";
+    }
+
+    /*
+     * construct the FeatureColour
+     */
+    FeatureColour featureColour;
+    try
+    {
+      Color minColour = ColorUtils.parseColourString(mincol);
+      Color maxColour = ColorUtils.parseColourString(maxcol);
+      featureColour = new FeatureColour(minColour, maxColour, min, max);
+      featureColour.setColourByLabel(labelColour);
+      featureColour.setAutoScaled(autoScaled);
+      // 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("|" + gcol.nextToken());
+        }
+        System.err.println("\n");
+      }
+      return featureColour;
+    } catch (Exception e)
+    {
+      throw new IllegalArgumentException(e.getMessage());
+    }
+  }
+
+  /**
    * Default constructor
    */
   public FeatureColour()
@@ -63,8 +324,9 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color c)
   {
-    minColour = null;
-    maxColour = null;
+    minColour = Color.WHITE;
+    maxColour = Color.BLACK;
+    noColour = DEFAULT_NO_COLOUR;
     minRed = 0f;
     minGreen = 0f;
     minBlue = 0f;
@@ -75,7 +337,8 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Constructor given a colour range and a score range
+   * Constructor given a colour range and a score range, defaulting 'no value
+   * colour' to be the same as minimum colour
    * 
    * @param low
    * @param high
@@ -84,28 +347,7 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color low, Color high, float min, float max)
   {
-    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;
-    }
+    this(low, high, low, min, max);
   }
 
   /**
@@ -115,9 +357,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,15 +371,17 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
+    byAttributeName = fc.byAttributeName;
     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
@@ -143,10 +389,46 @@ public class FeatureColour implements FeatureColourI
   public FeatureColour(FeatureColour fc, float min, float max)
   {
     this(fc);
-    graduatedColour = true;
+    setGraduatedColour(true);
     updateBounds(min, 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()
   {
@@ -157,8 +439,7 @@ 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)
+  void setGraduatedColour(boolean b)
   {
     graduatedColour = b;
     if (b)
@@ -186,6 +467,12 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
+  public Color getNoColour()
+  {
+    return noColour;
+  }
+
+  @Override
   public boolean isColourByLabel()
   {
     return colourByLabel;
@@ -204,6 +491,7 @@ public class FeatureColour implements FeatureColourI
       setGraduatedColour(false);
     }
   }
+
   @Override
   public boolean isBelowThreshold()
   {
@@ -273,10 +561,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)
@@ -309,8 +594,10 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      return UserColourScheme
-              .createColourFromName(feature.getDescription());
+      String label = byAttributeName == null ? feature.getDescription()
+              : feature.getValueAsString(byAttributeName);
+      return label == null ? noColour : ColorUtils
+              .createColourFromName(label);
     }
 
     if (!isGraduatedColour())
@@ -318,15 +605,41 @@ 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 is assigned minimum visible score colour
+     * no such attribute is assigned the 'no value' colour
+     */
+    float scr = feature.getScore();
+    if (byAttributeName != null)
     {
-      return getMaxColour();
+      try
+      {
+        String attVal = feature.getValueAsString(byAttributeName);
+        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 +654,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 +682,84 @@ 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 (isColourByLabel())
+      {
+        sb.append("label");
+        if (hasThreshold())
+        {
+          sb.append(BAR).append(BAR).append(BAR);
+        }
+      }
+      if (isGraduatedColour())
+      {
+        sb.append(Format.getHexString(getMinColour())).append(BAR);
+        sb.append(Format.getHexString(getMaxColour())).append(BAR);
+        if (!isAutoScaled())
+        {
+          sb.append("abso").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 byAttributeName != null;
   }
 
   @Override
-  public boolean hasThreshold()
+  public String getAttributeName()
   {
-    return isAboveThreshold() || isBelowThreshold();
+    return byAttributeName;
+  }
+
+  @Override
+  public void setAttributeName(String name)
+  {
+    byAttributeName = name;
   }
 
 }