JAL-2808 spike updated with latest
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 2 Nov 2017 15:53:06 +0000 (15:53 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 2 Nov 2017 15:53:06 +0000 (15:53 +0000)
27 files changed:
resources/lang/Messages.properties
src/jalview/api/FeatureRenderer.java
src/jalview/appletgui/SeqPanel.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/FeatureAttributes.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSources.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/JalviewDialog.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/util/matcher/Condition.java [new file with mode: 0644]
src/jalview/util/matcher/KeyedMatcher.java [new file with mode: 0644]
src/jalview/util/matcher/KeyedMatcherI.java [new file with mode: 0644]
src/jalview/util/matcher/KeyedMatcherSet.java [new file with mode: 0644]
src/jalview/util/matcher/KeyedMatcherSetI.java [new file with mode: 0644]
src/jalview/util/matcher/Matcher.java [new file with mode: 0644]
src/jalview/util/matcher/MatcherI.java [new file with mode: 0644]
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java
test/jalview/controller/AlignViewControllerTest.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/util/matcher/ConditionTest.java [new file with mode: 0644]
test/jalview/util/matcher/KeyedMatcherSetTest.java [new file with mode: 0644]
test/jalview/util/matcher/KeyedMatcherTest.java [new file with mode: 0644]
test/jalview/util/matcher/MatcherTest.java [new file with mode: 0644]

index 9ffe2ae..851585a 100644 (file)
@@ -1324,3 +1324,19 @@ label.overview = Overview
 label.reset_to_defaults = Reset to defaults
 label.oview_calc = Recalculating overview...
 label.feature_details = Feature details
+label.matchCondition_contains = Contains
+label.matchCondition_notcontains = Does not contain
+label.matchCondition_matches = Matches
+label.matchCondition_notmatches = Does not match
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
+label.numeric_required = The value should be numeric
+label.no_attributes_known = No attributes known
+label.filters = Filters
+label.match_condition = Match condition
+label.join_conditions = Join conditions with
+label.feature_to_filter = Feature to filter
index 9d2d7f4..40c7d4d 100644 (file)
@@ -22,6 +22,7 @@ package jalview.api;
 
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.Color;
 import java.awt.Graphics;
@@ -215,4 +216,53 @@ public interface FeatureRenderer
    */
   float getTransparency();
 
+  /**
+   * Answers the filters applied to the given feature type, or null if none is
+   * set
+   * 
+   * @param featureType
+   * @return
+   */
+  KeyedMatcherSetI getFeatureFilter(String featureType);
+
+  /**
+   * Answers a shallow copy of the feature filters map
+   * 
+   * @return
+   */
+  public Map<String, KeyedMatcherSetI> getFeatureFilters();
+
+  /**
+   * Sets the filters for the feature type, or removes them if a null or empty
+   * filter is passed
+   * 
+   * @param featureType
+   * @param filter
+   */
+  void setFeatureFilter(String featureType, KeyedMatcherSetI filter);
+
+  /**
+   * Replaces all feature filters with the given map
+   * 
+   * @param filters
+   */
+  void setFeatureFilters(Map<String, KeyedMatcherSetI> filters);
+
+  /**
+   * Returns the colour for a particular feature instance. This includes
+   * calculation of 'colour by label', or of a graduated score colour, if
+   * applicable.
+   * <p>
+   * Returns null if
+   * <ul>
+   * <li>feature type is not visible, or</li>
+   * <li>feature group is not visible, or</li>
+   * <li>feature values lie outside any colour threshold, or</li>
+   * <li>feature is excluded by filter conditions</li>
+   * </ul>
+   * 
+   * @param feature
+   * @return
+   */
+  Color getColour(SequenceFeature feature);
 }
index 55320ed..9a61f5f 100644 (file)
@@ -417,7 +417,6 @@ public class SeqPanel extends Panel implements MouseMotionListener,
    *          alignment column
    * @param seq
    *          index of sequence in alignment
-   * @return position of column in sequence or -1 if at gap
    */
   void setStatusMessage(SequenceI sequence, int column, int seq)
   {
index 460c2b3..5662d0c 100644 (file)
@@ -53,20 +53,19 @@ public class AlignViewController implements AlignViewControllerI
   private AlignViewControllerGuiI avcg;
 
   public AlignViewController(AlignViewControllerGuiI alignFrame,
-          AlignViewportI viewport, AlignmentViewPanel alignPanel)
+          AlignViewportI vp, AlignmentViewPanel ap)
   {
     this.avcg = alignFrame;
-    this.viewport = viewport;
-    this.alignPanel = alignPanel;
+    this.viewport = vp;
+    this.alignPanel = ap;
   }
 
   @Override
-  public void setViewportAndAlignmentPanel(AlignViewportI viewport,
-          AlignmentViewPanel alignPanel)
+  public void setViewportAndAlignmentPanel(AlignViewportI vp,
+          AlignmentViewPanel ap)
   {
-    this.alignPanel = alignPanel;
-    this.viewport = viewport;
-
+    this.alignPanel = ap;
+    this.viewport = vp;
   }
 
   @Override
@@ -215,17 +214,21 @@ public class AlignViewController implements AlignViewControllerI
 
   /**
    * Sets a bit in the BitSet for each column (base 0) in the sequence
-   * collection which includes the specified feature type. Returns the number of
-   * sequences which have the feature in the selected range.
+   * collection which includes a visible feature of the specified feature type.
+   * Returns the number of sequences which have the feature visible in the
+   * selected range.
    * 
    * @param featureType
    * @param sqcol
    * @param bs
    * @return
    */
-  static int findColumnsWithFeature(String featureType,
+  int findColumnsWithFeature(String featureType,
           SequenceCollectionI sqcol, BitSet bs)
   {
+    FeatureRenderer fr = alignPanel == null ? null : alignPanel
+            .getFeatureRenderer();
+
     final int startColumn = sqcol.getStartRes() + 1; // converted to base 1
     final int endColumn = sqcol.getEndRes() + 1;
     List<SequenceI> seqs = sqcol.getSequences();
@@ -238,13 +241,19 @@ public class AlignViewController implements AlignViewControllerI
         List<SequenceFeature> sfs = sq.findFeatures(startColumn,
                 endColumn, featureType);
 
-        if (!sfs.isEmpty())
-        {
-          nseq++;
-        }
-
+        boolean found = false;
         for (SequenceFeature sf : sfs)
         {
+          if (fr.getColour(sf) == null)
+          {
+            continue;
+          }
+          if (!found)
+          {
+            nseq++;
+          }
+          found = true;
+
           int sfStartCol = sq.findIndex(sf.getBegin());
           int sfEndCol = sq.findIndex(sf.getEnd());
 
index 8f82a1a..ffbd497 100755 (executable)
@@ -21,6 +21,7 @@
 package jalview.datamodel;
 
 import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.FeatureLocationI;
 import jalview.datamodel.features.FeatureSourceI;
 import jalview.datamodel.features.FeatureSources;
@@ -392,6 +393,23 @@ public class SequenceFeature implements FeatureLocationI
   }
 
   /**
+   * Answers the value of the specified attribute as string, or null if no such
+   * value
+   * 
+   * @param key
+   * @return
+   */
+  public String getValueAsString(String key)
+  {
+    if (otherDetails == null)
+    {
+      return null;
+    }
+    Object value = otherDetails.get(key);
+    return value == null ? null : value.toString();
+  }
+
+  /**
    * Returns a property value for the given key if known, else the specified
    * default value
    * 
@@ -424,6 +442,7 @@ public class SequenceFeature implements FeatureLocationI
       }
 
       otherDetails.put(key, value);
+      FeatureAttributes.getInstance().addAttribute(this.type, key);
     }
   }
 
diff --git a/src/jalview/datamodel/features/FeatureAttributes.java b/src/jalview/datamodel/features/FeatureAttributes.java
new file mode 100644 (file)
index 0000000..d4e9fb0
--- /dev/null
@@ -0,0 +1,93 @@
+package jalview.datamodel.features;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A singleton class to hold the set of attributes known for each feature type
+ */
+public class FeatureAttributes
+{
+  private static FeatureAttributes instance = new FeatureAttributes();
+
+  private Map<String, Set<String>> attributes;
+
+  /**
+   * Answers the singleton instance of this class
+   * 
+   * @return
+   */
+  public static FeatureAttributes getInstance()
+  {
+    return instance;
+  }
+
+  private FeatureAttributes()
+  {
+    attributes = new HashMap<>();
+  }
+
+  /**
+   * Answers the attributes known for the given feature type, in alphabetical
+   * order (not case sensitive), or an empty set if no attributes are known
+   * 
+   * @param featureType
+   * @return
+   */
+  public List<String> getAttributes(String featureType)
+  {
+    if (!attributes.containsKey(featureType))
+    {
+      return Collections.<String> emptyList();
+    }
+
+    return new ArrayList<>(attributes.get(featureType));
+  }
+
+  /**
+   * Answers true if at least one attribute is known for the given feature type,
+   * else false
+   * 
+   * @param featureType
+   * @return
+   */
+  public boolean hasAttributes(String featureType)
+  {
+
+    if (attributes.containsKey(featureType))
+    {
+      if (!attributes.get(featureType).isEmpty())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Records the given attribute name for the given feature type
+   * 
+   * @param featureType
+   * @param attName
+   */
+  public void addAttribute(String featureType, String attName)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+
+    if (!attributes.containsKey(featureType))
+    {
+      attributes.put(featureType, new TreeSet<String>(
+              String.CASE_INSENSITIVE_ORDER));
+    }
+
+    attributes.get(featureType).add(attName);
+  }
+}
index 96efb41..1be1b82 100644 (file)
@@ -3,6 +3,13 @@ package jalview.datamodel.features;
 import java.util.HashMap;
 import java.util.Map;
 
+/**
+ * A singleton to hold metadata about feature attributes, keyed by a unique
+ * feature source identifier
+ * 
+ * @author gmcarstairs
+ *
+ */
 public class FeatureSources
 {
   private static FeatureSources instance = new FeatureSources();
@@ -10,7 +17,7 @@ public class FeatureSources
   private Map<String, FeatureSourceI> sources;
 
   /**
-   * Answers the singelton instance of this class
+   * Answers the singleton instance of this class
    * 
    * @return
    */
index d8db546..89b64a7 100644 (file)
@@ -29,6 +29,7 @@ import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
+import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusAdapter;
@@ -221,54 +222,35 @@ public class FeatureColourChooser extends JalviewDialog
 
   private void jbInit() throws Exception
   {
+    this.setLayout(new GridLayout(4, 1));
 
-    minColour.setFont(JvSwingUtils.getLabelFont());
-    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
-    minColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (minColour.isEnabled())
-        {
-          minColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setFont(JvSwingUtils.getLabelFont());
-    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
-    maxColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (maxColour.isEnabled())
-        {
-          maxColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setBorder(new LineBorder(Color.black));
-    JLabel minText = new JLabel(MessageManager.getString("label.min"));
-    minText.setFont(JvSwingUtils.getLabelFont());
-    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
-    maxText.setFont(JvSwingUtils.getLabelFont());
-    this.setLayout(new BorderLayout());
-    JPanel jPanel1 = new JPanel();
-    jPanel1.setBackground(Color.white);
-    JPanel jPanel2 = new JPanel();
-    jPanel2.setLayout(new FlowLayout());
-    jPanel2.setBackground(Color.white);
+    JPanel colourByPanel = initColoursPanel();
+
+    JPanel thresholdPanel = initThresholdPanel();
+
+    JPanel okCancelPanel = initOkCancelPanel();
+
+    this.add(colourByPanel);
+    this.add(thresholdPanel);
+
+    this.add(okCancelPanel);
+  }
+
+  /**
+   * Lay out fields for threshold options
+   * 
+   * @return
+   */
+  protected JPanel initThresholdPanel()
+  {
+    JPanel thresholdPanel = new JPanel();
+    thresholdPanel.setLayout(new FlowLayout());
     threshold.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        threshold_actionPerformed();
+        changeColour(true);
       }
     });
     threshold.setToolTipText(MessageManager
@@ -280,8 +262,6 @@ public class FeatureColourChooser extends JalviewDialog
     threshold.addItem(MessageManager
             .getString("label.threshold_feature_below_threshold")); // index 2
 
-    JPanel jPanel3 = new JPanel();
-    jPanel3.setLayout(new FlowLayout());
     thresholdValue.addActionListener(new ActionListener()
     {
       @Override
@@ -308,7 +288,7 @@ public class FeatureColourChooser extends JalviewDialog
             MessageManager.getString("label.adjust_threshold"));
     thresholdValue.setEnabled(false);
     thresholdValue.setColumns(7);
-    jPanel3.setBackground(Color.white);
+    thresholdPanel.setBackground(Color.white);
     thresholdIsMin.setBackground(Color.white);
     thresholdIsMin
             .setText(MessageManager.getString("label.threshold_minmax"));
@@ -319,40 +299,101 @@ public class FeatureColourChooser extends JalviewDialog
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        thresholdIsMin_actionPerformed();
+        changeColour(true);
       }
     });
-    colourByLabel.setBackground(Color.white);
-    colourByLabel
-            .setText(MessageManager.getString("label.colour_by_label"));
-    colourByLabel.setToolTipText(MessageManager.getString(
-            "label.display_features_same_type_different_label_using_different_colour"));
-    colourByLabel.addActionListener(new ActionListener()
+    thresholdPanel.add(threshold);
+    thresholdPanel.add(slider);
+    thresholdPanel.add(thresholdValue);
+    thresholdPanel.add(thresholdIsMin);
+    return thresholdPanel;
+  }
+
+  /**
+   * Lay out OK and Cancel buttons
+   * 
+   * @return
+   */
+  protected JPanel initOkCancelPanel()
+  {
+    JPanel okCancelPanel = new JPanel();
+    okCancelPanel.setBackground(Color.white);
+    okCancelPanel.add(ok);
+    okCancelPanel.add(cancel);
+    return okCancelPanel;
+  }
+
+  /**
+   * Lay out Colour by Label and min/max colour widgets
+   * 
+   * @return
+   */
+  protected JPanel initColoursPanel()
+  {
+    JPanel colourByPanel = new JPanel();
+    colourByPanel.setLayout(new FlowLayout());
+    colourByPanel.setBackground(Color.white);
+    minColour.setFont(JvSwingUtils.getLabelFont());
+    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    minColour.setPreferredSize(new Dimension(40, 20));
+    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
+    minColour.addMouseListener(new MouseAdapter()
     {
       @Override
-      public void actionPerformed(ActionEvent actionEvent)
+      public void mousePressed(MouseEvent e)
       {
-        colourByLabel_actionPerformed();
+        if (minColour.isEnabled())
+        {
+          minColour_actionPerformed();
+        }
       }
     });
+    maxColour.setFont(JvSwingUtils.getLabelFont());
+    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    maxColour.setPreferredSize(new Dimension(40, 20));
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+    maxColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (maxColour.isEnabled())
+        {
+          maxColour_actionPerformed();
+        }
+      }
+    });
+    maxColour.setBorder(new LineBorder(Color.black));
+    JLabel minText = new JLabel(MessageManager.getString("label.min"));
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
+    maxText.setFont(JvSwingUtils.getLabelFont());
 
     JPanel colourPanel = new JPanel();
     colourPanel.setBackground(Color.white);
-    jPanel1.add(ok);
-    jPanel1.add(cancel);
-    jPanel2.add(colourByLabel, BorderLayout.WEST);
-    jPanel2.add(colourPanel, BorderLayout.EAST);
     colourPanel.add(minText);
     colourPanel.add(minColour);
     colourPanel.add(maxText);
     colourPanel.add(maxColour);
-    this.add(jPanel3, BorderLayout.CENTER);
-    jPanel3.add(threshold);
-    jPanel3.add(slider);
-    jPanel3.add(thresholdValue);
-    jPanel3.add(thresholdIsMin);
-    this.add(jPanel1, BorderLayout.SOUTH);
-    this.add(jPanel2, BorderLayout.NORTH);
+    colourByPanel.add(colourByLabel, BorderLayout.WEST);
+    colourByPanel.add(colourPanel, BorderLayout.EAST);
+
+    colourByLabel.setBackground(Color.white);
+    colourByLabel
+            .setText(MessageManager.getString("label.colour_by_label"));
+    colourByLabel
+            .setToolTipText(MessageManager
+                    .getString("label.display_features_same_type_different_label_using_different_colour"));
+    colourByLabel.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        changeColour(true);
+      }
+    });
+
+    return colourByPanel;
   }
 
   /**
@@ -505,6 +546,7 @@ public class FeatureColourChooser extends JalviewDialog
       maxColour.setForeground(oldmaxColour);
       minColour.setForeground(oldminColour);
     }
+
     fr.setColour(type, acg);
     cs = acg;
     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
@@ -544,14 +586,6 @@ public class FeatureColourChooser extends JalviewDialog
   }
 
   /**
-   * Action on change of choice of No / Above / Below Threshold
-   */
-  protected void threshold_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  /**
    * Action on text entry of a threshold value
    */
   protected void thresholdValue_actionPerformed()
@@ -594,16 +628,6 @@ public class FeatureColourChooser extends JalviewDialog
     changeColour(false);
   }
 
-  protected void thresholdIsMin_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  protected void colourByLabel_actionPerformed()
-  {
-    changeColour(true);
-  }
-
   void addActionListener(ActionListener graduatedColorEditor)
   {
     if (colourEditor != null)
index 3f1d9c7..d724b8c 100644 (file)
@@ -25,6 +25,7 @@ import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
@@ -34,19 +35,29 @@ import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.QuickSort;
+import jalview.util.ReverseListIterator;
+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 jalview.viewmodel.AlignmentViewport;
+import jalview.ws.DasSequenceFeatureFetcher;
 import jalview.ws.dbsources.das.api.jalviewSourceI;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
@@ -60,6 +71,7 @@ import java.io.FileOutputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Hashtable;
@@ -72,11 +84,13 @@ import java.util.Vector;
 import javax.help.HelpSetException;
 import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
 import javax.swing.Icon;
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
 import javax.swing.JDialog;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
@@ -84,15 +98,19 @@ import javax.swing.JLayeredPane;
 import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
+import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
 import javax.swing.JSlider;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicArrowButton;
 import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
@@ -100,11 +118,13 @@ import javax.swing.table.TableCellRenderer;
 public class FeatureSettings extends JPanel
         implements FeatureSettingsControllerI
 {
-  DasSourceBrowser dassourceBrowser;
+  private static final int MIN_WIDTH = 400;
 
-  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
+  private static final int MIN_HEIGHT = 400;
+
+  DasSourceBrowser dassourceBrowser;
 
-  JPanel settingsPane = new JPanel();
+  DasSequenceFeatureFetcher dasFeatureFetcher;
 
   JPanel dasSettingsPane = new JPanel();
 
@@ -112,10 +132,15 @@ public class FeatureSettings extends JPanel
 
   public final AlignFrame af;
 
+  /*
+   * 'original' fields hold settings to restore on Cancel
+   */
   Object[][] originalData;
 
   private float originalTransparency;
 
+  private Map<String, KeyedMatcherSetI> originalFilters;
+
   final JInternalFrame frame;
 
   JScrollPane scrollPane = new JScrollPane();
@@ -125,30 +150,68 @@ public class FeatureSettings extends JPanel
   JPanel groupPanel;
 
   JSlider transparency = new JSlider();
-
-  JPanel transPanel = new JPanel(new GridLayout(1, 2));
-
-  private static final int MIN_WIDTH = 400;
-
-  private static final int MIN_HEIGHT = 400;
   
-  /**
+  /*
    * when true, constructor is still executing - so ignore UI events
    */
   protected volatile boolean inConstruction = true;
 
+  int selectedRow = -1;
+
+  JButton fetchDAS = new JButton();
+
+  JButton saveDAS = new JButton();
+
+  JButton cancelDAS = new JButton();
+
+  boolean resettingTable = false;
+
+  /*
+   * true when Feature Settings are updating from feature renderer
+   */
+  private boolean handlingUpdate = false;
+
+  /*
+   * holds {featureCount, totalExtent} for each feature type
+   */
+  Map<String, float[]> typeWidth = null;
+
+  /*
+   * fields of the feature filters tab
+   */
+  private JPanel filtersPane;
+
+  private JPanel chooseFiltersPanel;
+
+  private JComboBox<String> filteredFeatureChoice;
+
+  private JRadioButton andFilters;
+
+  private JRadioButton orFilters;
+
+  /*
+   * filters for the currently selected feature type
+   */
+  private List<KeyedMatcherI> filters;
+
+  private JTextArea filtersAsText;
+
   /**
    * Constructor
    * 
    * @param af
    */
-  public FeatureSettings(AlignFrame af)
+  public FeatureSettings(AlignFrame alignFrame)
   {
-    this.af = af;
+    this.af = alignFrame;
     fr = af.getFeatureRenderer();
-    // allow transparency to be recovered
-    transparency.setMaximum(100
-            - (int) ((originalTransparency = fr.getTransparency()) * 100));
+
+    // save transparency for restore on Cancel
+    originalTransparency = fr.getTransparency();
+    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
+    transparency.setMaximum(100 - originalTransparencyAsPercent);
+
+    originalFilters = fr.getFeatureFilters();
 
     try
     {
@@ -286,13 +349,13 @@ public class FeatureSettings extends JPanel
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              475, 480);
+              600, 480);
     }
     else
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              400, 450);
+              600, 450);
     }
     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
@@ -478,16 +541,6 @@ public class FeatureSettings extends JPanel
     men.show(table, x, y);
   }
 
-  /**
-   * true when Feature Settings are updating from feature renderer
-   */
-  private boolean handlingUpdate = false;
-
-  /**
-   * holds {featureCount, totalExtent} for each feature type
-   */
-  Map<String, float[]> typeWidth = null;
-
   @Override
   synchronized public void discoverAllFeatureData()
   {
@@ -507,6 +560,8 @@ public class FeatureSettings extends JPanel
       }
     }
 
+    populateFilterableFeatures();
+
     resetTable(null);
 
     validate();
@@ -549,8 +604,6 @@ public class FeatureSettings extends JPanel
     return visible;
   }
 
-  boolean resettingTable = false;
-
   synchronized void resetTable(String[] groupChanged)
   {
     if (resettingTable)
@@ -1063,62 +1116,26 @@ public class FeatureSettings extends JPanel
     }
   }
 
-  int selectedRow = -1;
-
-  JTabbedPane tabbedPane = new JTabbedPane();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  BorderLayout borderLayout2 = new BorderLayout();
-
-  BorderLayout borderLayout3 = new BorderLayout();
-
-  JPanel bigPanel = new JPanel();
-
-  BorderLayout borderLayout4 = new BorderLayout();
-
-  JButton invert = new JButton();
-
-  JPanel buttonPanel = new JPanel();
-
-  JButton cancel = new JButton();
-
-  JButton ok = new JButton();
-
-  JButton loadColours = new JButton();
-
-  JButton saveColours = new JButton();
-
-  JPanel dasButtonPanel = new JPanel();
-
-  JButton fetchDAS = new JButton();
-
-  JButton saveDAS = new JButton();
-
-  JButton cancelDAS = new JButton();
-
-  JButton optimizeOrder = new JButton();
-
-  JButton sortByScore = new JButton();
+  private void jbInit() throws Exception
+  {
+    this.setLayout(new BorderLayout());
 
-  JButton sortByDens = new JButton();
+    JPanel settingsPane = new JPanel();
+    settingsPane.setLayout(new BorderLayout());
 
-  JButton help = new JButton();
+    filtersPane = new JPanel();
 
-  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
+    dasSettingsPane.setLayout(new BorderLayout());
 
-  private void jbInit() throws Exception
-  {
-    this.setLayout(borderLayout1);
-    settingsPane.setLayout(borderLayout2);
-    dasSettingsPane.setLayout(borderLayout3);
-    bigPanel.setLayout(borderLayout4);
+    JPanel bigPanel = new JPanel();
+    bigPanel.setLayout(new BorderLayout());
 
     groupPanel = new JPanel();
     bigPanel.add(groupPanel, BorderLayout.NORTH);
 
+    JButton invert = new JButton(
+            MessageManager.getString("label.invert_selection"));
     invert.setFont(JvSwingUtils.getLabelFont());
-    invert.setText(MessageManager.getString("label.invert_selection"));
     invert.addActionListener(new ActionListener()
     {
       @Override
@@ -1127,8 +1144,10 @@ public class FeatureSettings extends JPanel
         invertSelection();
       }
     });
+
+    JButton optimizeOrder = new JButton(
+            MessageManager.getString("label.optimise_order"));
     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
-    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
     optimizeOrder.addActionListener(new ActionListener()
     {
       @Override
@@ -1137,9 +1156,10 @@ public class FeatureSettings extends JPanel
         orderByAvWidth();
       }
     });
+
+    JButton sortByScore = new JButton(
+            MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.setFont(JvSwingUtils.getLabelFont());
-    sortByScore
-            .setText(MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.addActionListener(new ActionListener()
     {
       @Override
@@ -1148,9 +1168,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureScore(null);
       }
     });
-    sortByDens.setFont(JvSwingUtils.getLabelFont());
-    sortByDens.setText(
+    JButton sortByDens = new JButton(
             MessageManager.getString("label.sequence_sort_by_density"));
+    sortByDens.setFont(JvSwingUtils.getLabelFont());
     sortByDens.addActionListener(new ActionListener()
     {
       @Override
@@ -1159,8 +1179,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureDensity(null);
       }
     });
+
+    JButton help = new JButton(MessageManager.getString("action.help"));
     help.setFont(JvSwingUtils.getLabelFont());
-    help.setText(MessageManager.getString("action.help"));
     help.addActionListener(new ActionListener()
     {
       @Override
@@ -1191,20 +1212,23 @@ public class FeatureSettings extends JPanel
         }
       }
     });
+
+    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     cancel.setFont(JvSwingUtils.getLabelFont());
-    cancel.setText(MessageManager.getString("action.cancel"));
     cancel.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
         fr.setTransparency(originalTransparency);
+        fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
         close();
       }
     });
+
+    JButton ok = new JButton(MessageManager.getString("action.ok"));
     ok.setFont(JvSwingUtils.getLabelFont());
-    ok.setText(MessageManager.getString("action.ok"));
     ok.addActionListener(new ActionListener()
     {
       @Override
@@ -1213,8 +1237,10 @@ public class FeatureSettings extends JPanel
         close();
       }
     });
+
+    JButton loadColours = new JButton(
+            MessageManager.getString("label.load_colours"));
     loadColours.setFont(JvSwingUtils.getLabelFont());
-    loadColours.setText(MessageManager.getString("label.load_colours"));
     loadColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1223,8 +1249,10 @@ public class FeatureSettings extends JPanel
         load();
       }
     });
+
+    JButton saveColours = new JButton(
+            MessageManager.getString("label.save_colours"));
     saveColours.setFont(JvSwingUtils.getLabelFont());
-    saveColours.setText(MessageManager.getString("label.save_colours"));
     saveColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1267,6 +1295,8 @@ public class FeatureSettings extends JPanel
         saveDAS_actionPerformed(e);
       }
     });
+
+    JPanel dasButtonPanel = new JPanel();
     dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
     dasSettingsPane.setBorder(null);
     cancelDAS.setEnabled(false);
@@ -1279,32 +1309,455 @@ public class FeatureSettings extends JPanel
         cancelDAS_actionPerformed(e);
       }
     });
-    this.add(tabbedPane, java.awt.BorderLayout.CENTER);
+
+    JTabbedPane tabbedPane = new JTabbedPane();
+    this.add(tabbedPane, BorderLayout.CENTER);
     tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
             settingsPane);
-    tabbedPane.addTab(MessageManager.getString("label.das_settings"),
-            dasSettingsPane);
-    bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH);
+    tabbedPane.addTab(MessageManager.getString("label.filters"),
+            filtersPane);
+    // tabbedPane.addTab(MessageManager.getString("label.das_settings"),
+    // dasSettingsPane);
+
+    JPanel transPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(transPanel, BorderLayout.SOUTH);
+
+    JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
     transbuttons.add(invert);
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    JPanel sliderPanel = new JPanel();
-    sliderPanel.add(transparency);
     transPanel.add(transparency);
     transPanel.add(transbuttons);
+
+    JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
     buttonPanel.add(cancel);
     buttonPanel.add(loadColours);
     buttonPanel.add(saveColours);
-    bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER);
-    dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH);
+    bigPanel.add(scrollPane, BorderLayout.CENTER);
+    dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
     dasButtonPanel.add(fetchDAS);
     dasButtonPanel.add(cancelDAS);
     dasButtonPanel.add(saveDAS);
-    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
-    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
+    settingsPane.add(bigPanel, BorderLayout.CENTER);
+    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
+
+    initFiltersTab();
+  }
+
+  /**
+   * Populates initial layout of the feature attribute filters panel
+   */
+  protected void initFiltersTab()
+  {
+    filters = new ArrayList<>();
+
+    /*
+     * choose feature type
+     */
+    JPanel chooseTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    chooseTypePanel.setBackground(Color.white);
+    chooseTypePanel.setBorder(BorderFactory
+            .createTitledBorder(MessageManager
+                    .getString("label.feature_type")));
+    filteredFeatureChoice = new JComboBox<>();
+    filteredFeatureChoice.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        refreshFiltersDisplay();
+      }
+    });
+    chooseTypePanel.add(new JLabel(MessageManager
+            .getString("label.feature_to_filter")));
+    chooseTypePanel.add(filteredFeatureChoice);
+    populateFilterableFeatures();
+
+    /*
+     * the panel with the filters for the selected feature type
+     */
+    JPanel filtersPanel = new JPanel(new GridLayout(0, 1));
+    filtersPanel.setBackground(Color.white);
+    filtersPanel.setBorder(BorderFactory
+            .createTitledBorder(MessageManager.getString("label.filters")));
+
+    /*
+     * add AND or OR radio buttons
+     */
+    JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    andOrPanel.setBackground(Color.white);
+    andFilters = new JRadioButton("And");
+    orFilters = new JRadioButton("Or");
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filtersChanged();
+      }
+    };
+    andFilters.addActionListener(actionListener);
+    orFilters.addActionListener(actionListener);
+    ButtonGroup andOr = new ButtonGroup();
+    andOr.add(andFilters);
+    andOr.add(orFilters);
+    andFilters.setSelected(true);
+    andOrPanel.add(new JLabel(MessageManager
+            .getString("label.join_conditions")));
+    andOrPanel.add(andFilters);
+    andOrPanel.add(orFilters);
+    filtersPanel.add(andOrPanel);
+
+    /*
+     * panel with filters - populated by refreshFiltersDisplay
+     */
+    chooseFiltersPanel = new JPanel(new GridLayout(0, 1));
+    filtersPanel.add(chooseFiltersPanel);
+
+    /*
+     * a read-only text view of the current filters
+     */
+    JPanel showFiltersPanel = new JPanel(new BorderLayout(5, 5));
+    showFiltersPanel.setBackground(Color.white);
+    showFiltersPanel.setBorder(BorderFactory
+            .createTitledBorder(MessageManager
+                    .getString("label.match_condition")));
+    filtersAsText = new JTextArea();
+    filtersAsText.setLineWrap(true);
+    filtersAsText.setWrapStyleWord(true);
+    showFiltersPanel.add(filtersAsText);
+
+    filtersPane.setLayout(new BorderLayout());
+    filtersPane.add(chooseTypePanel, BorderLayout.NORTH);
+    filtersPane.add(filtersPanel, BorderLayout.CENTER);
+    filtersPane.add(showFiltersPanel, BorderLayout.SOUTH);
+
+    /*
+     * update display for initial feature type selection
+     */
+    refreshFiltersDisplay();
+  }
+
+  /**
+   * Adds entries to the 'choose feature to filter' drop-down choice. Only
+   * feature types which have known attributes (so can be filtered) are
+   * included, so recall this method to update the list (check for newly added
+   * attributes).
+   */
+  protected void populateFilterableFeatures()
+  {
+    /*
+     * suppress action handler while updating the list
+     */
+    ItemListener listener = filteredFeatureChoice.getItemListeners()[0];
+    filteredFeatureChoice.removeItemListener(listener);
+
+    filteredFeatureChoice.removeAllItems();
+    ReverseListIterator<String> types = new ReverseListIterator<>(
+            fr.getRenderOrder());
+
+    boolean found = false;
+    while (types.hasNext())
+    {
+      String type = types.next();
+      if (FeatureAttributes.getInstance().hasAttributes(type))
+      {
+        filteredFeatureChoice.addItem(type);
+        found = true;
+      }
+    }
+    if (!found)
+    {
+      filteredFeatureChoice
+              .addItem("No filterable feature attributes known");
+    }
+
+    filteredFeatureChoice.addItemListener(listener);
+
+  }
+
+  /**
+   * Refreshes the display to show any filters currently configured for the
+   * selected feature type (editable, with 'remove' option), plus one extra row
+   * for adding a condition. This should be called on change of selected feature
+   * type, or after a filter has been removed, added or amended.
+   */
+  protected void refreshFiltersDisplay()
+  {
+    /*
+     * clear the panel and list of filter conditions
+     */
+    chooseFiltersPanel.removeAll();
+
+    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
+
+    filters.clear();
+
+    /*
+     * look up attributes known for feature type
+     */
+    List<String> attNames = FeatureAttributes.getInstance().getAttributes(
+            selectedType);
+
+    /*
+     * if this feature type has filters set, load them first
+     */
+    KeyedMatcherSetI featureFilters = fr.getFeatureFilter(selectedType);
+    filtersAsText.setText("");
+    if (featureFilters != null)
+    {
+      filtersAsText.setText(featureFilters.toString());
+      if (!featureFilters.isAnded())
+      {
+        orFilters.setSelected(true);
+      }
+      Iterator<KeyedMatcherI> matchers = featureFilters.getMatchers();
+      while (matchers.hasNext())
+      {
+        filters.add(matchers.next());
+      }
+    }
+
+    /*
+     * and an empty filter for the user to populate (add)
+     */
+    KeyedMatcherI noFilter = new KeyedMatcher("", Condition.values()[0], "");
+    filters.add(noFilter);
+
+    /*
+     * render the conditions in rows, each in its own JPanel
+     */
+    int i = 0;
+    for (KeyedMatcherI filter : filters)
+    {
+      String key = filter.getKey();
+      Condition condition = filter.getMatcher()
+              .getCondition();
+      String pattern = filter.getMatcher().getPattern();
+      JPanel row = addFilter(key, attNames, condition, pattern, i);
+      chooseFiltersPanel.add(row);
+      i++;
+    }
+
+    filtersPane.validate();
+    filtersPane.repaint();
+  }
+
+  /**
+   * A helper method that constructs a panel with one filter condition:
+   * <ul>
+   * <li>a drop-down list of attribute names to choose from</li>
+   * <li>a drop-down list of conditions to choose from</li>
+   * <li>a text field for input of a match pattern</li>
+   * <li>optionally, a 'remove' button</li>
+   * </ul>
+   * If attribute, condition or pattern are not null, they are set as defaults
+   * for the input fields. The 'remove' button is added unless the pattern is
+   * null or empty (incomplete filter condition).
+   * 
+   * @param attribute
+   * @param attNames
+   * @param cond
+   * @param pattern
+   * @param i
+   * @return
+   */
+  protected JPanel addFilter(String attribute, List<String> attNames,
+          Condition cond, String pattern, int i)
+  {
+    JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    filterRow.setBackground(Color.white);
+
+    /*
+     * inputs for attribute, condition, pattern
+     */
+    JComboBox<String> attCombo = new JComboBox<>();
+    JComboBox<Condition> condCombo = new JComboBox<>();
+    JTextField patternField = new JTextField(8);
+
+    /*
+     * action handlers that validate and (if valid) apply changes
+     */
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (attCombo.getSelectedItem() != null)
+        {
+          if (validateFilter(patternField, condCombo))
+          {
+            updateFilter(attCombo, condCombo, patternField, i);
+            filtersChanged();
+          }
+        }
+      }
+    };
+    ItemListener itemListener = new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    };
+
+    /*
+     * drop-down choice of attribute
+     */
+    if (attNames.isEmpty())
+    {
+      attCombo.addItem("---");
+      attCombo.setToolTipText(MessageManager
+              .getString("label.no_attributes_known"));
+    }
+    else
+    {
+      attCombo.setToolTipText("");
+      for (String attName : attNames)
+      {
+        attCombo.addItem(attName);
+      }
+      if ("".equals(attribute))
+      {
+        attCombo.setSelectedItem(null);
+      }
+      else
+      {
+        attCombo.setSelectedItem(attribute);
+      }
+      attCombo.addItemListener(itemListener);
+    }
+    filterRow.add(attCombo);
+
+    /*
+     * drop-down choice of test condition
+     */
+    for (Condition c : Condition.values())
+    {
+      condCombo.addItem(c);
+    }
+    if (cond != null)
+    {
+      condCombo.setSelectedItem(cond);
+    }
+    condCombo.addItemListener(itemListener);
+    filterRow.add(condCombo);
+
+    /*
+     * pattern to match against
+     */
+    patternField.setText(pattern);
+    patternField.addActionListener(actionListener);
+    patternField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    });
+    filterRow.add(patternField);
+
+    /*
+     * add remove button if filter is populated (non-empty pattern)
+     */
+    if (pattern != null && pattern.trim().length() > 0)
+    {
+      // todo: gif for - button
+      JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+      removeCondition.setToolTipText(MessageManager
+              .getString("label.delete_row"));
+      removeCondition.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          filters.remove(i);
+          filtersChanged();
+        }
+      });
+      filterRow.add(removeCondition);
+    }
+
+    return filterRow;
+  }
+
+  /**
+   * Action on any change to feature filtering, namely
+   * <ul>
+   * <li>change of selected attribute</li>
+   * <li>change of selected condition</li>
+   * <li>change of match pattern</li>
+   * <li>removal of a condition</li>
+   * </ul>
+   * The action should be to
+   * <ul>
+   * <li>parse and validate the filters</li>
+   * <li>if valid, update the filter text box</li>
+   * <li>and apply the filters to the viewport</li>
+   * </ul>
+   */
+  protected void filtersChanged()
+  {
+    /*
+     * update the filter conditions for the feature type
+     */
+    String featureType = (String) filteredFeatureChoice.getSelectedItem();
+    boolean anded = andFilters.isSelected();
+    KeyedMatcherSetI combined = new KeyedMatcherSet();
+
+    for (KeyedMatcherI filter : filters)
+    {
+      String pattern = filter.getMatcher().getPattern();
+      if (pattern.trim().length() > 0)
+      {
+        if (anded)
+        {
+          combined.and(filter);
+        }
+        else
+        {
+          combined.or(filter);
+        }
+      }
+    }
+
+    /*
+     * save the filter conditions in the FeatureRenderer
+     * (note this might now be an empty filter with no conditions)
+     */
+    fr.setFeatureFilter(featureType, combined);
+
+    filtersAsText.setText(combined.toString());
+
+    refreshFiltersDisplay();
+
+    af.alignPanel.paintAlignment(true, true);
+  }
+
+  /**
+   * Constructs a filter condition from the given input fields, and replaces the
+   * condition at filterIndex with the new one
+   * 
+   * @param attCombo
+   * @param condCombo
+   * @param valueField
+   * @param filterIndex
+   */
+  protected void 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(attName, cond, pattern);
+
+    filters.set(filterIndex, km);
   }
 
   public void fetchDAS_actionPerformed(ActionEvent e)
@@ -1464,6 +1917,51 @@ public class FeatureSettings extends JPanel
             JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
   }
 
+  /**
+   * Answers true unless a numeric condition has been selected with a
+   * non-numeric value. Sets the value field to RED with a tooltip if in error.
+   * <p>
+   * If the pattern entered is empty, this method returns false, but does not
+   * mark the field as invalid. This supports selecting an attribute for a new
+   * condition before a match pattern has been entered.
+   * 
+   * @param value
+   * @param condCombo
+   */
+  protected boolean validateFilter(JTextField value,
+          JComboBox<Condition> condCombo)
+  {
+    if (value == null || condCombo == null)
+    {
+      return true; // fields not populated
+    }
+  
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    value.setBackground(Color.white);
+    value.setToolTipText("");
+    String v1 = value.getText().trim();
+    if (v1.length() == 0)
+    {
+      return false;
+    }
+
+    if (cond.isNumeric())
+    {
+      try
+      {
+        Float.valueOf(v1);
+      } catch (NumberFormatException e)
+      {
+        value.setBackground(Color.red);
+        value.setToolTipText(MessageManager
+                .getString("label.numeric_required"));
+        return false;
+      }
+    }
+  
+    return true;
+  }
+
   // ///////////////////////////////////////////////////////////////////////
   // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
   // ///////////////////////////////////////////////////////////////////////
index 05f5ffc..1008203 100644 (file)
@@ -27,8 +27,8 @@ import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
-import java.awt.event.WindowListener;
 
 import javax.swing.JButton;
 import javax.swing.JDialog;
@@ -118,55 +118,14 @@ public abstract class JalviewDialog extends JPanel
         closeDialog();
       }
     });
-    frame.addWindowListener(new WindowListener()
+    frame.addWindowListener(new WindowAdapter()
     {
-
-      @Override
-      public void windowOpened(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowIconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeiconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeactivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
       @Override
       public void windowClosing(WindowEvent e)
       {
         // user has cancelled the dialog
         closeDialog();
       }
-
-      @Override
-      public void windowClosed(WindowEvent e)
-      {
-      }
-
-      @Override
-      public void windowActivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
     });
   }
 
index 1f47da3..6687e6a 100644 (file)
@@ -304,14 +304,16 @@ public class FeatureRenderer extends FeatureRendererModel
       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
 
-      filterFeaturesForDisplay(overlaps, fc);
+      // filterFeaturesForDisplay(overlaps, fc);
 
       for (SequenceFeature sf : overlaps)
       {
-        Color featureColour = fc.getColor(sf);
+        Color featureColour = getColor(sf, fc);
         if (featureColour == null)
         {
-          // score feature outwith threshold for colouring
+          /*
+           * feature excluded by visibility settings, filters, or colour threshold
+           */
           continue;
         }
 
diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java
new file mode 100644 (file)
index 0000000..455f805
--- /dev/null
@@ -0,0 +1,57 @@
+package jalview.util.matcher;
+
+import jalview.util.MessageManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An enumeration for binary conditions that a user might choose from when
+ * setting filter or match conditions for values
+ */
+public enum Condition
+{
+  Contains(false), NotContains(false), Matches(false), NotMatches(false),
+  EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true);
+
+  private static Map<Condition, String> displayNames = new HashMap<>();
+  
+  private boolean numeric;
+
+  Condition(boolean isNumeric)
+  {
+    numeric = isNumeric;
+  }
+
+  /**
+   * Answers true if the condition does a numerical comparison, else false
+   * (string comparison)
+   * 
+   * @return
+   */
+  public boolean isNumeric()
+  {
+    return numeric;
+  }
+
+  /**
+   * 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.
+   * 
+   * @return
+   */
+  @Override
+  public String toString()
+  {
+    String name = displayNames.get(this);
+    if (name != null)
+    {
+      return name;
+    }
+    name = MessageManager
+            .getStringOrReturn("label.matchCondition_", name());
+    displayNames.put(this, name);
+    return name;
+  }
+}
diff --git a/src/jalview/util/matcher/KeyedMatcher.java b/src/jalview/util/matcher/KeyedMatcher.java
new file mode 100644 (file)
index 0000000..5e42e1c
--- /dev/null
@@ -0,0 +1,86 @@
+package jalview.util.matcher;
+
+import java.util.function.Function;
+
+/**
+ * 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 KeyedMatcher implements KeyedMatcherI
+{
+  final private String key;
+
+  final private MatcherI matcher;
+
+  /**
+   * Constructor given a key, a test condition and a match pattern
+   * 
+   * @param theKey
+   * @param cond
+   * @param pattern
+   */
+  public KeyedMatcher(String theKey, Condition cond, String pattern)
+  {
+    key = theKey;
+    matcher = new Matcher(cond, pattern);
+  }
+
+  /**
+   * Constructor given a key, a test condition and a numerical value to compare
+   * to. Note that if a non-numerical condition is specified, the float will be
+   * converted to a string.
+   * 
+   * @param theKey
+   * @param cond
+   * @param value
+   */
+  public KeyedMatcher(String theKey, Condition cond, float value)
+  {
+    key = theKey;
+    matcher = new Matcher(cond, value);
+  }
+
+  @Override
+  public boolean matches(Function<String, String> valueProvider)
+  {
+    String value = valueProvider.apply(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 debugging or
+   * logging. The format may change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append(key).append(" ").append(matcher.getCondition().toString())
+            .append(" ").append(matcher.getPattern());
+
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/util/matcher/KeyedMatcherI.java b/src/jalview/util/matcher/KeyedMatcherI.java
new file mode 100644 (file)
index 0000000..e9fe014
--- /dev/null
@@ -0,0 +1,36 @@
+package jalview.util.matcher;
+
+import java.util.function.Function;
+
+/**
+ * An interface for an object that can apply one or more match conditions, given
+ * a key-value provider. The match conditions are stored against key values, and
+ * applied to the value obtained by a key-value lookup.
+ * 
+ * @author gmcarstairs
+ */
+public interface KeyedMatcherI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param valueProvider
+   * @return
+   */
+  boolean matches(Function<String, String> valueProvider);
+
+  /**
+   * 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/util/matcher/KeyedMatcherSet.java b/src/jalview/util/matcher/KeyedMatcherSet.java
new file mode 100644 (file)
index 0000000..adc04ba
--- /dev/null
@@ -0,0 +1,122 @@
+package jalview.util.matcher;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+public class KeyedMatcherSet implements KeyedMatcherSetI
+{
+  List<KeyedMatcherI> matchConditions;
+
+  boolean andConditions;
+
+  /**
+   * Constructor
+   */
+  public KeyedMatcherSet()
+  {
+    matchConditions = new ArrayList<>();
+  }
+
+  @Override
+  public boolean matches(Function<String, String> valueProvider)
+  {
+    /*
+     * no conditions matches anything
+     */
+    if (matchConditions.isEmpty())
+    {
+      return true;
+    }
+
+    /*
+     * AND until failure
+     */
+    if (andConditions)
+    {
+      for (KeyedMatcherI m : matchConditions)
+      {
+        if (!m.matches(valueProvider))
+        {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /*
+     * OR until match
+     */
+    for (KeyedMatcherI m : matchConditions)
+    {
+      if (m.matches(valueProvider))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public KeyedMatcherSetI and(KeyedMatcherI 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 KeyedMatcherSetI or(KeyedMatcherI 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 Iterator<KeyedMatcherI> getMatchers()
+  {
+    return matchConditions.iterator();
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (KeyedMatcherI 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/util/matcher/KeyedMatcherSetI.java b/src/jalview/util/matcher/KeyedMatcherSetI.java
new file mode 100644 (file)
index 0000000..7cbebab
--- /dev/null
@@ -0,0 +1,65 @@
+package jalview.util.matcher;
+
+import java.util.Iterator;
+import java.util.function.Function;
+
+/**
+ * An interface to describe a set of one or more key-value match conditions,
+ * where all conditions are combined with either AND or OR
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface KeyedMatcherSetI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param valueProvider
+   * @return
+   */
+  boolean matches(Function<String, String> valueProvider);
+
+  /**
+   * 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
+   */
+  KeyedMatcherSetI and(KeyedMatcherI 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
+   */
+  KeyedMatcherSetI or(KeyedMatcherI m);
+
+  /**
+   * Answers an iterator over the combined match conditions
+   * 
+   * @return
+   */
+  Iterator<KeyedMatcherI> getMatchers();
+
+  /**
+   * Answers true if this object contains no conditions
+   * 
+   * @return
+   */
+  boolean isEmpty();
+}
diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java
new file mode 100644 (file)
index 0000000..d8c9361
--- /dev/null
@@ -0,0 +1,218 @@
+package jalview.util.matcher;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A bean to describe one attribute-based filter
+ */
+public class Matcher implements MatcherI
+{
+  /*
+   * the comparison condition
+   */
+  Condition condition;
+
+  /*
+   * the string value (upper-cased), or the regex, to compare to
+   * also holds the string form of float value if a numeric condition
+   */
+  String pattern;
+
+  /*
+   * the compiled regex if using a pattern match condition
+   * (reserved for possible future enhancement)
+   */
+  Pattern regexPattern;
+
+  /*
+   * the value to compare to for a numerical condition
+   */
+  float value;
+
+  /**
+   * Constructor
+   * 
+   * @param cond
+   * @param compareTo
+   * @return
+   * @throws NumberFormatException
+   *           if a numerical condition is specified with a non-numeric
+   *           comparision value
+   * @throws NullPointerException
+   *           if a null condition or comparison string is specified
+   */
+  public Matcher(Condition cond, String compareTo)
+  {
+    condition = cond;
+    if (cond.isNumeric())
+    {
+      value = Float.valueOf(compareTo);
+      pattern = String.valueOf(value);
+    }
+    else
+    {
+      // pattern matches will be non-case-sensitive
+      pattern = compareTo.toUpperCase();
+    }
+
+    // if we add regex conditions (e.g. matchesPattern), then
+    // pattern should hold the raw regex, and
+    // regexPattern = Pattern.compile(compareTo);
+  }
+
+  /**
+   * Constructor for a numerical match condition. Note that if a string
+   * comparison condition is specified, this will be converted to a comparison
+   * with the float value as string
+   * 
+   * @param cond
+   * @param compareTo
+   */
+  public Matcher(Condition cond, float compareTo)
+  {
+    Objects.requireNonNull(cond);
+    condition = cond;
+    value = compareTo;
+    pattern = String.valueOf(compareTo).toUpperCase();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("incomplete-switch")
+  @Override
+  public boolean matches(String val)
+  {
+    if (condition.isNumeric())
+    {
+      try
+      {
+        /*
+         * treat a null value (no such attribute) as
+         * failing any numerical filter condition
+         */
+        return val == null ? false : matches(Float.valueOf(val));
+      } catch (NumberFormatException e)
+      {
+        return false;
+      }
+    }
+    
+    /*
+     * a null value matches a negative condition, fails a positive test
+     */
+    if (val == null)
+    {
+      return condition == Condition.NotContains
+              || condition == Condition.NotMatches;
+    }
+    
+    String upper = val.toUpperCase().trim();
+    boolean matched = false;
+    switch(condition) {
+    case Matches:
+      matched = upper.equals(pattern);
+      break;
+    case NotMatches:
+      matched = !upper.equals(pattern);
+      break;
+    case Contains:
+      matched = upper.indexOf(pattern) > -1;
+      break;
+    case NotContains:
+      matched = upper.indexOf(pattern) == -1;
+      break;
+    }
+    return matched;
+  }
+
+  /**
+   * Applies a numerical comparison match condition
+   * 
+   * @param f
+   * @return
+   */
+  @SuppressWarnings("incomplete-switch")
+  boolean matches(float f)
+  {
+    if (!condition.isNumeric())
+    {
+      return matches(String.valueOf(f));
+    }
+    
+    boolean matched = false;
+    switch (condition) {
+    case LT:
+      matched = f < value;
+      break;
+    case LE:
+      matched = f <= value;
+      break;
+    case EQ:
+      matched = f == value;
+      break;
+    case NE:
+      matched = f != value;
+      break;
+    case GT:
+      matched = f > value;
+      break;
+    case GE:
+      matched = f >= value;
+      break;
+    }
+
+    return matched;
+  }
+
+  /**
+   * A simple hash function that guarantees that when two objects are equal,
+   * they have the same hashcode
+   */
+  @Override
+  public int hashCode()
+  {
+    return pattern.hashCode() + condition.hashCode() + (int) value;
+  }
+
+  /**
+   * equals is overridden so that we can safely remove Matcher objects from
+   * collections (e.g. delete an attribut match condition for a feature colour)
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj == null || !(obj instanceof Matcher))
+    {
+      return false;
+    }
+    Matcher m = (Matcher) obj;
+    return condition == m.condition && value == m.value
+            && pattern.equals(m.pattern);
+  }
+
+  @Override
+  public Condition getCondition()
+  {
+    return condition;
+  }
+
+  @Override
+  public String getPattern()
+  {
+    return pattern;
+  }
+
+  @Override
+  public float getFloatValue()
+  {
+    return value;
+  }
+
+  @Override
+  public String toString()
+  {
+    return condition.name() + " " + pattern;
+  }
+}
diff --git a/src/jalview/util/matcher/MatcherI.java b/src/jalview/util/matcher/MatcherI.java
new file mode 100644 (file)
index 0000000..ca6d44c
--- /dev/null
@@ -0,0 +1,18 @@
+package jalview.util.matcher;
+
+public interface MatcherI
+{
+  /**
+   * Answers true if the given value is matched, else false
+   * 
+   * @param s
+   * @return
+   */
+  boolean matches(String s);
+
+  Condition getCondition();
+
+  String getPattern();
+
+  float getFloatValue();
+}
index 2f30e94..6461748 100644 (file)
@@ -30,6 +30,7 @@ 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;
@@ -49,14 +50,25 @@ public abstract class FeatureRendererModel
         implements jalview.api.FeatureRenderer
 {
 
-  /**
+  /*
    * global transparency for feature
    */
   protected float transparency = 1.0f;
 
-  protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+  /*
+   * colour scheme for each feature type
+   */
+  protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
+
+  /*
+   * visibility flag for each feature group
+   */
+  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
 
-  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<String, Boolean>();
+  /*
+   * filters for each feature type
+   */
+  protected Map<String, KeyedMatcherSetI> featureFilters = new HashMap<>();
 
   protected String[] renderOrder;
 
@@ -100,6 +112,7 @@ public abstract class FeatureRendererModel
     this.renderOrder = frs.renderOrder;
     this.featureGroups = frs.featureGroups;
     this.featureColours = frs.featureColours;
+    this.featureFilters = frs.featureFilters;
     this.transparency = frs.transparency;
     this.featureOrder = frs.featureOrder;
     if (av != null && av != fr.getViewport())
@@ -557,20 +570,11 @@ public abstract class FeatureRendererModel
     return fc;
   }
 
-  /**
-   * Returns the configured colour for a particular feature instance. This
-   * includes calculation of 'colour by label', or of a graduated score colour,
-   * if applicable. It does not take into account feature visibility or colour
-   * transparency. Returns null for a score feature whose score value lies
-   * outside any colour threshold.
-   * 
-   * @param feature
-   * @return
-   */
+  @Override
   public Color getColour(SequenceFeature feature)
   {
     FeatureColourI fc = getFeatureStyle(feature.getType());
-    return fc.getColor(feature);
+    return getColor(feature, fc);
   }
 
   /**
@@ -995,11 +999,11 @@ public abstract class FeatureRendererModel
   }
 
   /**
-   * Removes from the list of features any that have a feature group that is not
-   * displayed, or duplicate the location of a feature of the same type (unless
-   * a graduated colour scheme or colour by label is applied). Should be used
-   * only for features of the same feature colour (which normally implies the
-   * same feature type).
+   * Removes from the list of features any that duplicate the location of a
+   * feature of the same type (unless feature is filtered out, or a graduated
+   * colour scheme or colour by label is applied). Should be used only for
+   * features of the same feature colour (which normally implies the same
+   * feature type).
    * 
    * @param features
    * @param fc
@@ -1019,11 +1023,6 @@ public abstract class FeatureRendererModel
     while (it.hasNext())
     {
       SequenceFeature sf = it.next();
-      if (featureGroupNotShown(sf))
-      {
-        it.remove();
-        continue;
-      }
 
       /*
        * a feature is redundant for rendering purposes if it has the
@@ -1045,4 +1044,88 @@ public abstract class FeatureRendererModel
     }
   }
 
+  @Override
+  public Map<String, KeyedMatcherSetI> getFeatureFilters()
+  {
+    return new HashMap<>(featureFilters);
+  }
+
+  @Override
+  public void setFeatureFilters(Map<String, KeyedMatcherSetI> filters)
+  {
+    featureFilters = filters;
+  }
+
+  @Override
+  public KeyedMatcherSetI getFeatureFilter(String featureType)
+  {
+    return featureFilters.get(featureType);
+  }
+
+  @Override
+  public void setFeatureFilter(String featureType, KeyedMatcherSetI filter)
+  {
+    if (filter == null || filter.isEmpty())
+    {
+      featureFilters.remove(featureType);
+    }
+    else
+    {
+      featureFilters.put(featureType, filter);
+    }
+  }
+
+  /**
+   * Answers the colour for the feature, or null if the feature is excluded by
+   * feature type or group visibility, by filters, or by colour threshold
+   * settings
+   * 
+   * @param sf
+   * @param fc
+   * @return
+   */
+  public Color getColor(SequenceFeature sf, FeatureColourI fc)
+  {
+    /*
+     * is the feature type displayed?
+     */
+    if (!showFeatureOfType(sf.getType()))
+    {
+      return null;
+    }
+
+    /*
+     * is the feature group displayed?
+     */
+    if (featureGroupNotShown(sf))
+    {
+      return null;
+    }
+
+    /*
+     * does the feature pass filters?
+     */
+    if (!featureMatchesFilters(sf))
+    {
+      return null;
+    }
+  
+    return fc.getColor(sf);
+  }
+
+  /**
+   * Answers true if there no are filters defined for the feature type, or this
+   * feature matches the filters. Answers false if the feature fails to match
+   * filters.
+   * 
+   * @param sf
+   * @return
+   */
+  protected boolean featureMatchesFilters(SequenceFeature sf)
+  {
+    KeyedMatcherSetI filter = featureFilters.get(sf.getType());
+    return filter == null ? true : filter.matches(key -> sf
+            .getValueAsString(key));
+  }
+
 }
index dc2ae11..6afaa54 100644 (file)
@@ -22,8 +22,10 @@ package jalview.viewmodel.seqfeatures;
 
 import jalview.api.FeatureColourI;
 import jalview.schemes.FeatureColour;
+import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -42,6 +44,11 @@ public class FeatureRendererSettings implements Cloneable
    */
   Map<String, FeatureColourI> featureColours;
 
+  /*
+   * map of {featureType, filters}
+   */
+  Map<String, KeyedMatcherSetI> featureFilters;
+
   float transparency;
 
   Map<String, Float> featureOrder;
@@ -72,7 +79,9 @@ public class FeatureRendererSettings implements Cloneable
     renderOrder = null;
     featureGroups = new ConcurrentHashMap<String, Boolean>();
     featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+    featureFilters = new HashMap<>();
     featureOrder = new ConcurrentHashMap<String, Float>();
+
     if (fr.renderOrder != null)
     {
       this.renderOrder = new String[fr.renderOrder.length];
@@ -100,6 +109,12 @@ public class FeatureRendererSettings implements Cloneable
         featureColours.put(next, new FeatureColour((FeatureColour) val));
       }
     }
+
+    if (fr.featureFilters != null)
+    {
+      this.featureFilters.putAll(fr.featureFilters);
+    }
+
     this.transparency = fr.transparency;
     if (fr.featureOrder != null)
     {
index 2e89b0e..efee93b 100644 (file)
@@ -25,6 +25,8 @@ import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.analysis.Finder;
 import jalview.api.AlignViewControllerI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Alignment;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
@@ -35,7 +37,9 @@ import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
 
+import java.awt.Color;
 import java.util.Arrays;
 import java.util.BitSet;
 
@@ -67,13 +71,14 @@ public class AlignViewControllerTest
             null));
     seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
             null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f,
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10,
+            10f,
             null));
     seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
-            0f, null));
+            10f, null));
     // disulfide bond is a 'contact feature' - only select its 'start' and 'end'
-    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12,
-            0f, null));
+    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc",
+            8, 12, 0f, null));
 
     /*
      * select the first five columns --> Metal in seq1 cols 4-5
@@ -86,9 +91,18 @@ public class AlignViewControllerTest
     sg.addSequence(seq3, false);
     sg.addSequence(seq4, false);
 
+    /*
+     * set features visible on a viewport as only visible features are selected
+     */
+    AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1,
+        seq2, seq3, seq4 }), 100, 100);
+    af.getFeatureRenderer().findAllFeatures(true);
+
+    AlignViewController avc = new AlignViewController(af, af.getViewport(),
+            af.alignPanel);
+
     BitSet bs = new BitSet();
-    int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
-            bs);
+    int seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(3)); // base 0
@@ -99,7 +113,7 @@ public class AlignViewControllerTest
      */
     sg.setEndRes(6);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(2, seqCount);
     assertEquals(4, bs.cardinality());
     assertTrue(bs.get(3));
@@ -113,7 +127,7 @@ public class AlignViewControllerTest
     sg.setStartRes(13);
     sg.setEndRes(13);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(1, bs.cardinality());
     assertTrue(bs.get(13));
@@ -124,18 +138,35 @@ public class AlignViewControllerTest
     sg.setStartRes(17);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
     /*
+     * threshold Metal to hide where score < 5
+     * seq1 feature in columns 4-6 is hidden
+     * seq2 feature in columns 6-7 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    af.getFeatureRenderer().setColour("Metal", fc);
+    sg.setStartRes(0);
+    sg.setEndRes(6);
+    bs.clear();
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    assertEquals(1, seqCount);
+    assertEquals(2, bs.cardinality());
+    assertTrue(bs.get(5));
+    assertTrue(bs.get(6));
+
+    /*
      * columns 11-13 should not match disulfide bond at 8/12
      */
     sg.setStartRes(10);
     sg.setEndRes(12);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
@@ -145,8 +176,7 @@ public class AlignViewControllerTest
     sg.setStartRes(5);
     sg.setEndRes(17);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(8));
@@ -158,7 +188,7 @@ public class AlignViewControllerTest
     sg.setStartRes(0);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Pfam", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
   }
index 8c9cbc9..c955979 100644 (file)
@@ -279,30 +279,30 @@ public class SequenceFeatureTest
   {
     // single locus, no group, no score
     SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
-    String expected = "<br><table><tr><td>Type</td><td>variant</td></tr>"
-            + "<tr><td>Start/end</td><td>22</td></tr>"
-            + "<tr><td>Description</td><td>G,C</td></tr></table>";
+    String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
     assertEquals(expected, sf.getDetailsReport());
 
     // contact feature
     sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
             null);
-    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td></tr>"
-            + "<tr><td>Start/end</td><td>28:31</td></tr>"
-            + "<tr><td>Description</td><td>a description</td></tr></table>";
+    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+            + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
     assertEquals(expected, sf.getDetailsReport());
 
     sf = new SequenceFeature("variant", "G,C", 22, 33,
             12.5f, "group");
     sf.setValue("Parent", "ENSG001");
     sf.setValue("Child", "ENSP002");
-    expected = "<br><table><tr><td>Type</td><td>variant</td></tr>"
-            + "<tr><td>Start/end</td><td>22-33</td></tr>"
-            + "<tr><td>Description</td><td>G,C</td></tr>"
-            + "<tr><td>Score</td><td>12.5</td></tr>"
-            + "<tr><td>Group</td><td>group</td></tr>"
-            + "<tr><td>Child</td><td>ENSP002</td></tr>"
-            + "<tr><td>Parent</td><td>ENSG001</td></tr></table>";
+    expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
+            + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
+            + "<tr><td>Group</td><td>group</td><td></td></tr>"
+            + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
+            + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
     assertEquals(expected, sf.getDetailsReport());
 
     /*
@@ -310,10 +310,10 @@ public class SequenceFeatureTest
      */
     String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
     sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
-    expected = "<br><table><tr><td>Type</td><td>Pfam</td></tr>"
-            + "<tr><td>Start/end</td><td>8-83</td></tr>"
-            + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td></tr>"
-            + "<tr><td>Group</td><td>Uniprot</td></tr></table>";
+    expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+            + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
+            + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
     assertEquals(expected, sf.getDetailsReport());
   }
 }
index af9c045..1ee25c7 100644 (file)
@@ -26,6 +26,7 @@ import static org.testng.Assert.assertNotSame;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.datamodel.Alignment;
@@ -39,6 +40,7 @@ import jalview.io.FileLoader;
 import jalview.io.Jalview2xmlTests;
 import jalview.renderer.ResidueShaderI;
 import jalview.schemes.BuriedColourScheme;
+import jalview.schemes.FeatureColour;
 import jalview.schemes.HelixColourScheme;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.StrandColourScheme;
@@ -69,16 +71,21 @@ public class AlignFrameTest
   {
     SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ");
     SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
-    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5,
-            Float.NaN, null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10,
-            Float.NaN, null));
+    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null));
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f,
+            null));
     seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4,
             Float.NaN, null));
     seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
             Float.NaN, null));
     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight());
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
+            al.getHeight());
+
+    /*
+     * make all features visible (select feature columns checks visibility)
+     */
+    alignFrame.getFeatureRenderer().findAllFeatures(true);
 
     /*
      * hiding a feature not present does nothing
@@ -86,13 +93,11 @@ public class AlignFrameTest
     assertFalse(alignFrame.hideFeatureColumns("exon", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
     assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-            .getHiddenColumnsCopy()
-            .isEmpty());
+            .getHiddenColumnsCopy().isEmpty());
     assertFalse(alignFrame.hideFeatureColumns("exon", false));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
     assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-            .getHiddenColumnsCopy()
-            .isEmpty());
+            .getHiddenColumnsCopy().isEmpty());
 
     /*
      * hiding a feature in all columns does nothing
@@ -100,15 +105,31 @@ public class AlignFrameTest
     assertFalse(alignFrame.hideFeatureColumns("Metal", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
     List<int[]> hidden = alignFrame.getViewport().getAlignment()
-            .getHiddenColumns()
-            .getHiddenColumnsCopy();
+            .getHiddenColumns().getHiddenColumnsCopy();
     assertTrue(hidden.isEmpty());
 
     /*
+     * threshold Metal to hide features where score < 5
+     * seq1 feature in columns 1-5 is hidden
+     * seq2 feature in columns 6-10 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    alignFrame.getFeatureRenderer().setColour("Metal", fc);
+    assertTrue(alignFrame.hideFeatureColumns("Metal", true));
+    hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getHiddenColumnsCopy();
+    assertEquals(hidden.size(), 1);
+    assertEquals(hidden.get(0)[0], 5);
+    assertEquals(hidden.get(0)[1], 9);
+
+    /*
      * hide a feature present in some columns
      * sequence positions [2-4], [7-9] are column positions
      * [1-3], [6-8] base zero
      */
+    alignFrame.getViewport().showAllHiddenColumns();
     assertTrue(alignFrame.hideFeatureColumns("Turn", true));
     hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getHiddenColumnsCopy();
diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java
new file mode 100644 (file)
index 0000000..270aa2a
--- /dev/null
@@ -0,0 +1,31 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class ConditionTest
+{
+  @Test
+  public void testToString()
+  {
+    Locale.setDefault(Locale.UK);
+    assertEquals(Condition.Contains.toString(), "Contains");
+    assertEquals(Condition.NotContains.toString(), "Does not contain");
+    assertEquals(Condition.Matches.toString(), "Matches");
+    assertEquals(Condition.NotMatches.toString(), "Does not match");
+    assertEquals(Condition.LT.toString(), "<");
+    assertEquals(Condition.LE.toString(), "<=");
+    assertEquals(Condition.GT.toString(), ">");
+    assertEquals(Condition.GE.toString(), ">=");
+    assertEquals(Condition.EQ.toString(), "=");
+    assertEquals(Condition.NE.toString(), "not =");
+
+    /*
+     * repeat call to get coverage of value caching
+     */
+    assertEquals(Condition.NE.toString(), "not =");
+  }
+}
diff --git a/test/jalview/util/matcher/KeyedMatcherSetTest.java b/test/jalview/util/matcher/KeyedMatcherSetTest.java
new file mode 100644 (file)
index 0000000..0d2767d
--- /dev/null
@@ -0,0 +1,124 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.function.Function;
+
+import org.testng.annotations.Test;
+
+public class KeyedMatcherSetTest
+{
+  @Test
+  public void testMatches()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    kms.and(km);
+    assertTrue(kms.matches(key -> "-2"));
+    assertTrue(kms.matches(key -> "-1"));
+    assertFalse(kms.matches(key -> "-3"));
+    assertFalse(kms.matches(key -> ""));
+    assertFalse(kms.matches(key -> "junk"));
+    assertFalse(kms.matches(key -> null));
+
+    /*
+     * a string pattern matcher
+     */
+    km = new KeyedMatcher("AF", Condition.Contains, "Cat");
+    kms = new KeyedMatcherSet();
+    kms.and(km);
+    assertTrue(kms
+            .matches(key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers"));
+  }
+
+  @Test
+  public void testAnd()
+  {
+    // condition1: AF value contains "dog" (matches)
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains,
+            "how");
+
+    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers";
+    assertTrue(km1.matches(vp));
+    assertFalse(km2.matches(vp));
+
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    assertTrue(kms.matches(vp)); // if no conditions, then 'all' pass
+    kms.and(km1);
+    assertTrue(kms.matches(vp));
+    kms.and(km2);
+    assertFalse(kms.matches(vp));
+  }
+
+  @Test
+  public void testToString()
+  {
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.LT, 1.2f);
+    assertEquals(km1.toString(), "AF < 1.2");
+
+    KeyedMatcher km2 = new KeyedMatcher("CLIN_SIG", Condition.NotContains, "path");
+    assertEquals(km2.toString(), "CLIN_SIG Does not contain PATH");
+
+    /*
+     * AND them
+     */
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    assertEquals(kms.toString(), "");
+    kms.and(km1);
+    assertEquals(kms.toString(), "(AF < 1.2)");
+    kms.and(km2);
+    assertEquals(kms.toString(),
+            "(AF < 1.2) AND (CLIN_SIG Does not contain PATH)");
+
+    /*
+     * OR them
+     */
+    kms = new KeyedMatcherSet();
+    assertEquals(kms.toString(), "");
+    kms.or(km1);
+    assertEquals(kms.toString(), "(AF < 1.2)");
+    kms.or(km2);
+    assertEquals(kms.toString(),
+            "(AF < 1.2) OR (CLIN_SIG Does not contain PATH)");
+  }
+
+  @Test
+  public void testOr()
+  {
+    // condition1: AF value contains "dog" (matches)
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains,
+            "how");
+
+    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers";
+    assertTrue(km1.matches(vp));
+    assertFalse(km2.matches(vp));
+
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    kms.or(km2);
+    assertFalse(kms.matches(vp));
+    kms.or(km1);
+    assertTrue(kms.matches(vp));
+  }
+
+  @Test
+  public void testIsEmpty()
+  {
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    assertTrue(kms.isEmpty());
+    kms.and(km);
+    assertFalse(kms.isEmpty());
+  }
+}
diff --git a/test/jalview/util/matcher/KeyedMatcherTest.java b/test/jalview/util/matcher/KeyedMatcherTest.java
new file mode 100644 (file)
index 0000000..164b8eb
--- /dev/null
@@ -0,0 +1,58 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class KeyedMatcherTest
+{
+  @Test
+  public void testMatches()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertTrue(km.matches(key -> "-2"));
+    assertTrue(km.matches(key -> "-1"));
+    assertFalse(km.matches(key -> "-3"));
+    assertFalse(km.matches(key -> ""));
+    assertFalse(km.matches(key -> "junk"));
+    assertFalse(km.matches(key -> null));
+
+    /*
+     * a string pattern matcher
+     */
+    km = new KeyedMatcher("AF", Condition.Contains, "Cat");
+    assertTrue(km.matches(key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers"));
+  }
+
+  @Test
+  public void testToString()
+  {
+    /*
+     * toString uses the i18n translation of the enum conditions
+     */
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.LT, 1.2f);
+    assertEquals(km.toString(), "AF < 1.2");
+  }
+
+  @Test
+  public void testGetKey()
+  {
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertEquals(km.getKey(), "AF");
+  }
+
+  @Test
+  public void testGetMatcher()
+  {
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertEquals(km.getMatcher().getCondition(), Condition.GE);
+    assertEquals(km.getMatcher().getFloatValue(), -2F);
+    assertEquals(km.getMatcher().getPattern(), "-2.0");
+  }
+}
diff --git a/test/jalview/util/matcher/MatcherTest.java b/test/jalview/util/matcher/MatcherTest.java
new file mode 100644 (file)
index 0000000..d988c3a
--- /dev/null
@@ -0,0 +1,250 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import org.testng.annotations.Test;
+
+public class MatcherTest
+{
+  @Test
+  public void testConstructor()
+  {
+    MatcherI m = new Matcher(Condition.Contains, "foo");
+    assertEquals(m.getCondition(), Condition.Contains);
+    assertEquals(m.getPattern(), "FOO"); // all comparisons upper-cased
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GT, -2.1f);
+    assertEquals(m.getCondition(), Condition.GT);
+    assertEquals(m.getPattern(), "-2.1");
+    assertEquals(m.getFloatValue(), -2.1f);
+
+    m = new Matcher(Condition.NotContains, "-1.2f");
+    assertEquals(m.getCondition(), Condition.NotContains);
+    assertEquals(m.getPattern(), "-1.2F");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GE, "-1.2f");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "-1.2");
+    assertEquals(m.getFloatValue(), -1.2f);
+
+    try
+    {
+      new Matcher(null, 0f);
+      fail("Expected exception");
+    } catch (NullPointerException e)
+    {
+      // expected
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123,456");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Tests for float comparison conditions
+   */
+  @Test
+  public void testMatches_float()
+  {
+    /*
+     * EQUALS test
+     */
+    MatcherI m = new Matcher(Condition.EQ, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.0"));
+    assertFalse(m.matches("2.01"));
+
+    /*
+     * NOT EQUALS test
+     */
+    m = new Matcher(Condition.NE, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.0"));
+    assertTrue(m.matches("2.01"));
+
+    /*
+     * >= test
+     */
+    m = new Matcher(Condition.GE, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * > test
+     */
+    m = new Matcher(Condition.GT, 2f);
+    assertFalse(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * <= test
+     */
+    m = new Matcher(Condition.LE, 2f);
+    assertTrue(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+
+    /*
+     * < test
+     */
+    m = new Matcher(Condition.LT, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+  }
+
+  @Test
+  public void testMatches_floatNullOrInvalid()
+  {
+    for (Condition cond : Condition.values())
+    {
+      if (cond.isNumeric())
+      {
+        MatcherI m = new Matcher(cond, 2f);
+        assertFalse(m.matches(null));
+        assertFalse(m.matches(""));
+        assertFalse(m.matches("two"));
+      }
+    }
+  }
+
+  /**
+   * Tests for string comparison conditions
+   */
+  @Test
+  public void testMatches_pattern()
+  {
+    /*
+     * Contains
+     */
+    MatcherI m = new Matcher(Condition.Contains, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not contain
+     */
+    m = new Matcher(Condition.NotContains, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null)); // null value passes this condition
+
+    /*
+     * matches
+     */
+    m = new Matcher(Condition.Matches, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches(" Benign ")); // trim before testing
+    assertFalse(m.matches("MOSTLY BENIGN"));
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not match
+     */
+    m = new Matcher(Condition.NotMatches, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches(" Benign ")); // trim before testing
+    assertTrue(m.matches("MOSTLY BENIGN"));
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null));
+
+    /*
+     * a float with a string match condition will be treated as string
+     */
+    Matcher m1 = new Matcher(Condition.Contains, "32");
+    assertFalse(m1.matches(-203f));
+    assertTrue(m1.matches(-4321.0f));
+  }
+
+  /**
+   * If a float is passed with a string condition it gets converted to a string
+   */
+  @Test
+  public void testMatches_floatWithStringCondition()
+  {
+    MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+    assertTrue(m.matches("1.2e-6"));
+
+    m = new Matcher(Condition.Contains, 0.0000001f);
+    assertTrue(m.matches("1.0e-7"));
+    assertTrue(m.matches("1.0E-7"));
+    assertFalse(m.matches("0.0000001f"));
+  }
+
+  @Test
+  public void testToString()
+  {
+    MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
+    assertEquals(m.toString(), "LT 1.2E-6");
+
+    m = new Matcher(Condition.NotMatches, "ABC");
+    assertEquals(m.toString(), "NotMatches ABC");
+
+    m = new Matcher(Condition.Contains, -1.2f);
+    assertEquals(m.toString(), "Contains -1.2");
+  }
+
+  @Test
+  public void testEquals()
+  {
+    /*
+     * string condition
+     */
+    MatcherI m = new Matcher(Condition.NotMatches, "ABC");
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC")));
+    // not case-sensitive:
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc")));
+    assertFalse(m.equals(new Matcher(Condition.Matches, "ABC")));
+    assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
+
+    /*
+     * numeric conditions
+     */
+    m = new Matcher(Condition.LT, -1f);
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.LT, -1f)));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1f")));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f")));
+    assertFalse(m.equals(new Matcher(Condition.LE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.GE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+  }
+
+  @Test
+  public void testHashCode()
+  {
+    MatcherI m1 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m2 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m3 = new Matcher(Condition.NotMatches, "AB");
+    MatcherI m4 = new Matcher(Condition.Matches, "ABC");
+    assertEquals(m1.hashCode(), m2.hashCode());
+    assertNotEquals(m1.hashCode(), m3.hashCode());
+    assertNotEquals(m1.hashCode(), m4.hashCode());
+    assertNotEquals(m3.hashCode(), m4.hashCode());
+  }
+}