JAL-2876 some Checkstyle and other review comments addressed
[jalview.git] / src / jalview / schemes / FeatureColour.java
index ce382c3..2007f8b 100644 (file)
@@ -1,20 +1,55 @@
+/*
+ * 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
  */
 public class FeatureColour implements FeatureColourI
 {
-  final private Color colour;
+  private static final float _255F = 255f;
+
+  private static final String ABOVE = "above";
+
+  private static final String BELOW = "below";
+
+  private static final String ABSO = "abso";
+
+  private static final String LABEL = "label";
 
-  final private Color minColour;
+  private static final String PIPE = "|";
 
-  final private Color maxColour;
+  private final Color colour;
+
+  private final Color minColour;
+
+  private final Color maxColour;
 
   private boolean graduatedColour;
 
@@ -36,17 +71,17 @@ public class FeatureColour implements FeatureColourI
 
   private boolean autoScaled;
 
-  final private float minRed;
+  private final float minRed;
 
-  final private float minGreen;
+  private final float minGreen;
 
-  final private float minBlue;
+  private final float minBlue;
 
-  final private float deltaRed;
+  private final float deltaRed;
 
-  final private float deltaGreen;
+  private final float deltaGreen;
 
-  final private float deltaBlue;
+  private final float deltaBlue;
 
   /**
    * Default constructor
@@ -63,8 +98,8 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color c)
   {
-    minColour = null;
-    maxColour = null;
+    minColour = Color.WHITE;
+    maxColour = Color.BLACK;
     minRed = 0f;
     minGreen = 0f;
     minBlue = 0f;
@@ -86,16 +121,16 @@ public class FeatureColour implements FeatureColourI
   {
     graduatedColour = true;
     colour = null;
-    minColour = low;
-    maxColour = high;
+    minColour = low == null ? Color.WHITE : low;
+    maxColour = high == null ? Color.BLACK : 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;
+    minRed = minColour.getRed() / _255F;
+    minGreen = minColour.getGreen() / _255F;
+    minBlue = minColour.getBlue() / _255F;
+    deltaRed = (maxColour.getRed() / _255F) - minRed;
+    deltaGreen = (maxColour.getGreen() / _255F) - minGreen;
+    deltaBlue = (maxColour.getBlue() / _255F) - minBlue;
     if (isHighToLow)
     {
       base = max;
@@ -115,6 +150,7 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(FeatureColour fc)
   {
+    graduatedColour = fc.graduatedColour;
     colour = fc.colour;
     minColour = fc.minColour;
     maxColour = fc.maxColour;
@@ -133,9 +169,10 @@ public class FeatureColour implements FeatureColourI
     setAutoScaled(fc.isAutoScaled());
     setColourByLabel(fc.isColourByLabel());
   }
-  
+
   /**
    * Copy constructor with new min/max ranges
+   * 
    * @param fc
    * @param min
    * @param max
@@ -147,6 +184,207 @@ public class FeatureColour implements FeatureColourI
     updateBounds(min, max);
   }
 
+  /**
+   * 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, PIPE, true);
+    float min = Float.MIN_VALUE;
+    float max = Float.MAX_VALUE;
+    boolean labelColour = false;
+
+    String mincol = gcol.nextToken();
+    if (mincol == PIPE)
+    {
+      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;
+    String minval;
+    String maxval;
+    if (mincol != null)
+    {
+      // at least four more tokens
+      if (mincol.equals(PIPE))
+      {
+        mincol = "";
+      }
+      else
+      {
+        gcol.nextToken(); // skip next '|'
+      }
+      maxcol = gcol.nextToken();
+      if (maxcol.equals(PIPE))
+      {
+        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 (NumberFormatException 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 (NumberFormatException 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;
+      String 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 (NumberFormatException 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(PIPE + gcol.nextToken());
+        }
+        System.err.println("\n");
+      }
+      return featureColour;
+    } catch (Exception e)
+    {
+      throw new IllegalArgumentException(e.getMessage());
+    }
+  }
+
   @Override
   public boolean isGraduatedColour()
   {
@@ -157,8 +395,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)
@@ -204,6 +441,7 @@ public class FeatureColour implements FeatureColourI
       setGraduatedColour(false);
     }
   }
+
   @Override
   public boolean isBelowThreshold()
   {
@@ -296,52 +534,70 @@ 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()).
+   * {@inheritDoc}
    * 
    * @param feature
    * @return
    */
   @Override
-  public Color getColor(SequenceFeature feature)
+  public Color getColour(SequenceFeature feature)
   {
-    if (isColourByLabel())
-    {
-      return UserColourScheme
-              .createColourFromName(feature.getDescription());
-    }
-
-    if (!isGraduatedColour())
+    if (isSimpleColour())
     {
       return getColour();
     }
 
-    // todo should we check for above/below threshold here?
-    if (range == 0.0)
+    /*
+     * return null if score is outwith any threshold
+     * (for graduated colour or colour by label)
+     */
+    float scr = feature.getScore();
+    boolean isNan = Float.isNaN(scr);
+    if (!isNan)
     {
-      return getMaxColour();
+      boolean isAbove = isAboveThreshold() && scr <= threshold;
+      boolean isBelow = !isNan && isBelowThreshold() && scr >= threshold;
+      if (isAbove || isBelow)
+      {
+        return null;
+      }
     }
-    float scr = feature.getScore();
-    if (Float.isNaN(scr))
+
+    if (isColourByLabel())
     {
-      return getMinColour();
+      return ColorUtils.createColourFromName(feature.getDescription());
     }
-    float scl = (scr - base) / range;
-    if (isHighToLow)
+
+    Color result = null;
+
+    if (isNan)
     {
-      scl = -scl;
+      result = getMinColour();
     }
-    if (scl < 0f)
+    else if (range == 0.0)
     {
-      scl = 0f;
+      result = getMaxColour();
     }
-    if (scl > 1f)
+    else
     {
-      scl = 1f;
+      float scl = (scr - base) / range;
+      if (isHighToLow)
+      {
+        scl = -scl;
+      }
+      if (scl < 0f)
+      {
+        scl = 0f;
+      }
+      if (scl > 1f)
+      {
+        scl = 1f;
+      }
+      result = new Color(minRed + scl * deltaRed,
+              minGreen + scl * deltaGreen, minBlue + scl * deltaBlue);
     }
-    return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, minBlue + scl * deltaBlue);
+
+    return result;
   }
 
   /**
@@ -368,44 +624,6 @@ 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)
-  {
-    if (isColourByLabel() || !isGraduatedColour())
-    {
-      return true;
-    }
-
-    float val = feature.getScore();
-    if (Float.isNaN(val))
-    {
-      return true;
-    }
-    if (Float.isNaN(this.threshold))
-    {
-      return true;
-    }
-
-    if (isAboveThreshold() && val <= threshold)
-    {
-      return false;
-    }
-    if (isBelowThreshold() && val >= threshold)
-    {
-      return false;
-    }
-    return true;
-  }
-
   @Override
   public boolean isSimpleColour()
   {
@@ -418,4 +636,54 @@ public class FeatureColour implements FeatureColourI
     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 (isColourByLabel())
+      {
+        sb.append(LABEL);
+        if (hasThreshold())
+        {
+          sb.append(PIPE).append(PIPE).append(PIPE);
+        }
+      }
+      if (isGraduatedColour())
+      {
+        sb.append(Format.getHexString(getMinColour())).append(PIPE);
+        sb.append(Format.getHexString(getMaxColour())).append(PIPE);
+        if (!isAutoScaled())
+        {
+          sb.append(ABSO).append(PIPE);
+        }
+      }
+      if (hasThreshold() || isGraduatedColour())
+      {
+        sb.append(getMin()).append(PIPE);
+        sb.append(getMax()).append(PIPE);
+        if (isBelowThreshold())
+        {
+          sb.append(BELOW).append(PIPE).append(getThreshold());
+        }
+        else if (isAboveThreshold())
+        {
+          sb.append(ABOVE).append(PIPE).append(getThreshold());
+        }
+        else
+        {
+          sb.append("none");
+        }
+      }
+      colourString = sb.toString();
+    }
+    return String.format("%s\t%s", featureType, colourString);
+  }
+
 }