JAL-2808 update spike to latest (filter range tooltip, Present condition)
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 23 Nov 2017 16:21:26 +0000 (16:21 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 23 Nov 2017 16:21:26 +0000 (16:21 +0000)
13 files changed:
src/jalview/api/FeatureRenderer.java
src/jalview/datamodel/features/FeatureMatcher.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSetI.java [new file with mode: 0644]
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/util/matcher/Condition.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java
test/jalview/datamodel/features/FeatureMatcherSetTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureMatcherTest.java [new file with mode: 0644]
test/jalview/renderer/seqfeatures/FeatureRendererTest.java

index ef0abbd..ead84fa 100644 (file)
@@ -22,7 +22,7 @@ package jalview.api;
 
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.util.matcher.KeyedMatcherSetI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 
 import java.awt.Color;
 import java.awt.Graphics;
@@ -223,14 +223,14 @@ public interface FeatureRenderer
    * @param featureType
    * @return
    */
-  KeyedMatcherSetI getFeatureFilter(String featureType);
+  FeatureMatcherSetI getFeatureFilter(String featureType);
 
   /**
    * Answers a shallow copy of the feature filters map
    * 
    * @return
    */
-  public Map<String, KeyedMatcherSetI> getFeatureFilters();
+  public Map<String, FeatureMatcherSetI> getFeatureFilters();
 
   /**
    * Sets the filters for the feature type, or removes them if a null or empty
@@ -239,14 +239,14 @@ public interface FeatureRenderer
    * @param featureType
    * @param filter
    */
-  void setFeatureFilter(String featureType, KeyedMatcherSetI filter);
+  void setFeatureFilter(String featureType, FeatureMatcherSetI filter);
 
   /**
    * Replaces all feature filters with the given map
    * 
    * @param filters
    */
-  void setFeatureFilters(Map<String, KeyedMatcherSetI> filters);
+  void setFeatureFilters(Map<String, FeatureMatcherSetI> filters);
 
   /**
    * Returns the colour for a particular feature instance. This includes
diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..1fc0e0f
--- /dev/null
@@ -0,0 +1,139 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureMatcher implements FeatureMatcherI
+{
+  private static final String COLON = ":";
+
+  /*
+   * if true, match is against feature description
+   */
+  final private boolean byLabel;
+
+  /*
+   * if true, match is against feature score
+   */
+  final private boolean byScore;
+
+  /*
+   * if not null, match is against feature attribute [sub-attribute]
+   */
+  final private String[] key;
+
+  final private MatcherI matcher;
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature label (description)
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   */
+  public static FeatureMatcher byLabel(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), true, false,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature score
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   */
+  public static FeatureMatcher byScore(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, true,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the named feature attribute [and optional sub-attribute]
+   * 
+   * @param cond
+   * @param pattern
+   * @param attName
+   * @return
+   */
+  public static FeatureMatcher byAttribute(Condition cond, String pattern,
+          String... attName)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, false,
+            attName);
+  }
+
+  private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
+          String[] theKey)
+  {
+    key = theKey;
+    matcher = m;
+    byLabel = forLabel;
+    byScore = forScore;
+  }
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    String value = byLabel ? feature.getDescription()
+            : (byScore ? String.valueOf(feature.getScore())
+                    : feature.getValueAsString(key));
+    return matcher.matches(value);
+  }
+
+  @Override
+  public String[] getKey()
+  {
+    return key;
+  }
+
+  @Override
+  public MatcherI getMatcher()
+  {
+    return matcher;
+  }
+
+  /**
+   * Answers a string description of this matcher, suitable for display, debugging
+   * or logging. The format may change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append(String.join(COLON, key)).append(" ")
+            .append(matcher.getCondition().toString());
+    Condition condition = matcher.getCondition();
+    if (condition.isNumeric())
+    {
+      sb.append(" ").append(matcher.getPattern());
+    }
+    else if (condition.needsAPattern())
+    {
+      sb.append(" '").append(matcher.getPattern()).append("'");
+    }
+
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java
new file mode 100644 (file)
index 0000000..078f4a4
--- /dev/null
@@ -0,0 +1,36 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An interface for an object that can apply a match condition to a
+ * SequenceFeature object
+ * 
+ * @author gmcarstairs
+ */
+public interface FeatureMatcherI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Answers the value key this matcher operates on
+   * 
+   * @return
+   */
+  String[] getKey();
+
+  /**
+   * Answers the match condition that is applied
+   * 
+   * @return
+   */
+  MatcherI getMatcher();
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..64ae61b
--- /dev/null
@@ -0,0 +1,122 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeatureMatcherSet implements FeatureMatcherSetI
+{
+  List<FeatureMatcherI> matchConditions;
+
+  boolean andConditions;
+
+  /**
+   * Constructor
+   */
+  public FeatureMatcherSet()
+  {
+    matchConditions = new ArrayList<>();
+  }
+
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    /*
+     * no conditions matches anything
+     */
+    if (matchConditions.isEmpty())
+    {
+      return true;
+    }
+
+    /*
+     * AND until failure
+     */
+    if (andConditions)
+    {
+      for (FeatureMatcherI m : matchConditions)
+      {
+        if (!m.matches(feature))
+        {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /*
+     * OR until match
+     */
+    for (FeatureMatcherI m : matchConditions)
+    {
+      if (m.matches(feature))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public FeatureMatcherSetI and(FeatureMatcherI m)
+  {
+    if (!andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an AND to OR conditions");
+    }
+    matchConditions.add(m);
+    andConditions = true;
+
+    return this;
+  }
+
+  @Override
+  public FeatureMatcherSetI or(FeatureMatcherI m)
+  {
+    if (andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an OR to AND conditions");
+    }
+    matchConditions.add(m);
+    andConditions = false;
+
+    return this;
+  }
+
+  @Override
+  public boolean isAnded()
+  {
+    return andConditions;
+  }
+
+  @Override
+  public Iterable<FeatureMatcherI> getMatchers()
+  {
+    return matchConditions;
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (FeatureMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        sb.append(andConditions ? " AND " : " OR ");
+      }
+      first = false;
+      sb.append("(").append(matcher.toString()).append(")");
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public boolean isEmpty()
+  {
+    return matchConditions == null || matchConditions.isEmpty();
+  }
+
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java
new file mode 100644 (file)
index 0000000..f064770
--- /dev/null
@@ -0,0 +1,63 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+/**
+ * An interface to describe a set of one or more feature matchers, where all
+ * matchers are combined with either AND or OR
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface FeatureMatcherSetI
+{
+  /**
+   * Answers true if the feature provided passes this matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Answers a new object that matches the logical AND of this and m
+   * 
+   * @param m
+   * @return
+   * @throws IllegalStateException
+   *           if an attempt is made to AND to existing OR-ed conditions
+   */
+  FeatureMatcherSetI and(FeatureMatcherI m);
+
+  /**
+   * Answers true if any second condition is AND-ed with this one, false if it
+   * is OR-ed
+   * 
+   * @return
+   */
+  boolean isAnded();
+
+  /**
+   * Answers a new object that matches the logical OR of this and m
+   * 
+   * @param m
+   * @return
+   * @throws IllegalStateException
+   *           if an attempt is made to OR to existing AND-ed conditions
+   */
+  FeatureMatcherSetI or(FeatureMatcherI m);
+
+  /**
+   * Answers an iterator over the combined match conditions
+   * 
+   * @return
+   */
+  Iterable<FeatureMatcherI> getMatchers();
+
+  /**
+   * Answers true if this object contains no conditions
+   * 
+   * @return
+   */
+  boolean isEmpty();
+}
index 2b3688f..0c4cd56 100644 (file)
@@ -30,6 +30,8 @@ import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
@@ -39,8 +41,6 @@ import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.QuickSort;
-import jalview.util.matcher.KeyedMatcherSet;
-import jalview.util.matcher.KeyedMatcherSetI;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 import jalview.ws.DasSequenceFeatureFetcher;
@@ -133,7 +133,7 @@ public class FeatureSettings extends JPanel
 
   private float originalTransparency;
 
-  private Map<String, KeyedMatcherSetI> originalFilters;
+  private Map<String, FeatureMatcherSetI> originalFilters;
 
   final JInternalFrame frame;
 
@@ -210,7 +210,7 @@ public class FeatureSettings extends JPanel
           break;
         case FILTER_COLUMN:
           int row = table.rowAtPoint(e.getPoint());
-          KeyedMatcherSet o = (KeyedMatcherSet) table.getValueAt(row,
+          FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row,
                   column);
           tip = o.isEmpty()
                   ? MessageManager.getString("label.filters_tooltip")
@@ -231,8 +231,8 @@ public class FeatureSettings extends JPanel
     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
 
-    table.setDefaultEditor(KeyedMatcherSet.class, new FilterEditor(this));
-    table.setDefaultRenderer(KeyedMatcherSet.class, new FilterRenderer());
+    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this));
+    table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
 
     TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
             new ColorRenderer(), new ColorEditor(this));
@@ -674,9 +674,9 @@ public class FeatureSettings extends JPanel
 
         data[dataIndex][TYPE_COLUMN] = type;
         data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
-        KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type);
+        FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
         data[dataIndex][FILTER_COLUMN] = featureFilter == null
-                ? new KeyedMatcherSet()
+                ? new FeatureMatcherSet()
                 : featureFilter;
         data[dataIndex][SHOW_COLUMN] = new Boolean(
                 af.getViewport().getFeaturesDisplayed().isVisible(type));
@@ -701,9 +701,9 @@ public class FeatureSettings extends JPanel
         fr.clearRenderOrder();
         return;
       }
-      KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type);
+      FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
       data[dataIndex][FILTER_COLUMN] = featureFilter == null
-              ? new KeyedMatcherSet()
+              ? new FeatureMatcherSet()
               : featureFilter;
       data[dataIndex][SHOW_COLUMN] = new Boolean(true);
       dataIndex++;
@@ -1120,7 +1120,7 @@ public class FeatureSettings extends JPanel
     {
       String type = (String) data[i][TYPE_COLUMN];
       FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
-      KeyedMatcherSetI theFilter = (KeyedMatcherSetI) data[i][FILTER_COLUMN];
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
       Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
       rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
               isShown);
@@ -1674,7 +1674,7 @@ public class FeatureSettings extends JPanel
             Object filter, boolean isSelected, boolean hasFocus, int row,
             int column)
     {
-      KeyedMatcherSetI theFilter = (KeyedMatcherSetI) filter;
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
       setOpaque(true);
       String asText = theFilter.toString();
       setBackground(tbl.getBackground());
@@ -1886,10 +1886,10 @@ public class FeatureSettings extends JPanel
            * update table data without triggering updateFeatureRenderer
            */
           currentColor = fr.getFeatureColours().get(type);
-          KeyedMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
+          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
           if (currentFilter == null)
           {
-            currentFilter = new KeyedMatcherSet();
+            currentFilter = new FeatureMatcherSet();
           }
           Object[] data = ((FeatureTableModel) table.getModel())
                   .getData()[rowSelected];
@@ -1948,7 +1948,7 @@ public class FeatureSettings extends JPanel
   {
     FeatureSettings me;
 
-    KeyedMatcherSetI currentFilter;
+    FeatureMatcherSetI currentFilter;
 
     Point lastLocation;
 
@@ -2003,7 +2003,7 @@ public class FeatureSettings extends JPanel
         currentFilter = me.fr.getFeatureFilter(type);
         if (currentFilter == null)
         {
-          currentFilter = new KeyedMatcherSet();
+          currentFilter = new FeatureMatcherSet();
         }
         Object[] data = ((FeatureTableModel) table.getModel())
                 .getData()[rowSelected];
@@ -2024,7 +2024,7 @@ public class FeatureSettings extends JPanel
     public Component getTableCellEditorComponent(JTable theTable, Object value,
             boolean isSelected, int row, int column)
     {
-      currentFilter = (KeyedMatcherSetI) value;
+      currentFilter = (FeatureMatcherSetI) value;
       this.rowSelected = row;
       type = me.table.getValueAt(row, TYPE_COLUMN).toString();
       button.setOpaque(true);
index 1dd12aa..835f1fc 100644 (file)
@@ -25,14 +25,14 @@ import jalview.api.FeatureColourI;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.FeatureAttributes.Datatype;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.schemes.FeatureColour;
 import jalview.util.ColorUtils;
 import jalview.util.MessageManager;
 import jalview.util.matcher.Condition;
-import jalview.util.matcher.KeyedMatcher;
-import jalview.util.matcher.KeyedMatcherI;
-import jalview.util.matcher.KeyedMatcherSet;
-import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -115,7 +115,7 @@ public class FeatureTypeSettings extends JalviewDialog
    */
   private final FeatureColourI originalColour;
 
-  private final KeyedMatcherSetI originalFilter;
+  private final FeatureMatcherSetI originalFilter;
 
   /*
    * set flag to true when setting values programmatically,
@@ -123,10 +123,20 @@ public class FeatureTypeSettings extends JalviewDialog
    */
   private boolean adjusting = false;
 
+  /*
+   * minimum of the value range for graduated colour
+   * (may be for feature score or for a numeric attribute)
+   */
   private float min;
 
+  /*
+   * maximum of the value range for graduated colour
+   */
   private float max;
 
+  /*
+   * scale factor for conversion between absolute min-max and slider
+   */
   private float scaleFactor;
 
   /*
@@ -181,7 +191,7 @@ public class FeatureTypeSettings extends JalviewDialog
   /*
    * filters for the currently selected feature type
    */
-  private List<KeyedMatcherI> filters;
+  private List<FeatureMatcherI> filters;
 
   // set white normally, black to debug layout
   private Color debugBorderColour = Color.white;
@@ -426,32 +436,48 @@ public class FeatureTypeSettings extends JalviewDialog
    * Updates the min-max range if Colour By selected item is Score, or an
    * attribute, with a min-max range
    */
-  protected void updateMinMax()
+  protected void updateColourMinMax()
   {
     if (!graduatedColour.isSelected())
     {
       return;
     }
 
-    float[] minMax = null;
     String colourBy = (String) colourByRangeCombo.getSelectedItem();
-    if (MessageManager.getString("label.score").equals(colourBy))
+    String[] attNames = fromAttributeDisplayName(colourBy);
+    float[] minMax = getMinMax(attNames);
+
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+    }
+  }
+
+  /**
+   * Retrieves the min-max range:
+   * <ul>
+   * <li>of feature score, if colour or filter is by Score</li>
+   * <li>else of the selected attribute</li>
+   * </ul>
+   * 
+   * @param attNames
+   * @return
+   */
+  private float[] getMinMax(String[] attNames)
+  {
+    float[] minMax = null;
+    if (MessageManager.getString("label.score").equals(attNames[0]))
     {
       minMax = fr.getMinMax().get(featureType)[0];
     }
     else
     {
       // colour by attribute range
-      String[] attNames = fromAttributeDisplayName(colourBy);
       minMax = FeatureAttributes.getInstance().getMinMax(featureType,
               attNames);
     }
-
-    if (minMax != null)
-    {
-      min = minMax[0];
-      max = minMax[1];
-    }
+    return minMax;
   }
 
   /**
@@ -839,7 +865,7 @@ public class FeatureTypeSettings extends JalviewDialog
      * ensure min-max range is for the latest choice of 
      * 'graduated colour by'
      */
-    updateMinMax();
+    updateColourMinMax();
 
     FeatureColourI acg = makeColourFromInputs();
 
@@ -1228,7 +1254,7 @@ public class FeatureTypeSettings extends JalviewDialog
     /*
      * if this feature type has filters set, load them first
      */
-    KeyedMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
+    FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
     if (featureFilters != null)
     {
       if (!featureFilters.isAnded())
@@ -1241,15 +1267,15 @@ public class FeatureTypeSettings extends JalviewDialog
     /*
      * and an empty filter for the user to populate (add)
      */
-    KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "",
-            (String[]) null);
+    FeatureMatcherI noFilter = FeatureMatcher.byLabel(Condition.values()[0],
+            "");
     filters.add(noFilter);
 
     /*
      * render the conditions in rows, each in its own JPanel
      */
     int filterIndex = 0;
-    for (KeyedMatcherI filter : filters)
+    for (FeatureMatcherI filter : filters)
     {
       String[] attName = filter.getKey();
       Condition condition = filter.getMatcher().getCondition();
@@ -1307,11 +1333,10 @@ public class FeatureTypeSettings extends JalviewDialog
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        if (attCombo.getSelectedItem() != null)
+        if (validateFilter(patternField, condCombo))
         {
-          if (validateFilter(patternField, condCombo))
+          if (updateFilter(attCombo, condCombo, patternField, filterIndex))
           {
-            updateFilter(attCombo, condCombo, patternField, filterIndex);
             filtersChanged();
           }
         }
@@ -1379,8 +1404,7 @@ public class FeatureTypeSettings extends JalviewDialog
      * disable pattern field for condition 'Present / NotPresent'
      */
     Condition selectedCondition = (Condition) condCombo.getSelectedItem();
-    if (selectedCondition == Condition.Present
-            || selectedCondition == Condition.NotPresent)
+    if (!selectedCondition.needsAPattern())
     {
       patternField.setEnabled(false);
     }
@@ -1389,22 +1413,13 @@ public class FeatureTypeSettings extends JalviewDialog
      * if a numeric condition is selected, show the value range
      * as a tooltip on the value input field
      */
-    if (selectedCondition.isNumeric())
-    {
-      float[] minMax = FeatureAttributes.getInstance()
-              .getMinMax(featureType, attName);
-      if (minMax != null)
-      {
-        String tip = String.format("(%s - %s)",
-                DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1]));
-        patternField.setToolTipText(tip);
-      }
-    }
+    updatePatternTooltip(attName, selectedCondition, patternField);
 
     /*
      * add remove button if filter is populated (non-empty pattern)
      */
-    if (pattern != null && pattern.trim().length() > 0)
+    if (!patternField.isEnabled()
+            || (pattern != null && pattern.trim().length() > 0))
     {
       // todo: gif for button drawing '-' or 'x'
       JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
@@ -1426,6 +1441,31 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
+   * If a numeric comparison condition is selected, retrieve the min-max range for
+   * the value (score or attribute), and set it as a tooltip on the value file
+   * 
+   * @param attName
+   * @param selectedCondition
+   * @param patternField
+   */
+  private void updatePatternTooltip(String[] attName,
+          Condition selectedCondition, JTextField patternField)
+  {
+    patternField.setToolTipText("");
+
+    if (selectedCondition.isNumeric())
+    {
+      float[] minMax = getMinMax(attName);
+      if (minMax != null)
+      {
+        String tip = String.format("(%s - %s)",
+                DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1]));
+        patternField.setToolTipText(tip);
+      }
+    }
+  }
+
+  /**
    * Populates the drop-down list of comparison conditions for the given attribute
    * name. The conditions added depend on the datatype of the attribute values.
    * The supplied condition is set as the selected item in the list, provided it
@@ -1504,7 +1544,7 @@ public class FeatureTypeSettings extends JalviewDialog
     }
 
     Condition cond = (Condition) condCombo.getSelectedItem();
-    if (cond == Condition.Present || cond == Condition.NotPresent)
+    if (cond.needsAPattern())
     {
       return true;
     }
@@ -1514,10 +1554,10 @@ public class FeatureTypeSettings extends JalviewDialog
     String v1 = value.getText().trim();
     if (v1.length() == 0)
     {
-      return false;
+      // return false;
     }
 
-    if (cond.isNumeric())
+    if (cond.isNumeric() && v1.length() > 0)
     {
       try
       {
@@ -1536,24 +1576,58 @@ public class FeatureTypeSettings extends JalviewDialog
 
   /**
    * Constructs a filter condition from the given input fields, and replaces the
-   * condition at filterIndex with the new one
+   * condition at filterIndex with the new one. Does nothing if the pattern field
+   * is blank (unless the match condition is one that doesn't require a pattern,
+   * e.g. 'Is present'). Answers true if the filter was updated, else false.
+   * <p>
+   * This method may update the tooltip on the filter value field to show the
+   * value range, if a numeric condition is selected. This ensures the tooltip is
+   * updated when a numeric valued attribute is chosen on the last 'add a filter'
+   * row.
    * 
    * @param attCombo
    * @param condCombo
    * @param valueField
    * @param filterIndex
    */
-  protected void updateFilter(JComboBox<String> attCombo,
+  protected boolean updateFilter(JComboBox<String> attCombo,
           JComboBox<Condition> condCombo, JTextField valueField,
           int filterIndex)
   {
     String attName = (String) attCombo.getSelectedItem();
     Condition cond = (Condition) condCombo.getSelectedItem();
-    String pattern = valueField.getText();
-    KeyedMatcherI km = new KeyedMatcher(cond, pattern,
-            fromAttributeDisplayName(attName));
+    String pattern = valueField.getText().trim();
+
+    updatePatternTooltip(fromAttributeDisplayName(attName), cond,
+            valueField);
+
+    if (pattern.length() == 0 && cond.needsAPattern())
+    {
+      return false;
+    }
+
+    /*
+     * Construct a matcher that operates on Label, Score, 
+     * or named attribute
+     */
+    FeatureMatcherI km = null;
+    if (MessageManager.getString("label.label").equals(attName))
+    {
+      km = FeatureMatcher.byLabel(cond, pattern);
+    }
+    else if (MessageManager.getString("label.score").equals(attName))
+    {
+      km = FeatureMatcher.byScore(cond, pattern);
+    }
+    else
+    {
+      km = FeatureMatcher.byAttribute(cond, pattern,
+              fromAttributeDisplayName(attName));
+    }
 
     filters.set(filterIndex, km);
+
+    return true;
   }
 
   /**
@@ -1584,14 +1658,13 @@ public class FeatureTypeSettings extends JalviewDialog
      * update the filter conditions for the feature type
      */
     boolean anded = andFilters.isSelected();
-    KeyedMatcherSetI combined = new KeyedMatcherSet();
+    FeatureMatcherSetI combined = new FeatureMatcherSet();
 
-    for (KeyedMatcherI filter : filters)
+    for (FeatureMatcherI filter : filters)
     {
       String pattern = filter.getMatcher().getPattern();
       Condition condition = filter.getMatcher().getCondition();
-      if (pattern.trim().length() > 0 || condition == Condition.Present
-              || condition == Condition.NotPresent)
+      if (pattern.trim().length() > 0 || !condition.needsAPattern())
       {
         if (anded)
         {
index 4d14063..3401ae8 100644 (file)
@@ -11,17 +11,21 @@ import java.util.Map;
  */
 public enum Condition
 {
-  Contains(false), NotContains(false), Matches(false), NotMatches(false),
-  Present(false), NotPresent(false),
-  EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true);
+  Contains(false, true), NotContains(false, true), Matches(false, true),
+  NotMatches(false, true), Present(false, false), NotPresent(false, false),
+  EQ(true, true), NE(true, true), LT(true, true), LE(true, true),
+  GT(true, true), GE(true, true);
 
   private static Map<Condition, String> displayNames = new HashMap<>();
   
   private boolean numeric;
 
-  Condition(boolean isNumeric)
+  private boolean needsAPattern;
+
+  Condition(boolean isNumeric, boolean needsPattern)
   {
     numeric = isNumeric;
+    needsAPattern = needsPattern;
   }
 
   /**
@@ -36,6 +40,17 @@ public enum Condition
   }
 
   /**
+   * Answers true if the condition requires a pattern to compare against, else
+   * false
+   * 
+   * @return
+   */
+  public boolean needsAPattern()
+  {
+    return needsAPattern;
+  }
+
+  /**
    * Answers a display name for the match condition, suitable for showing in
    * drop-down menus. The value may be internationalized using the resource key
    * "label.matchCondition_" with the enum name appended.
index 8bdcad4..c58461e 100644 (file)
@@ -26,11 +26,11 @@ import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
 import jalview.util.ColorUtils;
-import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.Color;
 import java.beans.PropertyChangeListener;
@@ -58,12 +58,12 @@ public abstract class FeatureRendererModel
 
     public final FeatureColourI featureColour;
 
-    public final KeyedMatcherSetI filter;
+    public final FeatureMatcherSetI filter;
 
     public final Boolean show;
 
     public FeatureSettingsBean(String type, FeatureColourI colour,
-            KeyedMatcherSetI theFilter, Boolean isShown)
+            FeatureMatcherSetI theFilter, Boolean isShown)
     {
       featureType = type;
       featureColour = colour;
@@ -90,7 +90,7 @@ public abstract class FeatureRendererModel
   /*
    * filters for each feature type
    */
-  protected Map<String, KeyedMatcherSetI> featureFilters = new HashMap<>();
+  protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
 
   protected String[] renderOrder;
 
@@ -1068,25 +1068,25 @@ public abstract class FeatureRendererModel
   }
 
   @Override
-  public Map<String, KeyedMatcherSetI> getFeatureFilters()
+  public Map<String, FeatureMatcherSetI> getFeatureFilters()
   {
     return new HashMap<>(featureFilters);
   }
 
   @Override
-  public void setFeatureFilters(Map<String, KeyedMatcherSetI> filters)
+  public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
   {
     featureFilters = filters;
   }
 
   @Override
-  public KeyedMatcherSetI getFeatureFilter(String featureType)
+  public FeatureMatcherSetI getFeatureFilter(String featureType)
   {
     return featureFilters.get(featureType);
   }
 
   @Override
-  public void setFeatureFilter(String featureType, KeyedMatcherSetI filter)
+  public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
   {
     if (filter == null || filter.isEmpty())
     {
@@ -1146,14 +1146,8 @@ public abstract class FeatureRendererModel
    */
   protected boolean featureMatchesFilters(SequenceFeature sf)
   {
-    KeyedMatcherSetI filter = featureFilters.get(sf.getType());
-    // TODO temporary fudge for Score and Label
-    return filter == null ? true
-            : filter.matches(
-                    key -> "Label".equals(key[0]) ? sf.getDescription()
-                            : ("Score".equals(key[0])
-                                    ? String.valueOf(sf.getScore())
-                                    : sf.getValueAsString(key)));
+    FeatureMatcherSetI filter = featureFilters.get(sf.getType());
+    return filter == null ? true : filter.matches(sf);
   }
 
 }
index 6afaa54..f594453 100644 (file)
@@ -21,8 +21,8 @@
 package jalview.viewmodel.seqfeatures;
 
 import jalview.api.FeatureColourI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.schemes.FeatureColour;
-import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -47,7 +47,7 @@ public class FeatureRendererSettings implements Cloneable
   /*
    * map of {featureType, filters}
    */
-  Map<String, KeyedMatcherSetI> featureFilters;
+  Map<String, FeatureMatcherSetI> featureFilters;
 
   float transparency;
 
diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java
new file mode 100644 (file)
index 0000000..a98013b
--- /dev/null
@@ -0,0 +1,284 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherSetTest
+{
+  @Test(groups = "Functional")
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fms.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testAnd()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12,
+            6.2f, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.and(fm1);
+    assertTrue(fms.matches(sf));
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains,
+            "Helix");
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a failed score condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT,
+            "5.9");
+    fms.or(byScoreFail);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR failed attribute and score conditions with matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.or(fm2).or(byScoreFail);
+    assertFalse(fms.matches(sf));
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm1.toString(), "AF < 1.2");
+
+    FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "path",
+            "CLIN_SIG");
+    assertEquals(fm2.toString(), "CLIN_SIG Does not contain 'PATH'");
+
+    /*
+     * AND them
+     */
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.and(fm1);
+    assertEquals(fms.toString(), "(AF < 1.2)");
+    fms.and(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) AND (CLIN_SIG Does not contain 'PATH')");
+
+    /*
+     * OR them
+     */
+    fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.or(fm1);
+    assertEquals(fms.toString(), "(AF < 1.2)");
+    fms.or(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) OR (CLIN_SIG Does not contain 'PATH')");
+
+    try
+    {
+      fms.and(fm1);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testOr()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.or(fm1);
+    assertTrue(fms.matches(sf));
+    fms.or(fm2);
+    assertTrue(fms.matches(sf)); // true or false makes true
+
+    fms = new FeatureMatcherSet();
+    fms.or(fm2);
+    assertFalse(fms.matches(sf));
+    fms.or(fm1);
+    assertTrue(fms.matches(sf)); // false or true makes true
+
+    try
+    {
+      fms.and(fm2);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testIsEmpty()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.isEmpty());
+    fms.and(fm);
+    assertFalse(fms.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatchers()
+  {
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+
+    /*
+     * empty iterable:
+     */
+    Iterator<FeatureMatcherI> iterator = fms.getMatchers().iterator();
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one matcher:
+     */
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    fms.and(fm1);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two matchers:
+     */
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f",
+            "AF");
+    fms.and(fm2);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertSame(fm2, iterator.next());
+    assertFalse(iterator.hasNext());
+  }
+
+  /**
+   * Tests for the 'compound attribute' key i.e. where first key's value is a map
+   * from which we take the value for the second key, e.g. CSQ : Consequence
+   */
+  @Test(groups = "Functional")
+  public void testMatches_compoundKey()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "CSQ", "Consequence");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    Map<String, String> csq = new HashMap<>();
+    sf.setValue("CSQ", csq);
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "-2");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-1");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-3");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "junk");
+    assertFalse(fms.matches(sf));
+  
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ",
+            "Consequence");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    csq.put("PolyPhen", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "Catastrophic");
+    assertTrue(fms.matches(sf));
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java
new file mode 100644 (file)
index 0000000..f4e9351
--- /dev/null
@@ -0,0 +1,169 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherTest
+{
+  @Test
+  public void testMatches_byLabel()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, "grp");
+
+    /*
+     * contains - not case sensitive
+     */
+    assertTrue(
+            FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf));
+
+    /*
+     * does not contain
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is")
+            .matches(sf));
+
+    /*
+     * matches
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * does not match
+     */
+    assertFalse(FeatureMatcher
+            .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * is present / not present
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf));
+  }
+
+  @Test
+  public void testMatches_byScore()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, 3.2f, "grp");
+
+    assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf));
+
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf));
+  }
+  @Test
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher
+            .byAttribute(Condition.GE, "-2", "AF");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fm.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AC", "21");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AC_Females", "21");
+    assertFalse(fm.matches(sf));
+  }
+
+  @Test
+  public void testToString()
+  {
+    /*
+     * toString uses the i18n translation of the enum conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm.toString(), "AF < 1.2");
+
+    /*
+     * Present / NotPresent omit the value pattern
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+    assertEquals(fm.toString(), "AF Is present");
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+    assertEquals(fm.toString(), "AF Is not present");
+  }
+
+  @Test
+  public void testGetKey()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    assertEquals(fm.getKey(), new String[] { "AF" });
+
+    /*
+     * compound key (attribute / subattribute)
+     */
+    fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ",
+            "Consequence");
+    assertEquals(fm.getKey(), new String[] { "CSQ", "Consequence" });
+  }
+
+  @Test
+  public void testGetMatcher()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f",
+            "AF");
+    assertEquals(fm.getMatcher().getCondition(), Condition.GE);
+    assertEquals(fm.getMatcher().getFloatValue(), -2F);
+    assertEquals(fm.getMatcher().getPattern(), "-2.0");
+  }
+}
index 73ae9d7..03398c0 100644 (file)
@@ -9,14 +9,14 @@ import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.AlignFrame;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
 import jalview.util.matcher.Condition;
-import jalview.util.matcher.KeyedMatcher;
-import jalview.util.matcher.KeyedMatcherSet;
-import jalview.util.matcher.KeyedMatcherSetI;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
@@ -68,9 +68,8 @@ public class FeatureRendererTest
     seqs.get(2).addSequenceFeature(
             new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
     // bug in findAllFeatures - group not checked for a known feature type
-    seqs.get(2).addSequenceFeature(
-            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN,
-                    "RfamGroup"));
+    seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9,
+            Float.NaN, "RfamGroup"));
     // existing feature type with null group
     seqs.get(3).addSequenceFeature(
             new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
@@ -129,7 +128,8 @@ public class FeatureRendererTest
     data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
     data[2] = new FeatureSettingsBean("Scop", colour, null, false);
     fr.setFeaturePriority(data);
-    assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam"));
+    assertEquals(fr.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam"));
     assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
 
     /*
@@ -260,7 +260,7 @@ public class FeatureRendererTest
     features = fr.findFeaturesAtColumn(seq, 5);
     assertEquals(features.size(), 1);
     assertTrue(features.contains(sf8));
-    
+
     /*
      * give "Type3" features a graduated colour scheme
      * - first with no threshold
@@ -412,7 +412,7 @@ public class FeatureRendererTest
     // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
     Color expected = new Color(255, 128, 0);
     assertEquals(fr.getColour(sf2), expected);
-    
+
     /*
      * above threshold, score is above threshold - no change
      */
@@ -468,14 +468,14 @@ public class FeatureRendererTest
     // with filter on AF < 4
     gc.setAboveThreshold(false);
     assertEquals(fr.getColour(sf2), expected);
-    KeyedMatcherSetI filter = new KeyedMatcherSet();
-    filter.and(new KeyedMatcher(Condition.LT, 4f, "AF"));
+    FeatureMatcherSetI filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
     fr.setFeatureFilter("Cath", filter);
     assertNull(fr.getColour(sf2));
 
     // with filter on 'Consequence contains missense'
-    filter = new KeyedMatcherSet();
-    filter.and(new KeyedMatcher(Condition.Contains, "missense",
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
             "Consequence"));
     fr.setFeatureFilter("Cath", filter);
     // if feature has no Consequence attribute, no colour
@@ -487,10 +487,10 @@ public class FeatureRendererTest
     sf2.setValue("Consequence", "Missense variant");
     assertEquals(fr.getColour(sf2), expected);
 
-    // with filter on CSQ.Feature contains "ENST01234"
-    filter = new KeyedMatcherSet();
-    filter.and(new KeyedMatcher(Condition.Matches, "ENST01234", "CSQ",
-            "Feature"));
+    // with filter on CSQ:Feature contains "ENST01234"
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
+            "CSQ", "Feature"));
     fr.setFeatureFilter("Cath", filter);
     // if feature has no CSQ data, no colour
     assertNull(fr.getColour(sf2));