Merge branch 'develop' into features/JAL-2094_colourInterface
[jalview.git] / src / jalview / io / FeaturesFile.java
index aa38540..46467c5 100755 (executable)
@@ -48,6 +48,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.StringTokenizer;
 
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
@@ -386,6 +387,227 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Process a feature type colour specification
+   * 
+   * @param line
+   *          the current input line (for error messages only)
+   * @param featureType
+   *          the first token on the line
+   * @param gffColumns
+   *          holds tokens on the line
+   * @param colours
+   *          map to which to add derived colour specification
+   */
+  protected void parseFeatureColour(String line, String featureType,
+          String[] gffColumns, Map<String, FeatureColourI> colours)
+  {
+    FeatureColourI colour = null;
+    String colscheme = gffColumns[1];
+    if (colscheme.indexOf("|") > -1
+            || colscheme.trim().equalsIgnoreCase("label"))
+    {
+      colour = parseGraduatedColourScheme(line, colscheme);
+    }
+    else
+    {
+      UserColourScheme ucs = new UserColourScheme(colscheme);
+      colour = new FeatureColour(ucs.findColour('A'));
+    }
+    if (colour != null)
+    {
+      colours.put(featureType, colour);
+    }
+  }
+
+  /**
+   * Parse a Jalview graduated colour descriptor
+   * 
+   * @param line
+   * @param colourDescriptor
+   * @return
+   */
+  protected FeatureColourI parseGraduatedColourScheme(String line,
+          String colourDescriptor)
+  {
+    // 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(colourDescriptor, "|", true);
+    // set defaults
+    float min = Float.MIN_VALUE, max = Float.MAX_VALUE;
+    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);
+      return null;
+    }
+    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 = Float.valueOf(minval);
+        }
+      } catch (Exception e)
+      {
+        System.err
+                .println("Couldn't parse the minimum value for graduated colour for type ("
+                        + colourDescriptor
+                        + ") - did you misspell 'auto' for the optional automatic colour switch ?");
+        e.printStackTrace();
+      }
+      try
+      {
+        if (maxval.length() > 0)
+        {
+          max = Float.valueOf(maxval);
+        }
+      } catch (Exception e)
+      {
+        System.err
+                .println("Couldn't parse the maximum value for graduated colour for type ("
+                        + colourDescriptor + ")");
+        e.printStackTrace();
+      }
+    }
+    else
+    {
+      // add in some dummy min/max colours for the label-only
+      // colourscheme.
+      mincol = "FFFFFF";
+      maxcol = "000000";
+    }
+
+    FeatureColourI colour = null;
+    try
+    {
+      colour = new FeatureColour(
+              new UserColourScheme(mincol).findColour('A'),
+              new UserColourScheme(maxcol).findColour('A'), min, max);
+    } catch (Exception e)
+    {
+      System.err.println("Couldn't parse the graduated colour scheme ("
+              + colourDescriptor + ")");
+      e.printStackTrace();
+    }
+    if (colour != null)
+    {
+      colour.setColourByLabel(labelCol);
+      colour.setAutoScaled(abso == null);
+      // add in any additional parameters
+      String ttype = null, tval = null;
+      boolean hasThreshold = false;
+      if (gcol.hasMoreTokens())
+      {
+        // threshold type and possibly a threshold value
+        ttype = gcol.nextToken();
+        if (ttype.toLowerCase().startsWith("below"))
+        {
+          colour.setBelowThreshold(true);
+          hasThreshold = true;
+        }
+        else if (ttype.toLowerCase().startsWith("above"))
+        {
+          colour.setAboveThreshold(true);
+          hasThreshold = true;
+        }
+        else
+        {
+          if (!ttype.toLowerCase().startsWith("no"))
+          {
+            System.err.println("Ignoring unrecognised threshold type : "
+                    + ttype);
+          }
+        }
+      }
+      if (hasThreshold)
+      {
+        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");
+      }
+    }
+    return colour;
+  }
+
+  /**
    * clear any temporary handles used to speed up ID matching
    */
   protected void resetMatcher()
@@ -499,7 +721,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * 
    * @param sequences
    *          source of features
-   * @param visible
+   * @param featureColours
    *          hash of Colours for each feature type
    * @param visOnly
    *          when true only feature types in 'visible' will be output
@@ -509,27 +731,28 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * @return features file contents
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean visOnly,
+          Map<String, FeatureColourI> featureColours, boolean visOnly,
           boolean nonpos)
   {
     StringBuilder out = new StringBuilder(256);
     boolean featuresGen = false;
-    if (visOnly && !nonpos && (visible == null || visible.size() < 1))
+    if (visOnly && !nonpos
+            && (featureColours == null || featureColours.size() < 1))
     {
       // no point continuing.
       return "No Features Visible";
     }
 
-    if (visible != null && visOnly)
+    if (featureColours != null && visOnly)
     {
       // write feature colours only if we're given them and we are generating
       // viewed features
       // TODO: decide if feature links should also be written here ?
-      Iterator<String> en = visible.keySet().iterator();
+      Iterator<String> en = featureColours.keySet().iterator();
       while (en.hasNext())
       {
-        String featureType = en.next().toString();
-        FeatureColourI colour = visible.get(featureType);
+        String featureType = en.next();
+        FeatureColourI colour = featureColours.get(featureType);
         out.append(colour.toJalviewFormat(featureType)).append(newline);
       }
     }
@@ -549,7 +772,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
         {
           isnonpos = features[j].begin == 0 && features[j].end == 0;
           if ((!nonpos && isnonpos)
-                  || (!isnonpos && visOnly && !visible
+                  || (!isnonpos && visOnly && !featureColours
                           .containsKey(features[j].type)))
           {
             continue;
@@ -589,7 +812,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           {
             isnonpos = sequenceFeature.begin == 0 && sequenceFeature.end == 0;
             if ((!nonpos && isnonpos)
-                    || (!isnonpos && visOnly && !visible
+                    || (!isnonpos && visOnly && !featureColours
                             .containsKey(sequenceFeature.type)))
             {
               // skip if feature is nonpos and we ignore them or if we only
@@ -757,14 +980,15 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * 
    * @param sequences
    *          the sequences whose features are to be output
-   * @param visible
+   * @param featureColours
    *          a map whose keys are the type names of visible features
    * @param outputVisibleOnly
    * @param includeNonPositionalFeatures
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean outputVisibleOnly,
+          Map<String, FeatureColourI> featureColours,
+          boolean outputVisibleOnly,
           boolean includeNonPositionalFeatures)
   {
     StringBuilder out = new StringBuilder(256);
@@ -790,7 +1014,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           // TODO why the test !isnonpos here?
           // what about not visible non-positional features?
           if (!isnonpos && outputVisibleOnly
-                  && !visible.containsKey(sf.type))
+                  && !featureColours.containsKey(sf.type))
           {
             /*
              * ignore not visible features if not wanted