JAL-2843 schema and code changes to save colour by attribute and filters in user...
[jalview.git] / src / jalview / gui / FeatureSettings.java
index 5fc00a3..811b377 100644 (file)
@@ -25,17 +25,26 @@ import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.schemabinding.version2.CompoundMatcher;
+import jalview.schemabinding.version2.Filter;
 import jalview.schemabinding.version2.JalviewUserColours;
+import jalview.schemabinding.version2.MatchCondition;
+import jalview.schemabinding.version2.MatcherSet;
+import jalview.schemabinding.version2.types.ColourNoValueColourType;
+import jalview.schemabinding.version2.types.ColourThreshTypeType;
+import jalview.schemabinding.version2.types.FeatureMatcherByType;
 import jalview.schemes.FeatureColour;
 import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.util.QuickSort;
+import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 import jalview.ws.DasSequenceFeatureFetcher;
@@ -66,6 +75,8 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
@@ -178,6 +189,290 @@ public class FeatureSettings extends JPanel
   Map<String, float[]> typeWidth = null;
 
   /**
+   * Populates an XML model of the feature colour scheme for one feature type
+   * 
+   * @param featureType
+   * @param fcol
+   * @return
+   */
+  protected static jalview.schemabinding.version2.Colour marshalColour(
+          String featureType, FeatureColourI fcol)
+  {
+    jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
+    if (fcol.isSimpleColour())
+    {
+      col.setRGB(Format.getHexString(fcol.getColour()));
+    }
+    else
+    {
+      col.setRGB(Format.getHexString(fcol.getMaxColour()));
+      col.setMin(fcol.getMin());
+      col.setMax(fcol.getMax());
+      col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
+      col.setAutoScale(fcol.isAutoScaled());
+      col.setThreshold(fcol.getThreshold());
+      col.setColourByLabel(fcol.isColourByLabel());
+      col.setThreshType(fcol.isAboveThreshold() ? ColourThreshTypeType.ABOVE
+              : (fcol.isBelowThreshold() ? ColourThreshTypeType.BELOW
+                      : ColourThreshTypeType.NONE));
+      if (fcol.isColourByAttribute())
+      {
+        col.setAttributeName(fcol.getAttributeName());
+      }
+      Color noColour = fcol.getNoColour();
+      if (noColour == null)
+      {
+        col.setNoValueColour(ColourNoValueColourType.NONE);
+      }
+      else if (noColour == fcol.getMaxColour())
+      {
+        col.setNoValueColour(ColourNoValueColourType.MAX);
+      }
+      else
+      {
+        col.setNoValueColour(ColourNoValueColourType.MIN);
+      }
+    }
+    col.setName(featureType);
+    return col;
+  }
+
+  /**
+   * Populates an XML model of the feature filter(s) for one feature type
+   * 
+   * @param firstMatcher
+   *          the first (or only) match condition)
+   * @param filter
+   *          remaining match conditions (if any)
+   * @param and
+   *          if true, conditions are and-ed, else or-ed
+   */
+  protected static MatcherSet marshalFilter(FeatureMatcherI firstMatcher,
+          Iterator<FeatureMatcherI> filters, boolean and)
+  {
+    MatcherSet result = new MatcherSet();
+
+    if (filters.hasNext())
+    {
+      /*
+       * compound matcher
+       */
+      CompoundMatcher compound = new CompoundMatcher();
+      compound.setAnd(and);
+      MatcherSet matcher1 = marshalFilter(firstMatcher,
+              Collections.emptyIterator(), and);
+      compound.addMatcherSet(matcher1);
+      FeatureMatcherI nextMatcher = filters.next();
+      MatcherSet matcher2 = marshalFilter(nextMatcher, filters, and);
+      compound.addMatcherSet(matcher2);
+      result.setCompoundMatcher(compound);
+    }
+    else
+    {
+      /*
+       * single condition matcher
+       */
+      MatchCondition matcherModel = new MatchCondition();
+      matcherModel.setCondition(
+              firstMatcher.getMatcher().getCondition().getStableName());
+      matcherModel.setValue(firstMatcher.getMatcher().getPattern());
+      if (firstMatcher.isByAttribute())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYATTRIBUTE);
+        matcherModel.setAttributeName(firstMatcher.getAttribute());
+      }
+      else if (firstMatcher.isByLabel())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYLABEL);
+      }
+      else if (firstMatcher.isByScore())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYSCORE);
+      }
+      result.setMatchCondition(matcherModel);
+    }
+
+    return result;
+  }
+
+  /**
+   * Loads one XML model of a feature filter to a Jalview object
+   * 
+   * @param colourModel
+   * @return
+   */
+  protected static FeatureMatcherSetI unmarshalFilter(Filter filterModel)
+  {
+    FeatureMatcherSetI result = new FeatureMatcherSet();
+    MatcherSet matcherSetModel = filterModel.getMatcherSet();
+    try
+    {
+      unmarshalFilterConditions(result, matcherSetModel, true);
+    } catch (IllegalStateException e)
+    {
+      // mixing AND and OR conditions perhaps
+      System.err.println(
+              String.format("Error reading filter conditions for '%s': %s",
+                      filterModel.getFeatureType(), e.getMessage()));
+      // return as much as was parsed up to the error
+    }
+
+    return result;
+  }
+
+  /**
+   * Adds feature match conditions to matcherSet as unmarshalled from XML
+   * (possibly recursively for compound conditions)
+   * 
+   * @param matcherSet
+   * @param matcherSetModel
+   * @param and
+   *          if true, multiple conditions are AND-ed, else they are OR-ed
+   * @throws IllegalStateException
+   *           if AND and OR conditions are mixed
+   */
+  protected static void unmarshalFilterConditions(
+          FeatureMatcherSetI matcherSet, MatcherSet matcherSetModel,
+          boolean and)
+  {
+    MatchCondition mc = matcherSetModel.getMatchCondition();
+    if (mc != null)
+    {
+      /*
+       * single condition
+       */
+      FeatureMatcherByType filterBy = mc.getBy();
+      Condition cond = Condition.fromString(mc.getCondition());
+      String pattern = mc.getValue();
+      FeatureMatcherI matchCondition = null;
+      if (filterBy == FeatureMatcherByType.BYLABEL)
+      {
+        matchCondition = FeatureMatcher.byLabel(cond, pattern);
+      }
+      else if (filterBy == FeatureMatcherByType.BYSCORE)
+      {
+        matchCondition = FeatureMatcher.byScore(cond, pattern);
+
+      }
+      else if (filterBy == FeatureMatcherByType.BYATTRIBUTE)
+      {
+        String[] attNames = mc.getAttributeName();
+        matchCondition = FeatureMatcher.byAttribute(cond, pattern,
+                attNames);
+      }
+
+      /*
+       * note this throws IllegalStateException if AND-ing to a 
+       * previously OR-ed compound condition, or vice versa
+       */
+      if (and)
+      {
+        matcherSet.and(matchCondition);
+      }
+      else
+      {
+        matcherSet.or(matchCondition);
+      }
+    }
+    else
+    {
+      /*
+       * compound condition
+       */
+      MatcherSet[] matchers = matcherSetModel.getCompoundMatcher()
+              .getMatcherSet();
+      boolean anded = matcherSetModel.getCompoundMatcher().getAnd();
+      if (matchers.length == 2)
+      {
+        unmarshalFilterConditions(matcherSet, matchers[0], anded);
+        unmarshalFilterConditions(matcherSet, matchers[1], anded);
+      }
+      else
+      {
+        System.err.println("Malformed compound filter condition");
+      }
+    }
+  }
+
+  /**
+   * Loads one XML model of a feature colour to a Jalview object
+   * 
+   * @param colourModel
+   * @return
+   */
+  protected static FeatureColourI unmarshalColour(
+          jalview.schemabinding.version2.Colour colourModel)
+  {
+    FeatureColourI colour = null;
+
+    if (colourModel.hasMax())
+    {
+      Color mincol = null;
+      Color maxcol = null;
+      Color noValueColour = null;
+
+      try
+      {
+        mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
+        maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      } catch (Exception e)
+      {
+        Cache.log.warn("Couldn't parse out graduated feature color.", e);
+      }
+
+      ColourNoValueColourType noCol = colourModel.getNoValueColour();
+      if (noCol == ColourNoValueColourType.MIN)
+      {
+        noValueColour = mincol;
+      }
+      else if (noCol == ColourNoValueColourType.MAX)
+      {
+        noValueColour = maxcol;
+      }
+
+      colour = new FeatureColour(mincol, maxcol, noValueColour,
+              colourModel.getMin(),
+              colourModel.getMax());
+      String[] attributes = colourModel.getAttributeName();
+      if (attributes != null && attributes.length > 0)
+      {
+        colour.setAttributeName(attributes);
+      }
+      if (colourModel.hasAutoScale())
+      {
+        colour.setAutoScaled(colourModel.getAutoScale());
+      }
+      if (colourModel.hasColourByLabel())
+      {
+        colour.setColourByLabel(colourModel.getColourByLabel());
+      }
+      if (colourModel.hasThreshold())
+      {
+        colour.setThreshold(colourModel.getThreshold());
+      }
+      ColourThreshTypeType ttyp = colourModel.getThreshType();
+      if (ttyp != null)
+      {
+        if (ttyp == ColourThreshTypeType.ABOVE)
+        {
+          colour.setAboveThreshold(true);
+        }
+        else if (ttyp == ColourThreshTypeType.BELOW)
+        {
+          colour.setBelowThreshold(true);
+        }
+      }
+    }
+    else
+    {
+      Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      colour = new FeatureColour(color);
+    }
+
+    return colour;
+  }
+
+  /**
    * Constructor
    * 
    * @param af
@@ -851,6 +1146,10 @@ public class FeatureSettings extends JPanel
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then loads the feature colours and
+   * filters from file in XML format and unmarshals to Jalview feature settings
+   */
   void load()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
@@ -873,60 +1172,35 @@ public class FeatureSettings extends JPanel
 
         JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 
+        /*
+         * load feature colours
+         */
         for (int i = jucs.getColourCount() - 1; i >= 0; i--)
         {
-          String name;
           jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
-          if (newcol.hasMax())
-          {
-            Color mincol = null, maxcol = null;
-            try
-            {
-              mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
-              maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
+          FeatureColourI colour = unmarshalColour(newcol);
+          fr.setColour(newcol.getName(), colour);
+          fr.setOrder(newcol.getName(), i / (float) jucs.getColourCount());
+        }
 
-            } catch (Exception e)
-            {
-              Cache.log.warn("Couldn't parse out graduated feature color.",
-                      e);
-            }
-            FeatureColourI gcol = new FeatureColour(mincol, maxcol,
-                    newcol.getMin(), newcol.getMax());
-            if (newcol.hasAutoScale())
-            {
-              gcol.setAutoScaled(newcol.getAutoScale());
-            }
-            if (newcol.hasColourByLabel())
-            {
-              gcol.setColourByLabel(newcol.getColourByLabel());
-            }
-            if (newcol.hasThreshold())
-            {
-              gcol.setThreshold(newcol.getThreshold());
-            }
-            if (newcol.getThreshType().length() > 0)
-            {
-              String ttyp = newcol.getThreshType();
-              if (ttyp.equalsIgnoreCase("ABOVE"))
-              {
-                gcol.setAboveThreshold(true);
-              }
-              if (ttyp.equalsIgnoreCase("BELOW"))
-              {
-                gcol.setBelowThreshold(true);
-              }
-            }
-            fr.setColour(name = newcol.getName(), gcol);
-          }
-          else
+        /*
+         * load feature filters; loaded filters will replace any that are
+         * currently defined, other defined filters are left unchanged 
+         */
+        for (int i = 0; i < jucs.getFilterCount(); i++)
+        {
+          jalview.schemabinding.version2.Filter filterModel = jucs
+                  .getFilter(i);
+          FeatureMatcherSetI filter = unmarshalFilter(filterModel);
+          if (!filter.isEmpty())
           {
-            Color color = new Color(
-                    Integer.parseInt(jucs.getColour(i).getRGB(), 16));
-            fr.setColour(name = jucs.getColour(i).getName(),
-                    new FeatureColour(color));
+            fr.setFeatureFilter(filterModel.getFeatureType(), filter);
           }
-          fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
         }
+
+        /*
+         * update feature settings table
+         */
         if (table != null)
         {
           resetTable(null);
@@ -943,6 +1217,10 @@ public class FeatureSettings extends JPanel
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then saves the current feature colours
+   * and any filters to the selected file in XML format
+   */
   void save()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
@@ -957,50 +1235,59 @@ public class FeatureSettings extends JPanel
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
       String choice = chooser.getSelectedFile().getPath();
-      jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
+      JalviewUserColours ucs = new JalviewUserColours();
       ucs.setSchemeName("Sequence Features");
       try
       {
         PrintWriter out = new PrintWriter(new OutputStreamWriter(
                 new FileOutputStream(choice), "UTF-8"));
 
+        /*
+         * sort feature types by colour order, from 0 (highest)
+         * to 1 (lowest)
+         */
         Set<String> fr_colours = fr.getAllFeatureColours();
-        Iterator<String> e = fr_colours.iterator();
-        float[] sortOrder = new float[fr_colours.size()];
-        String[] sortTypes = new String[fr_colours.size()];
-        int i = 0;
-        while (e.hasNext())
+        String[] sortedTypes = fr_colours
+                .toArray(new String[fr_colours.size()]);
+        Arrays.sort(sortedTypes, new Comparator<String>()
         {
-          sortTypes[i] = e.next();
-          sortOrder[i] = fr.getOrder(sortTypes[i]);
-          i++;
-        }
-        QuickSort.sort(sortOrder, sortTypes);
-        sortOrder = null;
-        for (i = 0; i < sortTypes.length; i++)
-        {
-          jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
-          col.setName(sortTypes[i]);
-          FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
-          if (fcol.isSimpleColour())
+          @Override
+          public int compare(String type1, String type2)
           {
-            col.setRGB(Format.getHexString(fcol.getColour()));
+            return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
           }
-          else
+        });
+
+        /*
+         * save feature colours
+         */
+        for (String featureType : sortedTypes)
+        {
+          FeatureColourI fcol = fr.getFeatureStyle(featureType);
+          jalview.schemabinding.version2.Colour col = marshalColour(
+                  featureType, fcol);
+          ucs.addColour(col);
+        }
+
+        /*
+         * save any feature filters
+         */
+        for (String featureType : sortedTypes)
+        {
+          FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
+          if (filter != null && !filter.isEmpty())
           {
-            col.setRGB(Format.getHexString(fcol.getMaxColour()));
-            col.setMin(fcol.getMin());
-            col.setMax(fcol.getMax());
-            col.setMinRGB(
-                    jalview.util.Format.getHexString(fcol.getMinColour()));
-            col.setAutoScale(fcol.isAutoScaled());
-            col.setThreshold(fcol.getThreshold());
-            col.setColourByLabel(fcol.isColourByLabel());
-            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
-                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
+            Iterator<FeatureMatcherI> iterator = filter.getMatchers().iterator();
+            FeatureMatcherI firstMatcher = iterator.next();
+            MatcherSet ms = marshalFilter(firstMatcher, iterator,
+                    filter.isAnded());
+            Filter filterModel = new Filter();
+            filterModel.setFeatureType(featureType);
+            filterModel.setMatcherSet(ms);
+            ucs.addFilter(filterModel);
           }
-          ucs.addColour(col);
         }
+
         ucs.marshal(out);
         out.close();
       } catch (Exception ex)