From 1aa039ed9dfe0528cb7b40231734cf3004452c39 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Thu, 23 Nov 2017 16:20:03 +0000 Subject: [PATCH] JAL-2808 refactor KeyedMatcher as FeatureMatcher with byLabel, byScore, byAttribute factory methods --- src/jalview/api/FeatureRenderer.java | 10 +- src/jalview/datamodel/features/FeatureMatcher.java | 139 ++++++++++ .../datamodel/features/FeatureMatcherI.java | 36 +++ .../features/FeatureMatcherSet.java} | 29 +- .../features/FeatureMatcherSetI.java} | 23 +- src/jalview/gui/FeatureSettings.java | 34 +-- src/jalview/gui/FeatureTypeSettings.java | 173 ++++++++---- src/jalview/util/matcher/Condition.java | 23 +- src/jalview/util/matcher/KeyedMatcher.java | 98 ------- src/jalview/util/matcher/KeyedMatcherI.java | 36 --- .../seqfeatures/FeatureRendererModel.java | 26 +- .../seqfeatures/FeatureRendererSettings.java | 4 +- .../datamodel/features/FeatureMatcherSetTest.java | 284 ++++++++++++++++++++ .../datamodel/features/FeatureMatcherTest.java | 169 ++++++++++++ .../renderer/seqfeatures/FeatureRendererTest.java | 34 +-- test/jalview/util/matcher/KeyedMatcherSetTest.java | 193 ------------- test/jalview/util/matcher/KeyedMatcherTest.java | 73 ----- 17 files changed, 847 insertions(+), 537 deletions(-) create mode 100644 src/jalview/datamodel/features/FeatureMatcher.java create mode 100644 src/jalview/datamodel/features/FeatureMatcherI.java rename src/jalview/{util/matcher/KeyedMatcherSet.java => datamodel/features/FeatureMatcherSet.java} (72%) rename src/jalview/{util/matcher/KeyedMatcherSetI.java => datamodel/features/FeatureMatcherSetI.java} (61%) delete mode 100644 src/jalview/util/matcher/KeyedMatcher.java delete mode 100644 src/jalview/util/matcher/KeyedMatcherI.java create mode 100644 test/jalview/datamodel/features/FeatureMatcherSetTest.java create mode 100644 test/jalview/datamodel/features/FeatureMatcherTest.java delete mode 100644 test/jalview/util/matcher/KeyedMatcherSetTest.java delete mode 100644 test/jalview/util/matcher/KeyedMatcherTest.java diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index ef0abbd..ead84fa 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -22,7 +22,7 @@ package jalview.api; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.util.matcher.KeyedMatcherSetI; +import jalview.datamodel.features.FeatureMatcherSetI; import java.awt.Color; import java.awt.Graphics; @@ -223,14 +223,14 @@ public interface FeatureRenderer * @param featureType * @return */ - KeyedMatcherSetI getFeatureFilter(String featureType); + FeatureMatcherSetI getFeatureFilter(String featureType); /** * Answers a shallow copy of the feature filters map * * @return */ - public Map getFeatureFilters(); + public Map getFeatureFilters(); /** * Sets the filters for the feature type, or removes them if a null or empty @@ -239,14 +239,14 @@ public interface FeatureRenderer * @param featureType * @param filter */ - void setFeatureFilter(String featureType, KeyedMatcherSetI filter); + void setFeatureFilter(String featureType, FeatureMatcherSetI filter); /** * Replaces all feature filters with the given map * * @param filters */ - void setFeatureFilters(Map filters); + void setFeatureFilters(Map filters); /** * Returns the colour for a particular feature instance. This includes diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java new file mode 100644 index 0000000..1fc0e0f --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcher.java @@ -0,0 +1,139 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.Condition; +import jalview.util.matcher.Matcher; +import jalview.util.matcher.MatcherI; + +/** + * An immutable class that models one or more match conditions, each of which is + * applied to the value obtained by lookup given the match key. + *

+ * For example, the value provider could be a SequenceFeature's attributes map, + * and the conditions might be + *

    + *
  • CSQ contains "pathological"
  • + *
  • AND
  • + *
  • AF <= 1.0e-5
  • + *
+ * + * @author gmcarstairs + * + */ +public class FeatureMatcher implements FeatureMatcherI +{ + private static final String COLON = ":"; + + /* + * if true, match is against feature description + */ + final private boolean byLabel; + + /* + * if true, match is against feature score + */ + final private boolean byScore; + + /* + * if not null, match is against feature attribute [sub-attribute] + */ + final private String[] key; + + final private MatcherI matcher; + + /** + * A factory constructor method for a matcher that applies its match condition + * to the feature label (description) + * + * @param cond + * @param pattern + * @return + */ + public static FeatureMatcher byLabel(Condition cond, String pattern) + { + return new FeatureMatcher(new Matcher(cond, pattern), true, false, + null); + } + + /** + * A factory constructor method for a matcher that applies its match condition + * to the feature score + * + * @param cond + * @param pattern + * @return + */ + public static FeatureMatcher byScore(Condition cond, String pattern) + { + return new FeatureMatcher(new Matcher(cond, pattern), false, true, + null); + } + + /** + * A factory constructor method for a matcher that applies its match condition + * to the named feature attribute [and optional sub-attribute] + * + * @param cond + * @param pattern + * @param attName + * @return + */ + public static FeatureMatcher byAttribute(Condition cond, String pattern, + String... attName) + { + return new FeatureMatcher(new Matcher(cond, pattern), false, false, + attName); + } + + private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore, + String[] theKey) + { + key = theKey; + matcher = m; + byLabel = forLabel; + byScore = forScore; + } + @Override + public boolean matches(SequenceFeature feature) + { + String value = byLabel ? feature.getDescription() + : (byScore ? String.valueOf(feature.getScore()) + : feature.getValueAsString(key)); + return matcher.matches(value); + } + + @Override + public String[] getKey() + { + return key; + } + + @Override + public MatcherI getMatcher() + { + return matcher; + } + + /** + * Answers a string description of this matcher, suitable for display, debugging + * or logging. The format may change in future. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(String.join(COLON, key)).append(" ") + .append(matcher.getCondition().toString()); + Condition condition = matcher.getCondition(); + if (condition.isNumeric()) + { + sb.append(" ").append(matcher.getPattern()); + } + else if (condition.needsAPattern()) + { + sb.append(" '").append(matcher.getPattern()).append("'"); + } + + return sb.toString(); + } +} diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java new file mode 100644 index 0000000..078f4a4 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcherI.java @@ -0,0 +1,36 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.MatcherI; + +/** + * An interface for an object that can apply a match condition to a + * SequenceFeature object + * + * @author gmcarstairs + */ +public interface FeatureMatcherI +{ + /** + * Answers true if the value provided for this matcher's key passes this + * matcher's match condition + * + * @param feature + * @return + */ + boolean matches(SequenceFeature feature); + + /** + * Answers the value key this matcher operates on + * + * @return + */ + String[] getKey(); + + /** + * Answers the match condition that is applied + * + * @return + */ + MatcherI getMatcher(); +} diff --git a/src/jalview/util/matcher/KeyedMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java similarity index 72% rename from src/jalview/util/matcher/KeyedMatcherSet.java rename to src/jalview/datamodel/features/FeatureMatcherSet.java index a4be48a..64ae61b 100644 --- a/src/jalview/util/matcher/KeyedMatcherSet.java +++ b/src/jalview/datamodel/features/FeatureMatcherSet.java @@ -1,25 +1,26 @@ -package jalview.util.matcher; +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; -public class KeyedMatcherSet implements KeyedMatcherSetI +public class FeatureMatcherSet implements FeatureMatcherSetI { - List matchConditions; + List matchConditions; boolean andConditions; /** * Constructor */ - public KeyedMatcherSet() + public FeatureMatcherSet() { matchConditions = new ArrayList<>(); } @Override - public boolean matches(Function valueProvider) + public boolean matches(SequenceFeature feature) { /* * no conditions matches anything @@ -34,9 +35,9 @@ public class KeyedMatcherSet implements KeyedMatcherSetI */ if (andConditions) { - for (KeyedMatcherI m : matchConditions) + for (FeatureMatcherI m : matchConditions) { - if (!m.matches(valueProvider)) + if (!m.matches(feature)) { return false; } @@ -47,9 +48,9 @@ public class KeyedMatcherSet implements KeyedMatcherSetI /* * OR until match */ - for (KeyedMatcherI m : matchConditions) + for (FeatureMatcherI m : matchConditions) { - if (m.matches(valueProvider)) + if (m.matches(feature)) { return true; } @@ -58,7 +59,7 @@ public class KeyedMatcherSet implements KeyedMatcherSetI } @Override - public KeyedMatcherSetI and(KeyedMatcherI m) + public FeatureMatcherSetI and(FeatureMatcherI m) { if (!andConditions && matchConditions.size() > 1) { @@ -71,7 +72,7 @@ public class KeyedMatcherSet implements KeyedMatcherSetI } @Override - public KeyedMatcherSetI or(KeyedMatcherI m) + public FeatureMatcherSetI or(FeatureMatcherI m) { if (andConditions && matchConditions.size() > 1) { @@ -90,7 +91,7 @@ public class KeyedMatcherSet implements KeyedMatcherSetI } @Override - public Iterable getMatchers() + public Iterable getMatchers() { return matchConditions; } @@ -100,7 +101,7 @@ public class KeyedMatcherSet implements KeyedMatcherSetI { StringBuilder sb = new StringBuilder(); boolean first = true; - for (KeyedMatcherI matcher : matchConditions) + for (FeatureMatcherI matcher : matchConditions) { if (!first) { diff --git a/src/jalview/util/matcher/KeyedMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java similarity index 61% rename from src/jalview/util/matcher/KeyedMatcherSetI.java rename to src/jalview/datamodel/features/FeatureMatcherSetI.java index 3e9f5b6..f064770 100644 --- a/src/jalview/util/matcher/KeyedMatcherSetI.java +++ b/src/jalview/datamodel/features/FeatureMatcherSetI.java @@ -1,24 +1,23 @@ -package jalview.util.matcher; +package jalview.datamodel.features; -import java.util.function.Function; +import jalview.datamodel.SequenceFeature; /** - * An interface to describe a set of one or more key-value match conditions, - * where all conditions are combined with either AND or OR + * An interface to describe a set of one or more feature matchers, where all + * matchers are combined with either AND or OR * * @author gmcarstairs * */ -public interface KeyedMatcherSetI +public interface FeatureMatcherSetI { /** - * Answers true if the value provided for this matcher's key passes this - * matcher's match condition + * Answers true if the feature provided passes this matcher's match condition * - * @param valueProvider + * @param feature * @return */ - boolean matches(Function valueProvider); + boolean matches(SequenceFeature feature); /** * Answers a new object that matches the logical AND of this and m @@ -28,7 +27,7 @@ public interface KeyedMatcherSetI * @throws IllegalStateException * if an attempt is made to AND to existing OR-ed conditions */ - KeyedMatcherSetI and(KeyedMatcherI m); + FeatureMatcherSetI and(FeatureMatcherI m); /** * Answers true if any second condition is AND-ed with this one, false if it @@ -46,14 +45,14 @@ public interface KeyedMatcherSetI * @throws IllegalStateException * if an attempt is made to OR to existing AND-ed conditions */ - KeyedMatcherSetI or(KeyedMatcherI m); + FeatureMatcherSetI or(FeatureMatcherI m); /** * Answers an iterator over the combined match conditions * * @return */ - Iterable getMatchers(); + Iterable getMatchers(); /** * Answers true if this object contains no conditions diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 2b3688f..0c4cd56 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -30,6 +30,8 @@ import jalview.api.FeatureSettingsControllerI; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.Help.HelpId; import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; @@ -39,8 +41,6 @@ import jalview.util.Format; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.util.QuickSort; -import jalview.util.matcher.KeyedMatcherSet; -import jalview.util.matcher.KeyedMatcherSetI; import jalview.viewmodel.AlignmentViewport; import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import jalview.ws.DasSequenceFeatureFetcher; @@ -133,7 +133,7 @@ public class FeatureSettings extends JPanel private float originalTransparency; - private Map originalFilters; + private Map originalFilters; final JInternalFrame frame; @@ -210,7 +210,7 @@ public class FeatureSettings extends JPanel break; case FILTER_COLUMN: int row = table.rowAtPoint(e.getPoint()); - KeyedMatcherSet o = (KeyedMatcherSet) table.getValueAt(row, + FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row, column); tip = o.isEmpty() ? MessageManager.getString("label.filters_tooltip") @@ -231,8 +231,8 @@ public class FeatureSettings extends JPanel table.setDefaultEditor(FeatureColour.class, new ColorEditor(this)); table.setDefaultRenderer(FeatureColour.class, new ColorRenderer()); - table.setDefaultEditor(KeyedMatcherSet.class, new FilterEditor(this)); - table.setDefaultRenderer(KeyedMatcherSet.class, new FilterRenderer()); + table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this)); + table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer()); TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75, new ColorRenderer(), new ColorEditor(this)); @@ -674,9 +674,9 @@ public class FeatureSettings extends JPanel data[dataIndex][TYPE_COLUMN] = type; data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type); - KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type); + FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type); data[dataIndex][FILTER_COLUMN] = featureFilter == null - ? new KeyedMatcherSet() + ? new FeatureMatcherSet() : featureFilter; data[dataIndex][SHOW_COLUMN] = new Boolean( af.getViewport().getFeaturesDisplayed().isVisible(type)); @@ -701,9 +701,9 @@ public class FeatureSettings extends JPanel fr.clearRenderOrder(); return; } - KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type); + FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type); data[dataIndex][FILTER_COLUMN] = featureFilter == null - ? new KeyedMatcherSet() + ? new FeatureMatcherSet() : featureFilter; data[dataIndex][SHOW_COLUMN] = new Boolean(true); dataIndex++; @@ -1120,7 +1120,7 @@ public class FeatureSettings extends JPanel { String type = (String) data[i][TYPE_COLUMN]; FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN]; - KeyedMatcherSetI theFilter = (KeyedMatcherSetI) data[i][FILTER_COLUMN]; + FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN]; Boolean isShown = (Boolean) data[i][SHOW_COLUMN]; rowData[i] = new FeatureSettingsBean(type, colour, theFilter, isShown); @@ -1674,7 +1674,7 @@ public class FeatureSettings extends JPanel Object filter, boolean isSelected, boolean hasFocus, int row, int column) { - KeyedMatcherSetI theFilter = (KeyedMatcherSetI) filter; + FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter; setOpaque(true); String asText = theFilter.toString(); setBackground(tbl.getBackground()); @@ -1886,10 +1886,10 @@ public class FeatureSettings extends JPanel * update table data without triggering updateFeatureRenderer */ currentColor = fr.getFeatureColours().get(type); - KeyedMatcherSetI currentFilter = me.fr.getFeatureFilter(type); + FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type); if (currentFilter == null) { - currentFilter = new KeyedMatcherSet(); + currentFilter = new FeatureMatcherSet(); } Object[] data = ((FeatureTableModel) table.getModel()) .getData()[rowSelected]; @@ -1948,7 +1948,7 @@ public class FeatureSettings extends JPanel { FeatureSettings me; - KeyedMatcherSetI currentFilter; + FeatureMatcherSetI currentFilter; Point lastLocation; @@ -2003,7 +2003,7 @@ public class FeatureSettings extends JPanel currentFilter = me.fr.getFeatureFilter(type); if (currentFilter == null) { - currentFilter = new KeyedMatcherSet(); + currentFilter = new FeatureMatcherSet(); } Object[] data = ((FeatureTableModel) table.getModel()) .getData()[rowSelected]; @@ -2024,7 +2024,7 @@ public class FeatureSettings extends JPanel public Component getTableCellEditorComponent(JTable theTable, Object value, boolean isSelected, int row, int column) { - currentFilter = (KeyedMatcherSetI) value; + currentFilter = (FeatureMatcherSetI) value; this.rowSelected = row; type = me.table.getValueAt(row, TYPE_COLUMN).toString(); button.setOpaque(true); diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java index 1dd12aa..835f1fc 100644 --- a/src/jalview/gui/FeatureTypeSettings.java +++ b/src/jalview/gui/FeatureTypeSettings.java @@ -25,14 +25,14 @@ import jalview.api.FeatureColourI; import jalview.datamodel.GraphLine; import jalview.datamodel.features.FeatureAttributes; import jalview.datamodel.features.FeatureAttributes.Datatype; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; import jalview.util.MessageManager; import jalview.util.matcher.Condition; -import jalview.util.matcher.KeyedMatcher; -import jalview.util.matcher.KeyedMatcherI; -import jalview.util.matcher.KeyedMatcherSet; -import jalview.util.matcher.KeyedMatcherSetI; import java.awt.BorderLayout; import java.awt.Color; @@ -115,7 +115,7 @@ public class FeatureTypeSettings extends JalviewDialog */ private final FeatureColourI originalColour; - private final KeyedMatcherSetI originalFilter; + private final FeatureMatcherSetI originalFilter; /* * set flag to true when setting values programmatically, @@ -123,10 +123,20 @@ public class FeatureTypeSettings extends JalviewDialog */ private boolean adjusting = false; + /* + * minimum of the value range for graduated colour + * (may be for feature score or for a numeric attribute) + */ private float min; + /* + * maximum of the value range for graduated colour + */ private float max; + /* + * scale factor for conversion between absolute min-max and slider + */ private float scaleFactor; /* @@ -181,7 +191,7 @@ public class FeatureTypeSettings extends JalviewDialog /* * filters for the currently selected feature type */ - private List filters; + private List filters; // set white normally, black to debug layout private Color debugBorderColour = Color.white; @@ -426,32 +436,48 @@ public class FeatureTypeSettings extends JalviewDialog * Updates the min-max range if Colour By selected item is Score, or an * attribute, with a min-max range */ - protected void updateMinMax() + protected void updateColourMinMax() { if (!graduatedColour.isSelected()) { return; } - float[] minMax = null; String colourBy = (String) colourByRangeCombo.getSelectedItem(); - if (MessageManager.getString("label.score").equals(colourBy)) + String[] attNames = fromAttributeDisplayName(colourBy); + float[] minMax = getMinMax(attNames); + + if (minMax != null) + { + min = minMax[0]; + max = minMax[1]; + } + } + + /** + * Retrieves the min-max range: + *
    + *
  • of feature score, if colour or filter is by Score
  • + *
  • else of the selected attribute
  • + *
+ * + * @param attNames + * @return + */ + private float[] getMinMax(String[] attNames) + { + float[] minMax = null; + if (MessageManager.getString("label.score").equals(attNames[0])) { minMax = fr.getMinMax().get(featureType)[0]; } else { // colour by attribute range - String[] attNames = fromAttributeDisplayName(colourBy); minMax = FeatureAttributes.getInstance().getMinMax(featureType, attNames); } - - if (minMax != null) - { - min = minMax[0]; - max = minMax[1]; - } + return minMax; } /** @@ -839,7 +865,7 @@ public class FeatureTypeSettings extends JalviewDialog * ensure min-max range is for the latest choice of * 'graduated colour by' */ - updateMinMax(); + updateColourMinMax(); FeatureColourI acg = makeColourFromInputs(); @@ -1228,7 +1254,7 @@ public class FeatureTypeSettings extends JalviewDialog /* * if this feature type has filters set, load them first */ - KeyedMatcherSetI featureFilters = fr.getFeatureFilter(featureType); + FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType); if (featureFilters != null) { if (!featureFilters.isAnded()) @@ -1241,15 +1267,15 @@ public class FeatureTypeSettings extends JalviewDialog /* * and an empty filter for the user to populate (add) */ - KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "", - (String[]) null); + FeatureMatcherI noFilter = FeatureMatcher.byLabel(Condition.values()[0], + ""); filters.add(noFilter); /* * render the conditions in rows, each in its own JPanel */ int filterIndex = 0; - for (KeyedMatcherI filter : filters) + for (FeatureMatcherI filter : filters) { String[] attName = filter.getKey(); Condition condition = filter.getMatcher().getCondition(); @@ -1307,11 +1333,10 @@ public class FeatureTypeSettings extends JalviewDialog @Override public void actionPerformed(ActionEvent e) { - if (attCombo.getSelectedItem() != null) + if (validateFilter(patternField, condCombo)) { - if (validateFilter(patternField, condCombo)) + if (updateFilter(attCombo, condCombo, patternField, filterIndex)) { - updateFilter(attCombo, condCombo, patternField, filterIndex); filtersChanged(); } } @@ -1379,8 +1404,7 @@ public class FeatureTypeSettings extends JalviewDialog * disable pattern field for condition 'Present / NotPresent' */ Condition selectedCondition = (Condition) condCombo.getSelectedItem(); - if (selectedCondition == Condition.Present - || selectedCondition == Condition.NotPresent) + if (!selectedCondition.needsAPattern()) { patternField.setEnabled(false); } @@ -1389,22 +1413,13 @@ public class FeatureTypeSettings extends JalviewDialog * if a numeric condition is selected, show the value range * as a tooltip on the value input field */ - if (selectedCondition.isNumeric()) - { - float[] minMax = FeatureAttributes.getInstance() - .getMinMax(featureType, attName); - if (minMax != null) - { - String tip = String.format("(%s - %s)", - DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1])); - patternField.setToolTipText(tip); - } - } + updatePatternTooltip(attName, selectedCondition, patternField); /* * add remove button if filter is populated (non-empty pattern) */ - if (pattern != null && pattern.trim().length() > 0) + if (!patternField.isEnabled() + || (pattern != null && pattern.trim().length() > 0)) { // todo: gif for button drawing '-' or 'x' JButton removeCondition = new BasicArrowButton(SwingConstants.WEST); @@ -1426,6 +1441,31 @@ public class FeatureTypeSettings extends JalviewDialog } /** + * If a numeric comparison condition is selected, retrieve the min-max range for + * the value (score or attribute), and set it as a tooltip on the value file + * + * @param attName + * @param selectedCondition + * @param patternField + */ + private void updatePatternTooltip(String[] attName, + Condition selectedCondition, JTextField patternField) + { + patternField.setToolTipText(""); + + if (selectedCondition.isNumeric()) + { + float[] minMax = getMinMax(attName); + if (minMax != null) + { + String tip = String.format("(%s - %s)", + DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1])); + patternField.setToolTipText(tip); + } + } + } + + /** * Populates the drop-down list of comparison conditions for the given attribute * name. The conditions added depend on the datatype of the attribute values. * The supplied condition is set as the selected item in the list, provided it @@ -1504,7 +1544,7 @@ public class FeatureTypeSettings extends JalviewDialog } Condition cond = (Condition) condCombo.getSelectedItem(); - if (cond == Condition.Present || cond == Condition.NotPresent) + if (cond.needsAPattern()) { return true; } @@ -1514,10 +1554,10 @@ public class FeatureTypeSettings extends JalviewDialog String v1 = value.getText().trim(); if (v1.length() == 0) { - return false; + // return false; } - if (cond.isNumeric()) + if (cond.isNumeric() && v1.length() > 0) { try { @@ -1536,24 +1576,58 @@ public class FeatureTypeSettings extends JalviewDialog /** * Constructs a filter condition from the given input fields, and replaces the - * condition at filterIndex with the new one + * condition at filterIndex with the new one. Does nothing if the pattern field + * is blank (unless the match condition is one that doesn't require a pattern, + * e.g. 'Is present'). Answers true if the filter was updated, else false. + *

+ * This method may update the tooltip on the filter value field to show the + * value range, if a numeric condition is selected. This ensures the tooltip is + * updated when a numeric valued attribute is chosen on the last 'add a filter' + * row. * * @param attCombo * @param condCombo * @param valueField * @param filterIndex */ - protected void updateFilter(JComboBox attCombo, + protected boolean updateFilter(JComboBox attCombo, JComboBox condCombo, JTextField valueField, int filterIndex) { String attName = (String) attCombo.getSelectedItem(); Condition cond = (Condition) condCombo.getSelectedItem(); - String pattern = valueField.getText(); - KeyedMatcherI km = new KeyedMatcher(cond, pattern, - fromAttributeDisplayName(attName)); + String pattern = valueField.getText().trim(); + + updatePatternTooltip(fromAttributeDisplayName(attName), cond, + valueField); + + if (pattern.length() == 0 && cond.needsAPattern()) + { + return false; + } + + /* + * Construct a matcher that operates on Label, Score, + * or named attribute + */ + FeatureMatcherI km = null; + if (MessageManager.getString("label.label").equals(attName)) + { + km = FeatureMatcher.byLabel(cond, pattern); + } + else if (MessageManager.getString("label.score").equals(attName)) + { + km = FeatureMatcher.byScore(cond, pattern); + } + else + { + km = FeatureMatcher.byAttribute(cond, pattern, + fromAttributeDisplayName(attName)); + } filters.set(filterIndex, km); + + return true; } /** @@ -1584,14 +1658,13 @@ public class FeatureTypeSettings extends JalviewDialog * update the filter conditions for the feature type */ boolean anded = andFilters.isSelected(); - KeyedMatcherSetI combined = new KeyedMatcherSet(); + FeatureMatcherSetI combined = new FeatureMatcherSet(); - for (KeyedMatcherI filter : filters) + for (FeatureMatcherI filter : filters) { String pattern = filter.getMatcher().getPattern(); Condition condition = filter.getMatcher().getCondition(); - if (pattern.trim().length() > 0 || condition == Condition.Present - || condition == Condition.NotPresent) + if (pattern.trim().length() > 0 || !condition.needsAPattern()) { if (anded) { diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java index 4d14063..3401ae8 100644 --- a/src/jalview/util/matcher/Condition.java +++ b/src/jalview/util/matcher/Condition.java @@ -11,17 +11,21 @@ import java.util.Map; */ public enum Condition { - Contains(false), NotContains(false), Matches(false), NotMatches(false), - Present(false), NotPresent(false), - EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true); + Contains(false, true), NotContains(false, true), Matches(false, true), + NotMatches(false, true), Present(false, false), NotPresent(false, false), + EQ(true, true), NE(true, true), LT(true, true), LE(true, true), + GT(true, true), GE(true, true); private static Map displayNames = new HashMap<>(); private boolean numeric; - Condition(boolean isNumeric) + private boolean needsAPattern; + + Condition(boolean isNumeric, boolean needsPattern) { numeric = isNumeric; + needsAPattern = needsPattern; } /** @@ -36,6 +40,17 @@ public enum Condition } /** + * Answers true if the condition requires a pattern to compare against, else + * false + * + * @return + */ + public boolean needsAPattern() + { + return needsAPattern; + } + + /** * Answers a display name for the match condition, suitable for showing in * drop-down menus. The value may be internationalized using the resource key * "label.matchCondition_" with the enum name appended. diff --git a/src/jalview/util/matcher/KeyedMatcher.java b/src/jalview/util/matcher/KeyedMatcher.java deleted file mode 100644 index f21756a..0000000 --- a/src/jalview/util/matcher/KeyedMatcher.java +++ /dev/null @@ -1,98 +0,0 @@ -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. - *

- * For example, the value provider could be a SequenceFeature's attributes map, - * and the conditions might be - *

    - *
  • CSQ contains "pathological"
  • - *
  • AND
  • - *
  • AF <= 1.0e-5
  • - *
- * - * @author gmcarstairs - * - */ -public class KeyedMatcher implements KeyedMatcherI -{ - private static final String COLON = ":"; - - final private String[] key; - - final private MatcherI matcher; - - /** - * Constructor given a key, a test condition and a match pattern - * - * @param cond - * @param pattern - * @param theKey - */ - public KeyedMatcher(Condition cond, String pattern, String... theKey) - { - 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 cond - * @param value - * @param theKey - */ - public KeyedMatcher(Condition cond, float value, String... theKey) - { - key = theKey; - matcher = new Matcher(cond, value); - } - - @Override - public boolean matches(Function 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 display, debugging - * or logging. The format may change in future. - */ - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append(String.join(COLON, key)).append(" ") - .append(matcher.getCondition().toString()); - Condition condition = matcher.getCondition(); - if (condition.isNumeric()) - { - sb.append(" ").append(matcher.getPattern()); - } - else if (condition != Condition.Present - && condition != Condition.NotPresent) - { - sb.append(" '").append(matcher.getPattern()).append("'"); - } - - return sb.toString(); - } -} diff --git a/src/jalview/util/matcher/KeyedMatcherI.java b/src/jalview/util/matcher/KeyedMatcherI.java deleted file mode 100644 index e8d71c1..0000000 --- a/src/jalview/util/matcher/KeyedMatcherI.java +++ /dev/null @@ -1,36 +0,0 @@ -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 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/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 8bdcad4..c58461e 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -26,11 +26,11 @@ import jalview.api.FeaturesDisplayedI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.datamodel.features.SequenceFeatures; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; -import jalview.util.matcher.KeyedMatcherSetI; import java.awt.Color; import java.beans.PropertyChangeListener; @@ -58,12 +58,12 @@ public abstract class FeatureRendererModel public final FeatureColourI featureColour; - public final KeyedMatcherSetI filter; + public final FeatureMatcherSetI filter; public final Boolean show; public FeatureSettingsBean(String type, FeatureColourI colour, - KeyedMatcherSetI theFilter, Boolean isShown) + FeatureMatcherSetI theFilter, Boolean isShown) { featureType = type; featureColour = colour; @@ -90,7 +90,7 @@ public abstract class FeatureRendererModel /* * filters for each feature type */ - protected Map featureFilters = new HashMap<>(); + protected Map featureFilters = new HashMap<>(); protected String[] renderOrder; @@ -1068,25 +1068,25 @@ public abstract class FeatureRendererModel } @Override - public Map getFeatureFilters() + public Map getFeatureFilters() { return new HashMap<>(featureFilters); } @Override - public void setFeatureFilters(Map filters) + public void setFeatureFilters(Map filters) { featureFilters = filters; } @Override - public KeyedMatcherSetI getFeatureFilter(String featureType) + public FeatureMatcherSetI getFeatureFilter(String featureType) { return featureFilters.get(featureType); } @Override - public void setFeatureFilter(String featureType, KeyedMatcherSetI filter) + public void setFeatureFilter(String featureType, FeatureMatcherSetI filter) { if (filter == null || filter.isEmpty()) { @@ -1146,14 +1146,8 @@ public abstract class FeatureRendererModel */ protected boolean featureMatchesFilters(SequenceFeature sf) { - KeyedMatcherSetI filter = featureFilters.get(sf.getType()); - // TODO temporary fudge for Score and Label - return filter == null ? true - : filter.matches( - key -> "Label".equals(key[0]) ? sf.getDescription() - : ("Score".equals(key[0]) - ? String.valueOf(sf.getScore()) - : sf.getValueAsString(key))); + FeatureMatcherSetI filter = featureFilters.get(sf.getType()); + return filter == null ? true : filter.matches(sf); } } diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java index 6afaa54..f594453 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java @@ -21,8 +21,8 @@ package jalview.viewmodel.seqfeatures; import jalview.api.FeatureColourI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.schemes.FeatureColour; -import jalview.util.matcher.KeyedMatcherSetI; import java.util.Arrays; import java.util.HashMap; @@ -47,7 +47,7 @@ public class FeatureRendererSettings implements Cloneable /* * map of {featureType, filters} */ - Map featureFilters; + Map featureFilters; float transparency; diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java new file mode 100644 index 0000000..a98013b --- /dev/null +++ b/test/jalview/datamodel/features/FeatureMatcherSetTest.java @@ -0,0 +1,284 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.Condition; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.testng.annotations.Test; + +public class FeatureMatcherSetTest +{ + @Test(groups = "Functional") + public void testMatches_byAttribute() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", + "AF"); + FeatureMatcherSetI fms = new FeatureMatcherSet(); + fms.and(fm); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "foobar"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "-2"); + assertTrue(fms.matches(sf)); + sf.setValue("AF", "-1"); + assertTrue(fms.matches(sf)); + sf.setValue("AF", "-3"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", ""); + assertFalse(fms.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF"); + fms = new FeatureMatcherSet(); + fms.and(fm); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "raining cats and dogs"); + assertTrue(fms.matches(sf)); + } + + @Test(groups = "Functional") + public void testAnd() + { + // condition1: AF value contains "dog" (matches) + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains, + "dog", "AF"); + // condition 2: CSQ value does not contain "how" (does not match) + FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "how", "CSQ"); + + SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12, + 6.2f, "grp"); + sf.setValue("AF", "raining cats and dogs"); + sf.setValue("CSQ", "showers"); + + assertTrue(fm1.matches(sf)); + assertFalse(fm2.matches(sf)); + + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass + fms.and(fm1); + assertTrue(fms.matches(sf)); + fms.and(fm2); + assertFalse(fms.matches(sf)); + + /* + * OR a failed attribute condition with a matched label condition + */ + fms = new FeatureMatcherSet(); + fms.and(fm2); + assertFalse(fms.matches(sf)); + FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains, + "Helix"); + fms.or(byLabelPass); + assertTrue(fms.matches(sf)); + + /* + * OR a failed attribute condition with a failed score condition + */ + fms = new FeatureMatcherSet(); + fms.and(fm2); + assertFalse(fms.matches(sf)); + FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT, + "5.9"); + fms.or(byScoreFail); + assertFalse(fms.matches(sf)); + + /* + * OR failed attribute and score conditions with matched label condition + */ + fms = new FeatureMatcherSet(); + fms.or(fm2).or(byScoreFail); + assertFalse(fms.matches(sf)); + fms.or(byLabelPass); + assertTrue(fms.matches(sf)); + } + + @Test(groups = "Functional") + public void testToString() + { + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm1.toString(), "AF < 1.2"); + + FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "path", + "CLIN_SIG"); + assertEquals(fm2.toString(), "CLIN_SIG Does not contain 'PATH'"); + + /* + * AND them + */ + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertEquals(fms.toString(), ""); + fms.and(fm1); + assertEquals(fms.toString(), "(AF < 1.2)"); + fms.and(fm2); + assertEquals(fms.toString(), + "(AF < 1.2) AND (CLIN_SIG Does not contain 'PATH')"); + + /* + * OR them + */ + fms = new FeatureMatcherSet(); + assertEquals(fms.toString(), ""); + fms.or(fm1); + assertEquals(fms.toString(), "(AF < 1.2)"); + fms.or(fm2); + assertEquals(fms.toString(), + "(AF < 1.2) OR (CLIN_SIG Does not contain 'PATH')"); + + try + { + fms.and(fm1); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + @Test(groups = "Functional") + public void testOr() + { + // condition1: AF value contains "dog" (matches) + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains, + "dog", "AF"); + // condition 2: CSQ value does not contain "how" (does not match) + FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "how", "CSQ"); + + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + sf.setValue("AF", "raining cats and dogs"); + sf.setValue("CSQ", "showers"); + + assertTrue(fm1.matches(sf)); + assertFalse(fm2.matches(sf)); + + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass + fms.or(fm1); + assertTrue(fms.matches(sf)); + fms.or(fm2); + assertTrue(fms.matches(sf)); // true or false makes true + + fms = new FeatureMatcherSet(); + fms.or(fm2); + assertFalse(fms.matches(sf)); + fms.or(fm1); + assertTrue(fms.matches(sf)); // false or true makes true + + try + { + fms.and(fm2); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + @Test(groups = "Functional") + public void testIsEmpty() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0", + "AF"); + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.isEmpty()); + fms.and(fm); + assertFalse(fms.isEmpty()); + } + + @Test(groups = "Functional") + public void testGetMatchers() + { + FeatureMatcherSetI fms = new FeatureMatcherSet(); + + /* + * empty iterable: + */ + Iterator iterator = fms.getMatchers().iterator(); + assertFalse(iterator.hasNext()); + + /* + * one matcher: + */ + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2", + "AF"); + fms.and(fm1); + iterator = fms.getMatchers().iterator(); + assertSame(fm1, iterator.next()); + assertFalse(iterator.hasNext()); + + /* + * two matchers: + */ + FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f", + "AF"); + fms.and(fm2); + iterator = fms.getMatchers().iterator(); + assertSame(fm1, iterator.next()); + assertSame(fm2, iterator.next()); + assertFalse(iterator.hasNext()); + } + + /** + * Tests for the 'compound attribute' key i.e. where first key's value is a map + * from which we take the value for the second key, e.g. CSQ : Consequence + */ + @Test(groups = "Functional") + public void testMatches_compoundKey() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", + "CSQ", "Consequence"); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp"); + FeatureMatcherSetI fms = new FeatureMatcherSet(); + fms.and(fm); + assertFalse(fms.matches(sf)); + Map csq = new HashMap<>(); + sf.setValue("CSQ", csq); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "-2"); + assertTrue(fms.matches(sf)); + csq.put("Consequence", "-1"); + assertTrue(fms.matches(sf)); + csq.put("Consequence", "-3"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", ""); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "junk"); + assertFalse(fms.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ", + "Consequence"); + fms = new FeatureMatcherSet(); + fms.and(fm); + assertFalse(fms.matches(sf)); + csq.put("PolyPhen", "damaging"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "damaging"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "Catastrophic"); + assertTrue(fms.matches(sf)); + } +} diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java new file mode 100644 index 0000000..f4e9351 --- /dev/null +++ b/test/jalview/datamodel/features/FeatureMatcherTest.java @@ -0,0 +1,169 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.Condition; + +import org.testng.annotations.Test; + +public class FeatureMatcherTest +{ + @Test + public void testMatches_byLabel() + { + SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, + 12, "grp"); + + /* + * contains - not case sensitive + */ + assertTrue( + FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf)); + assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf)); + assertFalse( + FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf)); + + /* + * does not contain + */ + assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt") + .matches(sf)); + assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is") + .matches(sf)); + + /* + * matches + */ + assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label") + .matches(sf)); + assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY") + .matches(sf)); + + /* + * does not match + */ + assertFalse(FeatureMatcher + .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf)); + assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY") + .matches(sf)); + + /* + * is present / not present + */ + assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf)); + assertFalse( + FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf)); + } + + @Test + public void testMatches_byScore() + { + SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, + 12, 3.2f, "grp"); + + assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf)); + + assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf)); + } + @Test + public void testMatches_byAttribute() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + FeatureMatcherI fm = FeatureMatcher + .byAttribute(Condition.GE, "-2", "AF"); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "foobar"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "-2"); + assertTrue(fm.matches(sf)); + sf.setValue("AF", "-1"); + assertTrue(fm.matches(sf)); + sf.setValue("AF", "-3"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", ""); + assertFalse(fm.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "raining cats and dogs"); + assertTrue(fm.matches(sf)); + + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC"); + assertFalse(fm.matches(sf)); + sf.setValue("AC", "21"); + assertTrue(fm.matches(sf)); + + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females"); + assertTrue(fm.matches(sf)); + sf.setValue("AC_Females", "21"); + assertFalse(fm.matches(sf)); + } + + @Test + public void testToString() + { + /* + * toString uses the i18n translation of the enum conditions + */ + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm.toString(), "AF < 1.2"); + + /* + * Present / NotPresent omit the value pattern + */ + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF"); + assertEquals(fm.toString(), "AF Is present"); + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF"); + assertEquals(fm.toString(), "AF Is not present"); + } + + @Test + public void testGetKey() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", + "AF"); + assertEquals(fm.getKey(), new String[] { "AF" }); + + /* + * compound key (attribute / subattribute) + */ + fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ", + "Consequence"); + assertEquals(fm.getKey(), new String[] { "CSQ", "Consequence" }); + } + + @Test + public void testGetMatcher() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f", + "AF"); + assertEquals(fm.getMatcher().getCondition(), Condition.GE); + assertEquals(fm.getMatcher().getFloatValue(), -2F); + assertEquals(fm.getMatcher().getPattern(), "-2.0"); + } +} diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java index 73ae9d7..03398c0 100644 --- a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java +++ b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java @@ -9,14 +9,14 @@ import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.AlignFrame; import jalview.io.DataSourceType; import jalview.io.FileLoader; import jalview.schemes.FeatureColour; import jalview.util.matcher.Condition; -import jalview.util.matcher.KeyedMatcher; -import jalview.util.matcher.KeyedMatcherSet; -import jalview.util.matcher.KeyedMatcherSetI; import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import java.awt.Color; @@ -68,9 +68,8 @@ public class FeatureRendererTest seqs.get(2).addSequenceFeature( new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup")); // bug in findAllFeatures - group not checked for a known feature type - seqs.get(2).addSequenceFeature( - new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, - "RfamGroup")); + seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9, + Float.NaN, "RfamGroup")); // existing feature type with null group seqs.get(3).addSequenceFeature( new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null)); @@ -129,7 +128,8 @@ public class FeatureRendererTest data[1] = new FeatureSettingsBean("Pfam", colour, null, false); data[2] = new FeatureSettingsBean("Scop", colour, null, false); fr.setFeaturePriority(data); - assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam")); + assertEquals(fr.getRenderOrder(), + Arrays.asList("Scop", "Pfam", "Rfam")); assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam")); /* @@ -260,7 +260,7 @@ public class FeatureRendererTest features = fr.findFeaturesAtColumn(seq, 5); assertEquals(features.size(), 1); assertTrue(features.contains(sf8)); - + /* * give "Type3" features a graduated colour scheme * - first with no threshold @@ -412,7 +412,7 @@ public class FeatureRendererTest // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0) Color expected = new Color(255, 128, 0); assertEquals(fr.getColour(sf2), expected); - + /* * above threshold, score is above threshold - no change */ @@ -468,14 +468,14 @@ public class FeatureRendererTest // with filter on AF < 4 gc.setAboveThreshold(false); assertEquals(fr.getColour(sf2), expected); - KeyedMatcherSetI filter = new KeyedMatcherSet(); - filter.and(new KeyedMatcher(Condition.LT, 4f, "AF")); + FeatureMatcherSetI filter = new FeatureMatcherSet(); + filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF")); fr.setFeatureFilter("Cath", filter); assertNull(fr.getColour(sf2)); // with filter on 'Consequence contains missense' - filter = new KeyedMatcherSet(); - filter.and(new KeyedMatcher(Condition.Contains, "missense", + filter = new FeatureMatcherSet(); + filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense", "Consequence")); fr.setFeatureFilter("Cath", filter); // if feature has no Consequence attribute, no colour @@ -487,10 +487,10 @@ public class FeatureRendererTest sf2.setValue("Consequence", "Missense variant"); assertEquals(fr.getColour(sf2), expected); - // with filter on CSQ.Feature contains "ENST01234" - filter = new KeyedMatcherSet(); - filter.and(new KeyedMatcher(Condition.Matches, "ENST01234", "CSQ", - "Feature")); + // with filter on CSQ:Feature contains "ENST01234" + filter = new FeatureMatcherSet(); + filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234", + "CSQ", "Feature")); fr.setFeatureFilter("Cath", filter); // if feature has no CSQ data, no colour assertNull(fr.getColour(sf2)); diff --git a/test/jalview/util/matcher/KeyedMatcherSetTest.java b/test/jalview/util/matcher/KeyedMatcherSetTest.java deleted file mode 100644 index 3d597d2..0000000 --- a/test/jalview/util/matcher/KeyedMatcherSetTest.java +++ /dev/null @@ -1,193 +0,0 @@ -package jalview.util.matcher; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertSame; -import static org.testng.Assert.assertTrue; - -import java.util.Iterator; -import java.util.function.Function; - -import org.testng.annotations.Test; - -public class KeyedMatcherSetTest -{ - @Test(groups = "Functional") - public void testMatches() - { - /* - * a numeric matcher - MatcherTest covers more conditions - */ - KeyedMatcherI km = new KeyedMatcher(Condition.GE, -2F, "AF"); - 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(Condition.Contains, "Cat", "AF"); - kms = new KeyedMatcherSet(); - kms.and(km); - assertTrue(kms - .matches(key -> "AF".equals(key[0]) ? "raining cats and dogs" - : "showers")); - } - - @Test(groups = "Functional") - public void testAnd() - { - // condition1: AF value contains "dog" (matches) - KeyedMatcherI km1 = new KeyedMatcher(Condition.Contains, "dog", "AF"); - // condition 2: CSQ value does not contain "how" (does not match) - KeyedMatcherI km2 = new KeyedMatcher(Condition.NotContains, "how", - "CSQ"); - - Function vp = key -> "AF".equals(key[0]) - ? "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(groups = "Functional") - public void testToString() - { - KeyedMatcherI km1 = new KeyedMatcher(Condition.LT, 1.2f, "AF"); - assertEquals(km1.toString(), "AF < 1.2"); - - KeyedMatcher km2 = new KeyedMatcher(Condition.NotContains, "path", - "CLIN_SIG"); - 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(groups = "Functional") - public void testOr() - { - // condition1: AF value contains "dog" (matches) - KeyedMatcherI km1 = new KeyedMatcher(Condition.Contains, "dog", "AF"); - // condition 2: CSQ value does not contain "how" (does not match) - KeyedMatcherI km2 = new KeyedMatcher(Condition.NotContains, "how", - "CSQ"); - - Function vp = key -> "AF".equals(key[0]) - ? "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(groups = "Functional") - public void testIsEmpty() - { - KeyedMatcherI km = new KeyedMatcher(Condition.GE, -2F, "AF"); - KeyedMatcherSetI kms = new KeyedMatcherSet(); - assertTrue(kms.isEmpty()); - kms.and(km); - assertFalse(kms.isEmpty()); - } - - @Test(groups = "Functional") - public void testGetMatchers() - { - KeyedMatcherSetI kms = new KeyedMatcherSet(); - - /* - * empty iterable: - */ - Iterator iterator = kms.getMatchers().iterator(); - assertFalse(iterator.hasNext()); - - /* - * one matcher: - */ - KeyedMatcherI km1 = new KeyedMatcher(Condition.GE, -2F, "AF"); - kms.and(km1); - iterator = kms.getMatchers().iterator(); - assertSame(km1, iterator.next()); - assertFalse(iterator.hasNext()); - - /* - * two matchers: - */ - KeyedMatcherI km2 = new KeyedMatcher(Condition.LT, 8F, "AF"); - kms.and(km2); - iterator = kms.getMatchers().iterator(); - assertSame(km1, iterator.next()); - assertSame(km2, iterator.next()); - assertFalse(iterator.hasNext()); - } - - /** - * Tests for the 'compound attribute' key i.e. where first key's value is a map - * from which we take the value for the second key, e.g. CSQ : Consequence - */ - @Test(groups = "Functional") - public void testMatches_compoundKey() - { - /* - * a numeric matcher - MatcherTest covers more conditions - */ - KeyedMatcherI km = new KeyedMatcher(Condition.GE, -2F, "CSQ", - "Consequence"); - 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(Condition.Contains, "Cat", "CSQ", "Consequence"); - kms = new KeyedMatcherSet(); - kms.and(km); - assertTrue(kms.matches(key -> "csq".equalsIgnoreCase(key[0]) - && "Consequence".equalsIgnoreCase(key[1]) - ? "raining cats and dogs" - : "showers")); - } -} diff --git a/test/jalview/util/matcher/KeyedMatcherTest.java b/test/jalview/util/matcher/KeyedMatcherTest.java deleted file mode 100644 index 3fd7800..0000000 --- a/test/jalview/util/matcher/KeyedMatcherTest.java +++ /dev/null @@ -1,73 +0,0 @@ -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(Condition.GE, -2F, "AF"); - 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(Condition.Contains, "Cat", "AF"); - assertTrue( - km.matches(key -> "AF".equals(key[0]) ? "raining cats and dogs" - : "showers")); - } - - @Test - public void testToString() - { - /* - * toString uses the i18n translation of the enum conditions - */ - KeyedMatcherI km = new KeyedMatcher(Condition.LT, 1.2f, "AF"); - assertEquals(km.toString(), "AF < 1.2"); - - /* - * Present / NotPresent omit the value pattern - */ - km = new KeyedMatcher(Condition.Present, "", "AF"); - assertEquals(km.toString(), "AF Is present"); - km = new KeyedMatcher(Condition.NotPresent, "", "AF"); - assertEquals(km.toString(), "AF Is not present"); - } - - @Test - public void testGetKey() - { - KeyedMatcherI km = new KeyedMatcher(Condition.GE, -2F, "AF"); - assertEquals(km.getKey(), new String[] { "AF" }); - - /* - * compound key (attribute / subattribute) - */ - km = new KeyedMatcher(Condition.GE, -2F, "CSQ", "Consequence"); - assertEquals(km.getKey(), new String[] { "CSQ", "Consequence" }); - } - - @Test - public void testGetMatcher() - { - KeyedMatcherI km = new KeyedMatcher(Condition.GE, -2F, "AF"); - assertEquals(km.getMatcher().getCondition(), Condition.GE); - assertEquals(km.getMatcher().getFloatValue(), -2F); - assertEquals(km.getMatcher().getPattern(), "-2.0"); - } -} -- 1.7.10.2