JAL-2808 new Filters tab in Feature Settings
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 2 Nov 2017 14:52:05 +0000 (14:52 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 2 Nov 2017 14:52:05 +0000 (14:52 +0000)
resources/lang/Messages.properties
src/jalview/datamodel/features/FeatureAttributes.java
src/jalview/gui/FeatureSettings.java

index c950bbc..851585a 100644 (file)
@@ -1334,3 +1334,9 @@ 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 7990f6b..d4e9fb0 100644 (file)
@@ -1,7 +1,9 @@
 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;
@@ -37,14 +39,34 @@ public class FeatureAttributes
    * @param featureType
    * @return
    */
-  public Iterable<String> getAttributes(String featureType)
+  public List<String> getAttributes(String featureType)
   {
     if (!attributes.containsKey(featureType))
     {
-      return Collections.emptySet();
+      return Collections.<String> emptyList();
     }
 
-    return attributes.get(featureType);
+    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;
   }
 
   /**
index 3f1d9c7..d90bf4e 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,450 @@ 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
+     */
+    chooseFiltersPanel = new JPanel(new GridLayout(0, 1));
+    chooseFiltersPanel.setBackground(Color.white);
+    chooseFiltersPanel.setBorder(BorderFactory
+            .createTitledBorder(MessageManager.getString("label.filters")));
+
+    /*
+     * 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(chooseFiltersPanel, 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();
+
+    /*
+     * 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);
+    chooseFiltersPanel.add(andOrPanel);
+
+    /*
+     * 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);
+    andFilters.setSelected(true);
+    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 +1912,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
   // ///////////////////////////////////////////////////////////////////////