Merge branch 'bug/JAL-3120restoreFeatureColour' into merge/JAL-3120
[jalview.git] / src / jalview / schemes / FeatureColour.java
index 168ab54..c73e32b 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureMatcher;
 import jalview.util.ColorUtils;
 import jalview.util.Format;
 
@@ -49,7 +50,28 @@ import java.util.StringTokenizer;
  */
 public class FeatureColour implements FeatureColourI
 {
-  static final Color DEFAULT_NO_COLOUR = Color.LIGHT_GRAY;
+  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 = "|";
 
@@ -78,10 +100,10 @@ public class FeatureColour implements FeatureColourI
   private boolean colourByLabel;
 
   /*
-   * if not null, the value of this named attribute is used for
-   * colourByLabel or graduatedColour
+   * if not null, the value of [attribute, [sub-attribute] ...]
+   *  is used for colourByLabel or graduatedColour
    */
-  private String byAttributeName;
+  private String[] attributeName;
 
   private float threshold;
 
@@ -93,8 +115,6 @@ public class FeatureColour implements FeatureColourI
 
   private boolean aboveThreshold;
 
-  private boolean thresholdIsMinOrMax;
-
   private boolean isHighToLow;
 
   private boolean autoScaled;
@@ -113,16 +133,29 @@ public class FeatureColour implements FeatureColourI
 
   /**
    * Parses a Jalview features file format colour descriptor
-   * [label|][mincolour|maxcolour
-   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
+   * <p>
+   * <code>
+   * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
+   * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
+   * <p>
+   * '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 <code>noValueMin, noValueMax, noValueNone</code>
+   * with default noValueMin.
+   * <p>
+   * Examples:
    * <ul>
    * <li>red</li>
    * <li>a28bbb</li>
    * <li>25,125,213</li>
    * <li>label</li>
+   * <li>attribute|CSQ:PolyPhen</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>score|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|noValueNone|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>
@@ -132,34 +165,71 @@ public class FeatureColour implements FeatureColourI
    * @throws IllegalArgumentException
    *           if not parseable
    */
-  public static FeatureColour parseJalviewFeatureColour(String descriptor)
+  public static FeatureColourI parseJalviewFeatureColour(String descriptor)
   {
-    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
+    StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
     float min = Float.MIN_VALUE;
     float max = Float.MAX_VALUE;
-    boolean labelColour = false;
+    boolean byLabel = false;
+    boolean byAttribute = false;
+    String attName = null;
+    String mincol = null;
+    String maxcol = null;
 
-    String mincol = gcol.nextToken();
-    if (mincol == "|")
+    /*
+     * 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);
     }
-    String maxcol = null;
-    if (mincol.toLowerCase().indexOf("label") == 0)
+    if (nextToken.toLowerCase().startsWith(LABEL))
     {
-      labelColour = true;
+      byLabel = true;
+      // get the token after the next delimiter:
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
-      // skip '|'
       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 (!labelColour && !gcol.hasMoreTokens())
+    /*
+     * if only one token, it can validly be label, attributeName,
+     * or a plain colour value
+     */
+    if (!gcol.hasMoreTokens())
     {
-      /*
-       * only a simple colour specification - parse it
-       */
+      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)
       {
@@ -170,34 +240,64 @@ public class FeatureColour implements FeatureColourI
     }
 
     /*
+     * 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("|"))
+      if (mincol.equals(BAR))
       {
-        mincol = "";
+        mincol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       maxcol = gcol.nextToken();
-      if (maxcol.equals("|"))
+      if (maxcol.equals(BAR))
       {
-        maxcol = "";
+        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("abso"))
+      if (tok.toLowerCase().startsWith(ABSOLUTE))
       {
         minval = gcol.nextToken();
         gcol.nextToken(); // skip next '|'
@@ -221,8 +321,8 @@ public class FeatureColour implements FeatureColourI
       } catch (Exception e)
       {
         throw new IllegalArgumentException(
-                "Couldn't parse the minimum value for graduated colour ("
-                        + descriptor + ")");
+                "Couldn't parse the minimum value for graduated colour ('"
+                        + minval + "')");
       }
       try
       {
@@ -239,34 +339,45 @@ public class FeatureColour implements FeatureColourI
     }
     else
     {
-      // add in some dummy min/max colours for the label-only
-      // colourscheme.
-      mincol = "FFFFFF";
-      maxcol = "000000";
+      /*
+       * dummy min/max colours for colour by text
+       * (label or attribute value)
+       */
+      mincol = "white";
+      maxcol = "black";
+      byLabel = true;
     }
 
     /*
-     * construct the FeatureColour
+     * 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);
+      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"))
+        if (ttype.toLowerCase().startsWith(BELOW))
         {
           featureColour.setBelowThreshold(true);
         }
-        else if (ttype.toLowerCase().startsWith("above"))
+        else if (ttype.toLowerCase().startsWith(ABOVE))
         {
           featureColour.setAboveThreshold(true);
         }
@@ -298,7 +409,7 @@ public class FeatureColour implements FeatureColourI
                 "Ignoring additional tokens in parameters in graduated colour specification\n");
         while (gcol.hasMoreTokens())
         {
-          System.err.println("|" + gcol.nextToken());
+          System.err.println(BAR + gcol.nextToken());
         }
         System.err.println("\n");
       }
@@ -318,36 +429,25 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Constructor given a simple colour
+   * 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)
   {
-    minColour = Color.WHITE;
-    maxColour = Color.BLACK;
-    noColour = DEFAULT_NO_COLOUR;
-    minRed = 0f;
-    minGreen = 0f;
-    minBlue = 0f;
-    deltaRed = 0f;
-    deltaGreen = 0f;
-    deltaBlue = 0f;
-    colour = 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);
 
-  /**
-   * 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);
+    /*
+     * but enforce simple colour for now!
+     */
+    setGraduatedColour(false);
   }
 
   /**
@@ -371,7 +471,7 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
-    byAttributeName = fc.byAttributeName;
+    attributeName = fc.attributeName;
     setAboveThreshold(fc.isAboveThreshold());
     setBelowThreshold(fc.isBelowThreshold());
     setThreshold(fc.getThreshold());
@@ -380,20 +480,23 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Copy constructor with new min/max ranges
+   * 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.
+   * <p>
+   * This sets the colour scheme to 'graduated' by default. Override this if
+   * wanted by calling <code>setGraduatedColour(false)</code> for a simple
+   * colour, or <code>setColourByLabel(true)</code> for colour by label.
    * 
-   * @param fc
+   * @param myColour
+   * @param low
+   * @param high
+   * @param noValueColour
    * @param min
    * @param max
    */
-  public FeatureColour(FeatureColour fc, float min, float max)
-  {
-    this(fc);
-    updateBounds(min, max);
-  }
-
-  public FeatureColour(Color low, Color high, Color noValueColour,
-          float min, float max)
+  public FeatureColour(Color myColour, Color low, Color high,
+          Color noValueColour, float min, float max)
   {
     if (low == null)
     {
@@ -403,10 +506,10 @@ public class FeatureColour implements FeatureColourI
     {
       high = Color.black;
     }
-    graduatedColour = true;
-    colour = null;
+    colour = myColour;
     minColour = low;
     maxColour = high;
+    setGraduatedColour(true);
     noColour = noValueColour;
     threshold = Float.NaN;
     isHighToLow = min >= max;
@@ -438,7 +541,7 @@ public class FeatureColour implements FeatureColourI
    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
    * false.
    */
-  void setGraduatedColour(boolean b)
+  public void setGraduatedColour(boolean b)
   {
     graduatedColour = b;
     if (b)
@@ -524,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;
@@ -581,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()).
+   * <p>
+   * Answers null if feature score (or attribute) value lies outside a
+   * configured threshold.
    * 
    * @param feature
    * @return
@@ -593,8 +687,8 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      String label = byAttributeName == null ? feature.getDescription()
-              : feature.getValueAsString(byAttributeName);
+      String label = attributeName == null ? feature.getDescription()
+              : feature.getValueAsString(attributeName);
       return label == null ? noColour : ColorUtils
               .createColourFromName(label);
     }
@@ -607,15 +701,14 @@ public class FeatureColour implements FeatureColourI
     /*
      * 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.NaN, or no value, is assigned the 'no value' colour
      */
     float scr = feature.getScore();
-    if (byAttributeName != null)
+    if (attributeName != null)
     {
       try
       {
-        String attVal = feature.getValueAsString(byAttributeName);
+        String attVal = feature.getValueAsString(attributeName);
         scr = Float.valueOf(attVal);
       } catch (Throwable e)
       {
@@ -704,21 +797,54 @@ public class FeatureColour implements FeatureColourI
     else
     {
       StringBuilder sb = new StringBuilder(32);
-      if (isColourByLabel())
+      if (isColourByAttribute())
       {
-        sb.append("label");
-        if (hasThreshold())
-        {
-          sb.append(BAR).append(BAR).append(BAR);
-        }
+        sb.append(ATTRIBUTE).append(BAR);
+        sb.append(
+                FeatureMatcher.toAttributeDisplayName(getAttributeName()));
+      }
+      else if (isColourByLabel())
+      {
+        sb.append(LABEL);
+      }
+      else
+      {
+        sb.append(SCORE);
       }
       if (isGraduatedColour())
       {
-        sb.append(Format.getHexString(getMinColour())).append(BAR);
+        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("abso").append(BAR);
+          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())
@@ -727,11 +853,11 @@ public class FeatureColour implements FeatureColourI
         sb.append(getMax()).append(BAR);
         if (isBelowThreshold())
         {
-          sb.append("below").append(BAR).append(getThreshold());
+          sb.append(BELOW).append(BAR).append(getThreshold());
         }
         else if (isAboveThreshold())
         {
-          sb.append("above").append(BAR).append(getThreshold());
+          sb.append(ABOVE).append(BAR).append(getThreshold());
         }
         else
         {
@@ -746,19 +872,47 @@ public class FeatureColour implements FeatureColourI
   @Override
   public boolean isColourByAttribute()
   {
-    return byAttributeName != null;
+    return attributeName != null;
   }
 
   @Override
-  public String getAttributeName()
+  public String[] getAttributeName()
   {
-    return byAttributeName;
+    return attributeName;
   }
 
   @Override
-  public void setAttributeName(String name)
+  public void setAttributeName(String... name)
   {
-    byAttributeName = 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));
   }
 
 }