JAL-2015 parse/write Jalview feature format pushed into FeatureColour,
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 25 Feb 2016 12:43:11 +0000 (12:43 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 25 Feb 2016 12:43:11 +0000 (12:43 +0000)
tests added for same

src/jalview/api/FeatureColourI.java
src/jalview/io/FeaturesFile.java
src/jalview/schemes/FeatureColour.java
src/jalview/schemes/UserColourScheme.java
test/jalview/schemes/FeatureColourTest.java
test/jalview/schemes/UserColourSchemeTest.java [new file with mode: 0644]

index 0a333ca..2e82afd 100644 (file)
@@ -146,4 +146,11 @@ public interface FeatureColourI
    * @param max
    */
   void updateBounds(float min, float max);
+
+  /**
+   * Returns the colour in Jalview features file format
+   * 
+   * @return
+   */
+  String toJalviewFormat(String featureType);
 }
index 6bc0374..d1c33ce 100755 (executable)
@@ -27,10 +27,8 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.FeatureColour;
 import jalview.schemes.UserColourScheme;
-import jalview.util.Format;
 import jalview.util.MapList;
 
 import java.io.IOException;
@@ -218,14 +216,12 @@ public class FeaturesFile extends AlignFile
        * GFF3 file
        */
       ArrayList<SequenceI> newseqs = new ArrayList<SequenceI>();
-      String type, desc, token = null;
+      String theType, desc, token = null;
 
-      int index, start, end;
-      float score;
       StringTokenizer st;
       SequenceFeature sf;
       String featureGroup = null, groupLink = null;
-      Map typeLink = new Hashtable();
+      Map<String, String> typeLink = new Hashtable<String, String>();
       /**
        * when true, assume GFF style features rather than Jalview style.
        */
@@ -233,6 +229,7 @@ public class FeaturesFile extends AlignFile
       Map<String, String> gffProps = new HashMap<String, String>();
       while ((line = nextLine()) != null)
       {
+        int featureStart, featureEnd;
         // skip comments/process pragmas
         if (line.startsWith("#"))
         {
@@ -258,8 +255,8 @@ public class FeaturesFile extends AlignFile
         if (st.countTokens() > 1 && st.countTokens() < 4)
         {
           GFFFile = false;
-          type = st.nextToken();
-          if (type.equalsIgnoreCase("startgroup"))
+          theType = st.nextToken();
+          if (theType.equalsIgnoreCase("startgroup"))
           {
             featureGroup = st.nextToken();
             if (st.hasMoreElements())
@@ -268,7 +265,7 @@ public class FeaturesFile extends AlignFile
               featureLink.put(featureGroup, groupLink);
             }
           }
-          else if (type.equalsIgnoreCase("endgroup"))
+          else if (theType.equalsIgnoreCase("endgroup"))
           {
             // We should check whether this is the current group,
             // but at present theres no way of showing more than 1 group
@@ -278,207 +275,29 @@ public class FeaturesFile extends AlignFile
           }
           else
           {
-            FeatureColourI colour = null;
             String colscheme = st.nextToken();
-            if (colscheme.indexOf("|") > -1
-                    || colscheme.trim().equalsIgnoreCase("label"))
+            try
             {
-              // Parse '|' separated graduated colourscheme fields:
-              // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
-              // can either provide 'label' only, first is optional, next two
-              // colors are required (but may be
-              // left blank), next is optional, nxt two min/max are required.
-              // first is either 'label'
-              // first/second and third are both hexadecimal or word equivalent
-              // colour.
-              // next two are values parsed as floats.
-              // fifth is either 'above','below', or 'none'.
-              // sixth is a float value and only required when fifth is either
-              // 'above' or 'below'.
-              StringTokenizer gcol = new StringTokenizer(colscheme, "|",
-                      true);
-              // set defaults
-              int threshtype = AnnotationColourGradient.NO_THRESHOLD;
-              float min = Float.MIN_VALUE, max = Float.MAX_VALUE, threshval = Float.NaN;
-              boolean labelCol = false;
-              // Parse spec line
-              String mincol = gcol.nextToken();
-              if (mincol == "|")
-              {
-                System.err
-                        .println("Expected either 'label' or a colour specification in the line: "
-                                + line);
-                continue;
-              }
-              String maxcol = null;
-              if (mincol.toLowerCase().indexOf("label") == 0)
-              {
-                labelCol = true;
-                mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip
-                                                                           // '|'
-                mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
-              }
-              String abso = null, minval, maxval;
-              if (mincol != null)
-              {
-                // at least four more tokens
-                if (mincol.equals("|"))
-                {
-                  mincol = "";
-                }
-                else
-                {
-                  gcol.nextToken(); // skip next '|'
-                }
-                // continue parsing rest of line
-                maxcol = gcol.nextToken();
-                if (maxcol.equals("|"))
-                {
-                  maxcol = "";
-                }
-                else
-                {
-                  gcol.nextToken(); // skip next '|'
-                }
-                abso = gcol.nextToken();
-                gcol.nextToken(); // skip next '|'
-                if (abso.toLowerCase().indexOf("abso") != 0)
-                {
-                  minval = abso;
-                  abso = null;
-                }
-                else
-                {
-                  minval = gcol.nextToken();
-                  gcol.nextToken(); // skip next '|'
-                }
-                maxval = gcol.nextToken();
-                if (gcol.hasMoreTokens())
-                {
-                  gcol.nextToken(); // skip next '|'
-                }
-                try
-                {
-                  if (minval.length() > 0)
-                  {
-                    min = new Float(minval).floatValue();
-                  }
-                } catch (Exception e)
-                {
-                  System.err
-                          .println("Couldn't parse the minimum value for graduated colour for type ("
-                                  + colscheme
-                                  + ") - did you misspell 'auto' for the optional automatic colour switch ?");
-                  e.printStackTrace();
-                }
-                try
-                {
-                  if (maxval.length() > 0)
-                  {
-                    max = new Float(maxval).floatValue();
-                  }
-                } catch (Exception e)
-                {
-                  System.err
-                          .println("Couldn't parse the maximum value for graduated colour for type ("
-                                  + colscheme + ")");
-                  e.printStackTrace();
-                }
-              }
-              else
-              {
-                // add in some dummy min/max colours for the label-only
-                // colourscheme.
-                mincol = "FFFFFF";
-                maxcol = "000000";
-              }
-              try
-              {
-                colour = new FeatureColour(
-                        new UserColourScheme(mincol).findColour('A'),
-                        new UserColourScheme(maxcol).findColour('A'), min,
-                        max);
-              } catch (Exception e)
+              FeatureColourI colour = FeatureColour
+                      .parseJalviewFeatureColour(colscheme);
+              if (colour != null)
               {
-                System.err
-                        .println("Couldn't parse the graduated colour scheme ("
-                                + colscheme + ")");
-                e.printStackTrace();
+                colours.put(theType, colour);
               }
-              if (colour != null)
+              if (st.hasMoreElements())
               {
-                colour.setColourByLabel(labelCol);
-                colour.setAutoScaled(abso == null);
-                // 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"))
-                  {
-                    colour.setBelowThreshold(true);
-                  }
-                  else if (ttype.toLowerCase().startsWith("above"))
-                  {
-                    colour.setAboveThreshold(true);
-                  }
-                  else
-                  {
-                    if (!ttype.toLowerCase().startsWith("no"))
-                    {
-                      System.err
-                              .println("Ignoring unrecognised threshold type : "
-                                      + ttype);
-                    }
-                  }
-                }
-                if (colour.hasThreshold())
+                String link = st.nextToken();
+                typeLink.put(theType, link);
+                if (featureLink == null)
                 {
-                  try
-                  {
-                    gcol.nextToken();
-                    tval = gcol.nextToken();
-                    colour.setThreshold(new Float(tval).floatValue());
-                  } catch (Exception e)
-                  {
-                    System.err
-                            .println("Couldn't parse threshold value as a float: ("
-                                    + tval + ")");
-                    e.printStackTrace();
-                  }
-                }
-                // parse the thresh-is-min token ?
-                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");
+                  featureLink = new Hashtable();
                 }
+                featureLink.put(theType, link);
               }
-            }
-            else
+            } catch (IllegalArgumentException e)
             {
-              UserColourScheme ucs = new UserColourScheme(colscheme);
-              colour = new FeatureColour(ucs.findColour('A'));
-            }
-            if (colour != null)
-            {
-              colours.put(type, colour);
-            }
-            if (st.hasMoreElements())
-            {
-              String link = st.nextToken();
-              typeLink.put(type, link);
-              if (featureLink == null)
-              {
-                featureLink = new Hashtable();
-              }
-              featureLink.put(type, link);
+              System.err.println("Error parsing feature colour scheme "
+                      + colscheme + " : " + e.getMessage());
             }
           }
           continue;
@@ -502,53 +321,54 @@ public class FeaturesFile extends AlignFile
                 // could also be a source term rather than description line
                 group = new String(desc);
               }
-              type = st.nextToken();
+              theType = st.nextToken();
               try
               {
                 String stt = st.nextToken();
                 if (stt.length() == 0 || stt.equals("-"))
                 {
-                  start = 0;
+                  featureStart = 0;
                 }
                 else
                 {
-                  start = Integer.parseInt(stt);
+                  featureStart = Integer.parseInt(stt);
                 }
               } catch (NumberFormatException ex)
               {
-                start = 0;
+                featureStart = 0;
               }
               try
               {
                 String stt = st.nextToken();
                 if (stt.length() == 0 || stt.equals("-"))
                 {
-                  end = 0;
+                  featureEnd = 0;
                 }
                 else
                 {
-                  end = Integer.parseInt(stt);
+                  featureEnd = Integer.parseInt(stt);
                 }
               } catch (NumberFormatException ex)
               {
-                end = 0;
+                featureEnd = 0;
               }
               // TODO: decide if non positional feature assertion for input data
               // where end==0 is generally valid
-              if (end == 0)
+              if (featureEnd == 0)
               {
                 // treat as non-positional feature, regardless.
-                start = 0;
+                featureStart = 0;
               }
+              float score = 0f;
               try
               {
                 score = new Float(st.nextToken()).floatValue();
               } catch (NumberFormatException ex)
               {
-                score = 0;
+                // ignore
               }
 
-              sf = new SequenceFeature(type, desc, start, end, score, group);
+              sf = new SequenceFeature(theType, desc, featureStart, featureEnd, score, group);
 
               try
               {
@@ -560,11 +380,12 @@ public class FeaturesFile extends AlignFile
 
               if (st.hasMoreTokens())
               {
-                StringBuffer attributes = new StringBuffer();
+                StringBuilder attributes = new StringBuilder();
                 boolean sep = false;
                 while (st.hasMoreTokens())
                 {
-                  attributes.append((sep ? "\t" : "") + st.nextElement());
+                  attributes.append(sep ? "\t" : "").append(
+                          st.nextElement());
                   sep = true;
                 }
                 // TODO validate and split GFF2 attributes field ? parse out
@@ -616,8 +437,8 @@ public class FeaturesFile extends AlignFile
             seqId = null;
             try
             {
-              index = Integer.parseInt(st.nextToken());
-              seq = align.getSequenceAt(index);
+              int idx = Integer.parseInt(st.nextToken());
+              seq = align.getSequenceAt(idx);
             } catch (NumberFormatException ex)
             {
               seq = null;
@@ -630,27 +451,30 @@ public class FeaturesFile extends AlignFile
             break;
           }
 
-          start = Integer.parseInt(st.nextToken());
-          end = Integer.parseInt(st.nextToken());
+          featureStart = Integer.parseInt(st.nextToken());
+          featureEnd = Integer.parseInt(st.nextToken());
 
-          type = st.nextToken();
+          theType = st.nextToken();
 
-          if (!colours.containsKey(type))
+          if (!colours.containsKey(theType))
           {
             // Probably the old style groups file
-            UserColourScheme ucs = new UserColourScheme(type);
-            colours.put(type, ucs.findColour('A'));
+            colours.put(
+                    theType,
+                    new FeatureColour(UserColourScheme
+                            .getColourFromString(theType)));
           }
-          sf = new SequenceFeature(type, desc, "", start, end, featureGroup);
+          sf = new SequenceFeature(theType, desc, "", featureStart, featureEnd, featureGroup);
           if (st.hasMoreTokens())
           {
+            float score = 0f;
             try
             {
               score = new Float(st.nextToken()).floatValue();
               // update colourgradient bounds if allowed to
             } catch (NumberFormatException ex)
             {
-              score = 0;
+              // ignore
             }
             sf.setScore(score);
           }
@@ -659,9 +483,9 @@ public class FeaturesFile extends AlignFile
             sf.addLink(groupLink);
             sf.description += "%LINK%";
           }
-          if (typeLink.containsKey(type) && removeHTML)
+          if (typeLink.containsKey(theType) && removeHTML)
           {
-            sf.addLink(typeLink.get(type).toString());
+            sf.addLink(typeLink.get(theType));
             sf.description += "%LINK%";
           }
 
@@ -1198,14 +1022,14 @@ public class FeaturesFile extends AlignFile
           Map<String, FeatureColourI> visible,
           boolean visOnly, boolean nonpos)
   {
-    StringBuffer out = new StringBuffer();
-    SequenceFeature[] next;
-    boolean featuresGen = false;
     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
     {
       // no point continuing.
       return "No Features Visible";
     }
+    StringBuilder out = new StringBuilder(128);
+    SequenceFeature[] next;
+    boolean featuresGen = false;
 
     if (visible != null && visOnly)
     {
@@ -1213,48 +1037,11 @@ public class FeaturesFile extends AlignFile
       // viewed features
       // TODO: decide if feature links should also be written here ?
       Iterator<String> en = visible.keySet().iterator();
-      String feature, color;
       while (en.hasNext())
       {
-        feature = en.next();
-
-        FeatureColourI gc = visible.get(feature);
-        if (!gc.isSimpleColour())
-        {
-          color = (gc.isColourByLabel() ? "label|" : "")
-                  + Format.getHexString(gc.getMinColour()) + "|"
-                  + Format.getHexString(gc.getMaxColour())
-                  + (gc.isAutoScaled() ? "|" : "|abso|") + gc.getMin()
-                  + "|"
-                  + gc.getMax() + "|";
-          if (gc.isBelowThreshold())
-          {
-            color += "below|" + gc.getThreshold();
-          }
-          else if (gc.isAboveThreshold())
-          {
-            color += "above|" + gc.getThreshold();
-          }
-          else
-          {
-            color += "none";
-          }
-        }
-        else
-        {
-          color = Format.getHexString(gc.getColour());
-        }
-        // else
-        // {
-        // // legacy support for integer objects containing colour triplet
-        // values
-        // color = Format.getHexString(new java.awt.Color(Integer
-        // .parseInt(visible.get(type).toString())));
-        // }
-        out.append(feature);
-        out.append("\t");
-        out.append(color);
-        out.append(newline);
+        String featureType = en.next();
+        FeatureColourI colour = visible.get(featureType);
+        out.append(colour.toJalviewFormat(featureType)).append(newline);
       }
     }
     // Work out which groups are both present and visible
index ce382c3..bd58273 100644 (file)
@@ -2,14 +2,18 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+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
 {
+  private static final String BAR = "|";
+
   final private Color colour;
 
   final private Color minColour;
@@ -49,6 +53,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 = UserColourScheme.getColourFromString(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 = false;
+    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().indexOf("abso") != 0)
+      {
+        minval = tok;
+        autoScaled = true;
+      }
+      else
+      {
+        minval = gcol.nextToken();
+        gcol.nextToken(); // skip next '|'
+      }
+      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
+    {
+      featureColour = new FeatureColour(
+              new UserColourScheme(mincol).findColour('A'),
+              new UserColourScheme(maxcol).findColour('A'), 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 +265,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;
@@ -418,4 +620,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(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 String.format("%s\t%s", featureType, colourString);
+  }
+
 }
index 2498208..1a2e417 100755 (executable)
@@ -101,6 +101,10 @@ public class UserColourScheme extends ResidueColourScheme
 
   public static Color getColourFromString(String colour)
   {
+    if (colour == null)
+    {
+      return null;
+    }
     colour = colour.trim();
 
     Color col = null;
index 81357fa..483ea5d 100644 (file)
@@ -3,8 +3,10 @@ package jalview.schemes;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.util.Format;
 
 import java.awt.Color;
 
@@ -112,4 +114,187 @@ public class FeatureColourTest
     assertFalse(fc.isColored(sf));
     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
   }
+
+  /**
+   * Test output of feature colours to Jalview features file format
+   */
+  @Test(groups = { "Functional" })
+  public void testToJalviewFormat()
+  {
+    /*
+     * plain colour - to RGB hex code
+     */
+    FeatureColour fc = new FeatureColour(Color.RED);
+    String redHex = Format.getHexString(Color.RED);
+    String hexColour = redHex;
+    assertEquals("domain\t" + hexColour, fc.toJalviewFormat("domain"));
+    
+    /*
+     * colour by label (no threshold)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
+
+    /*
+     * colour by label (autoscaled) (an odd state you can reach by selecting
+     * 'above threshold', then deselecting 'threshold is min/max' then 'colour
+     * by label')
+     */
+    fc.setAutoScaled(true);
+    assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
+
+    /*
+     * colour by label (above threshold) (min/max values are output though not
+     * used by this scheme)
+     */
+    fc.setAutoScaled(false);
+    fc.setThreshold(12.5f);
+    fc.setAboveThreshold(true);
+    assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
+            fc.toJalviewFormat("domain"));
+
+    /*
+     * colour by label (below threshold)
+     */
+    fc.setBelowThreshold(true);
+    assertEquals("domain\tlabel|||0.0|0.0|below|12.5",
+            fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour, no threshold
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, 12f, 25f);
+    String greenHex = Format.getHexString(Color.GREEN);
+    String expected = String.format("domain\t%s|%s|12.0|25.0|none",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * colour ranges over the actual score ranges (not min/max)
+     */
+    fc.setAutoScaled(true);
+    expected = String.format("domain\t%s|%s|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour below threshold
+     */
+    fc.setThreshold(12.5f);
+    fc.setBelowThreshold(true);
+    expected = String.format("domain\t%s|%s|abso|12.0|25.0|below|12.5",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour above threshold
+     */
+    fc.setThreshold(12.5f);
+    fc.setAboveThreshold(true);
+    expected = String.format("domain\t%s|%s|abso|12.0|25.0|above|12.5",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+  }
+
+  /**
+   * Test parsing of feature colours from Jalview features file format
+   */
+  @Test(groups = { "Functional" })
+  public void testParseJalviewFeatureColour()
+  {
+    /*
+     * simple colour by name
+     */
+    FeatureColour fc = FeatureColour.parseJalviewFeatureColour("red");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(Color.RED, fc.getColour());
+
+    /*
+     * simple colour by hex code
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(Format
+            .getHexString(Color.RED));
+    assertTrue(fc.isSimpleColour());
+    assertEquals(Color.RED, fc.getColour());
+
+    /*
+     * simple colour by rgb triplet
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("255,0,0");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(Color.RED, fc.getColour());
+
+    /*
+     * malformed colour
+     */
+    try
+    {
+      fc = FeatureColour.parseJalviewFeatureColour("oops");
+      fail("expected exception");
+    } catch (IllegalArgumentException e)
+    {
+      assertEquals("Invalid colour descriptor: oops", e.getMessage());
+    }
+
+    /*
+     * colour by label (no threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("label");
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.hasThreshold());
+
+    /*
+     * colour by label (with threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("label|||0.0|0.0|above|12.0");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(12.0f, fc.getThreshold());
+
+    /*
+     * graduated colour (by name) (no threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour (by hex code) (above threshold)
+     */
+    String descriptor = String.format("%s|%s|10.0|20.0|above|15",
+            Format.getHexString(Color.RED),
+            Format.getHexString(Color.GREEN));
+    fc = FeatureColour.parseJalviewFeatureColour(descriptor);
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.hasThreshold());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(15f, fc.getThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour (by RGB triplet) (below threshold), absolute scale
+     */
+    descriptor = String.format("255,0,0|0,255,0|abso|10.0|20.0|below|15");
+    fc = FeatureColour.parseJalviewFeatureColour(descriptor);
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.isAutoScaled());
+    assertTrue(fc.hasThreshold());
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(15f, fc.getThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+  }
 }
diff --git a/test/jalview/schemes/UserColourSchemeTest.java b/test/jalview/schemes/UserColourSchemeTest.java
new file mode 100644 (file)
index 0000000..88f4331
--- /dev/null
@@ -0,0 +1,50 @@
+package jalview.schemes;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertSame;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+public class UserColourSchemeTest
+{
+
+  @Test(groups = "functional")
+  public void testGetColourFromString()
+  {
+    /*
+     * by colour name - if known to AWT, and included in
+     * 
+     * @see ColourSchemeProperty.getAWTColorFromName()
+     */
+    assertSame(Color.RED, UserColourScheme.getColourFromString("red"));
+    assertSame(Color.RED, UserColourScheme.getColourFromString("Red"));
+    assertSame(Color.RED, UserColourScheme.getColourFromString(" RED "));
+
+    /*
+     * by RGB hex code
+     */
+    String hexColour = Integer.toHexString(Color.RED.getRGB() & 0xffffff);
+    assertEquals(Color.RED, UserColourScheme.getColourFromString(hexColour));
+    // 'hex' prefixes _not_ wanted here
+    assertNull(UserColourScheme.getColourFromString("0x" + hexColour));
+    assertNull(UserColourScheme.getColourFromString("#" + hexColour));
+
+    /*
+     * by RGB triplet
+     */
+    String rgb = String.format("%d,%d,%d", Color.red.getRed(),
+            Color.red.getGreen(), Color.red.getBlue());
+    assertEquals(Color.RED, UserColourScheme.getColourFromString(rgb));
+
+    /*
+     * odds and ends
+     */
+    assertNull(UserColourScheme.getColourFromString(null));
+    assertNull(UserColourScheme.getColourFromString("rubbish"));
+    assertEquals(Color.WHITE, UserColourScheme.getColourFromString("-1"));
+    assertNull(UserColourScheme.getColourFromString(String
+            .valueOf(Integer.MAX_VALUE)));
+  }
+}