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;
* @param featureType
* @return
*/
- KeyedMatcherSetI getFeatureFilter(String featureType);
+ FeatureMatcherSetI getFeatureFilter(String featureType);
/**
* Answers a shallow copy of the feature filters map
*
* @return
*/
- public Map<String, KeyedMatcherSetI> getFeatureFilters();
+ public Map<String, FeatureMatcherSetI> getFeatureFilters();
/**
* Sets the filters for the feature type, or removes them if a null or empty
* @param featureType
* @param filter
*/
- void setFeatureFilter(String featureType, KeyedMatcherSetI filter);
+ void setFeatureFilter(String featureType, FeatureMatcherSetI filter);
/**
* Replaces all feature filters with the given map
*
* @param filters
*/
- void setFeatureFilters(Map<String, KeyedMatcherSetI> filters);
+ void setFeatureFilters(Map<String, FeatureMatcherSetI> filters);
/**
* Returns the colour for a particular feature instance. This includes
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ *
+ * @author gmcarstairs
+ *
+ */
+public class FeatureMatcher implements FeatureMatcherI
+{
+ private static final String COLON = ":";
+
+ /*
+ * if true, match is against feature description
+ */
+ final private boolean byLabel;
+
+ /*
+ * if true, match is against feature score
+ */
+ final private boolean byScore;
+
+ /*
+ * if not null, match is against feature attribute [sub-attribute]
+ */
+ final private String[] key;
+
+ final private MatcherI matcher;
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the feature label (description)
+ *
+ * @param cond
+ * @param pattern
+ * @return
+ */
+ public static FeatureMatcher byLabel(Condition cond, String pattern)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), true, false,
+ null);
+ }
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the feature score
+ *
+ * @param cond
+ * @param pattern
+ * @return
+ */
+ public static FeatureMatcher byScore(Condition cond, String pattern)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), false, true,
+ null);
+ }
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the named feature attribute [and optional sub-attribute]
+ *
+ * @param cond
+ * @param pattern
+ * @param attName
+ * @return
+ */
+ public static FeatureMatcher byAttribute(Condition cond, String pattern,
+ String... attName)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), false, false,
+ attName);
+ }
+
+ private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
+ String[] theKey)
+ {
+ key = theKey;
+ matcher = m;
+ byLabel = forLabel;
+ byScore = forScore;
+ }
+ @Override
+ public boolean matches(SequenceFeature feature)
+ {
+ String value = byLabel ? feature.getDescription()
+ : (byScore ? String.valueOf(feature.getScore())
+ : feature.getValueAsString(key));
+ return matcher.matches(value);
+ }
+
+ @Override
+ public String[] getKey()
+ {
+ return key;
+ }
+
+ @Override
+ public MatcherI getMatcher()
+ {
+ return matcher;
+ }
+
+ /**
+ * Answers a string description of this matcher, suitable for display, debugging
+ * or logging. The format may change in future.
+ */
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.join(COLON, key)).append(" ")
+ .append(matcher.getCondition().toString());
+ Condition condition = matcher.getCondition();
+ if (condition.isNumeric())
+ {
+ sb.append(" ").append(matcher.getPattern());
+ }
+ else if (condition.needsAPattern())
+ {
+ sb.append(" '").append(matcher.getPattern()).append("'");
+ }
+
+ return sb.toString();
+ }
+}
--- /dev/null
+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();
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeatureMatcherSet implements FeatureMatcherSetI
+{
+ List<FeatureMatcherI> matchConditions;
+
+ boolean andConditions;
+
+ /**
+ * Constructor
+ */
+ public FeatureMatcherSet()
+ {
+ matchConditions = new ArrayList<>();
+ }
+
+ @Override
+ public boolean matches(SequenceFeature feature)
+ {
+ /*
+ * no conditions matches anything
+ */
+ if (matchConditions.isEmpty())
+ {
+ return true;
+ }
+
+ /*
+ * AND until failure
+ */
+ if (andConditions)
+ {
+ for (FeatureMatcherI m : matchConditions)
+ {
+ if (!m.matches(feature))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * OR until match
+ */
+ for (FeatureMatcherI m : matchConditions)
+ {
+ if (m.matches(feature))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public FeatureMatcherSetI and(FeatureMatcherI m)
+ {
+ if (!andConditions && matchConditions.size() > 1)
+ {
+ throw new IllegalStateException("Can't add an AND to OR conditions");
+ }
+ matchConditions.add(m);
+ andConditions = true;
+
+ return this;
+ }
+
+ @Override
+ public FeatureMatcherSetI or(FeatureMatcherI m)
+ {
+ if (andConditions && matchConditions.size() > 1)
+ {
+ throw new IllegalStateException("Can't add an OR to AND conditions");
+ }
+ matchConditions.add(m);
+ andConditions = false;
+
+ return this;
+ }
+
+ @Override
+ public boolean isAnded()
+ {
+ return andConditions;
+ }
+
+ @Override
+ public Iterable<FeatureMatcherI> getMatchers()
+ {
+ return matchConditions;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (FeatureMatcherI matcher : matchConditions)
+ {
+ if (!first)
+ {
+ sb.append(andConditions ? " AND " : " OR ");
+ }
+ first = false;
+ sb.append("(").append(matcher.toString()).append(")");
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return matchConditions == null || matchConditions.isEmpty();
+ }
+
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+/**
+ * An interface to describe a set of one or more feature matchers, where all
+ * matchers are combined with either AND or OR
+ *
+ * @author gmcarstairs
+ *
+ */
+public interface FeatureMatcherSetI
+{
+ /**
+ * Answers true if the feature provided passes this matcher's match condition
+ *
+ * @param feature
+ * @return
+ */
+ boolean matches(SequenceFeature feature);
+
+ /**
+ * Answers a new object that matches the logical AND of this and m
+ *
+ * @param m
+ * @return
+ * @throws IllegalStateException
+ * if an attempt is made to AND to existing OR-ed conditions
+ */
+ FeatureMatcherSetI and(FeatureMatcherI m);
+
+ /**
+ * Answers true if any second condition is AND-ed with this one, false if it
+ * is OR-ed
+ *
+ * @return
+ */
+ boolean isAnded();
+
+ /**
+ * Answers a new object that matches the logical OR of this and m
+ *
+ * @param m
+ * @return
+ * @throws IllegalStateException
+ * if an attempt is made to OR to existing AND-ed conditions
+ */
+ FeatureMatcherSetI or(FeatureMatcherI m);
+
+ /**
+ * Answers an iterator over the combined match conditions
+ *
+ * @return
+ */
+ Iterable<FeatureMatcherI> getMatchers();
+
+ /**
+ * Answers true if this object contains no conditions
+ *
+ * @return
+ */
+ boolean isEmpty();
+}
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;
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;
private float originalTransparency;
- private Map<String, KeyedMatcherSetI> originalFilters;
+ private Map<String, FeatureMatcherSetI> originalFilters;
final JInternalFrame frame;
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")
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));
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));
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++;
{
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);
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());
* 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];
{
FeatureSettings me;
- KeyedMatcherSetI currentFilter;
+ FeatureMatcherSetI currentFilter;
Point lastLocation;
currentFilter = me.fr.getFeatureFilter(type);
if (currentFilter == null)
{
- currentFilter = new KeyedMatcherSet();
+ currentFilter = new FeatureMatcherSet();
}
Object[] data = ((FeatureTableModel) table.getModel())
.getData()[rowSelected];
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);
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;
*/
private final FeatureColourI originalColour;
- private final KeyedMatcherSetI originalFilter;
+ private final FeatureMatcherSetI originalFilter;
/*
* set flag to true when setting values programmatically,
*/
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;
/*
/*
* filters for the currently selected feature type
*/
- private List<KeyedMatcherI> filters;
+ private List<FeatureMatcherI> filters;
// set white normally, black to debug layout
private Color debugBorderColour = Color.white;
* Updates the min-max range if Colour By selected item is Score, or an
* attribute, with a min-max range
*/
- protected void updateMinMax()
+ protected void updateColourMinMax()
{
if (!graduatedColour.isSelected())
{
return;
}
- float[] minMax = null;
String colourBy = (String) colourByRangeCombo.getSelectedItem();
- if (MessageManager.getString("label.score").equals(colourBy))
+ String[] attNames = fromAttributeDisplayName(colourBy);
+ float[] minMax = getMinMax(attNames);
+
+ if (minMax != null)
+ {
+ min = minMax[0];
+ max = minMax[1];
+ }
+ }
+
+ /**
+ * Retrieves the min-max range:
+ * <ul>
+ * <li>of feature score, if colour or filter is by Score</li>
+ * <li>else of the selected attribute</li>
+ * </ul>
+ *
+ * @param attNames
+ * @return
+ */
+ private float[] getMinMax(String[] attNames)
+ {
+ float[] minMax = null;
+ if (MessageManager.getString("label.score").equals(attNames[0]))
{
minMax = fr.getMinMax().get(featureType)[0];
}
else
{
// colour by attribute range
- String[] attNames = fromAttributeDisplayName(colourBy);
minMax = FeatureAttributes.getInstance().getMinMax(featureType,
attNames);
}
-
- if (minMax != null)
- {
- min = minMax[0];
- max = minMax[1];
- }
+ return minMax;
}
/**
* ensure min-max range is for the latest choice of
* 'graduated colour by'
*/
- updateMinMax();
+ updateColourMinMax();
FeatureColourI acg = makeColourFromInputs();
/*
* 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())
/*
* 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();
@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();
}
}
* 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);
}
* 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);
}
/**
+ * 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
}
Condition cond = (Condition) condCombo.getSelectedItem();
- if (cond == Condition.Present || cond == Condition.NotPresent)
+ if (cond.needsAPattern())
{
return true;
}
String v1 = value.getText().trim();
if (v1.length() == 0)
{
- return false;
+ // return false;
}
- if (cond.isNumeric())
+ if (cond.isNumeric() && v1.length() > 0)
{
try
{
/**
* Constructs a filter condition from the given input fields, and replaces the
- * condition at filterIndex with the new one
+ * condition at filterIndex with the new one. Does nothing if the pattern field
+ * is blank (unless the match condition is one that doesn't require a pattern,
+ * e.g. 'Is present'). Answers true if the filter was updated, else false.
+ * <p>
+ * This method may update the tooltip on the filter value field to show the
+ * value range, if a numeric condition is selected. This ensures the tooltip is
+ * updated when a numeric valued attribute is chosen on the last 'add a filter'
+ * row.
*
* @param attCombo
* @param condCombo
* @param valueField
* @param filterIndex
*/
- protected void updateFilter(JComboBox<String> attCombo,
+ protected boolean updateFilter(JComboBox<String> attCombo,
JComboBox<Condition> condCombo, JTextField valueField,
int filterIndex)
{
String attName = (String) attCombo.getSelectedItem();
Condition cond = (Condition) condCombo.getSelectedItem();
- String pattern = valueField.getText();
- KeyedMatcherI km = new KeyedMatcher(cond, pattern,
- fromAttributeDisplayName(attName));
+ String pattern = valueField.getText().trim();
+
+ updatePatternTooltip(fromAttributeDisplayName(attName), cond,
+ valueField);
+
+ if (pattern.length() == 0 && cond.needsAPattern())
+ {
+ return false;
+ }
+
+ /*
+ * Construct a matcher that operates on Label, Score,
+ * or named attribute
+ */
+ FeatureMatcherI km = null;
+ if (MessageManager.getString("label.label").equals(attName))
+ {
+ km = FeatureMatcher.byLabel(cond, pattern);
+ }
+ else if (MessageManager.getString("label.score").equals(attName))
+ {
+ km = FeatureMatcher.byScore(cond, pattern);
+ }
+ else
+ {
+ km = FeatureMatcher.byAttribute(cond, pattern,
+ fromAttributeDisplayName(attName));
+ }
filters.set(filterIndex, km);
+
+ return true;
}
/**
* 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)
{
*/
public enum Condition
{
- Contains(false), NotContains(false), Matches(false), NotMatches(false),
- Present(false), NotPresent(false),
- EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true);
+ Contains(false, true), NotContains(false, true), Matches(false, true),
+ NotMatches(false, true), Present(false, false), NotPresent(false, false),
+ EQ(true, true), NE(true, true), LT(true, true), LE(true, true),
+ GT(true, true), GE(true, true);
private static Map<Condition, String> displayNames = new HashMap<>();
private boolean numeric;
- Condition(boolean isNumeric)
+ private boolean needsAPattern;
+
+ Condition(boolean isNumeric, boolean needsPattern)
{
numeric = isNumeric;
+ needsAPattern = needsPattern;
}
/**
}
/**
+ * 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.
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;
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;
/*
* filters for each feature type
*/
- protected Map<String, KeyedMatcherSetI> featureFilters = new HashMap<>();
+ protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
protected String[] renderOrder;
}
@Override
- public Map<String, KeyedMatcherSetI> getFeatureFilters()
+ public Map<String, FeatureMatcherSetI> getFeatureFilters()
{
return new HashMap<>(featureFilters);
}
@Override
- public void setFeatureFilters(Map<String, KeyedMatcherSetI> filters)
+ public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
{
featureFilters = filters;
}
@Override
- public KeyedMatcherSetI getFeatureFilter(String featureType)
+ public FeatureMatcherSetI getFeatureFilter(String featureType)
{
return featureFilters.get(featureType);
}
@Override
- public void setFeatureFilter(String featureType, KeyedMatcherSetI filter)
+ public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
{
if (filter == null || filter.isEmpty())
{
*/
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);
}
}
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;
/*
* map of {featureType, filters}
*/
- Map<String, KeyedMatcherSetI> featureFilters;
+ Map<String, FeatureMatcherSetI> featureFilters;
float transparency;
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherSetTest
+{
+ @Test(groups = "Functional")
+ public void testMatches_byAttribute()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "AF");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ fms.and(fm);
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "foobar");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "-2");
+ assertTrue(fms.matches(sf));
+ sf.setValue("AF", "-1");
+ assertTrue(fms.matches(sf));
+ sf.setValue("AF", "-3");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "");
+ assertFalse(fms.matches(sf));
+
+ /*
+ * a string pattern matcher
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+ fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "raining cats and dogs");
+ assertTrue(fms.matches(sf));
+ }
+
+ @Test(groups = "Functional")
+ public void testAnd()
+ {
+ // condition1: AF value contains "dog" (matches)
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+ "dog", "AF");
+ // condition 2: CSQ value does not contain "how" (does not match)
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "how", "CSQ");
+
+ SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12,
+ 6.2f, "grp");
+ sf.setValue("AF", "raining cats and dogs");
+ sf.setValue("CSQ", "showers");
+
+ assertTrue(fm1.matches(sf));
+ assertFalse(fm2.matches(sf));
+
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+ fms.and(fm1);
+ assertTrue(fms.matches(sf));
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+
+ /*
+ * OR a failed attribute condition with a matched label condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+ FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains,
+ "Helix");
+ fms.or(byLabelPass);
+ assertTrue(fms.matches(sf));
+
+ /*
+ * OR a failed attribute condition with a failed score condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+ FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT,
+ "5.9");
+ fms.or(byScoreFail);
+ assertFalse(fms.matches(sf));
+
+ /*
+ * OR failed attribute and score conditions with matched label condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.or(fm2).or(byScoreFail);
+ assertFalse(fms.matches(sf));
+ fms.or(byLabelPass);
+ assertTrue(fms.matches(sf));
+ }
+
+ @Test(groups = "Functional")
+ public void testToString()
+ {
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+ "AF");
+ assertEquals(fm1.toString(), "AF < 1.2");
+
+ FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "path",
+ "CLIN_SIG");
+ assertEquals(fm2.toString(), "CLIN_SIG Does not contain 'PATH'");
+
+ /*
+ * AND them
+ */
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertEquals(fms.toString(), "");
+ fms.and(fm1);
+ assertEquals(fms.toString(), "(AF < 1.2)");
+ fms.and(fm2);
+ assertEquals(fms.toString(),
+ "(AF < 1.2) AND (CLIN_SIG Does not contain 'PATH')");
+
+ /*
+ * OR them
+ */
+ fms = new FeatureMatcherSet();
+ assertEquals(fms.toString(), "");
+ fms.or(fm1);
+ assertEquals(fms.toString(), "(AF < 1.2)");
+ fms.or(fm2);
+ assertEquals(fms.toString(),
+ "(AF < 1.2) OR (CLIN_SIG Does not contain 'PATH')");
+
+ try
+ {
+ fms.and(fm1);
+ fail("Expected exception");
+ } catch (IllegalStateException e)
+ {
+ // expected
+ }
+ }
+
+ @Test(groups = "Functional")
+ public void testOr()
+ {
+ // condition1: AF value contains "dog" (matches)
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+ "dog", "AF");
+ // condition 2: CSQ value does not contain "how" (does not match)
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "how", "CSQ");
+
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+ sf.setValue("AF", "raining cats and dogs");
+ sf.setValue("CSQ", "showers");
+
+ assertTrue(fm1.matches(sf));
+ assertFalse(fm2.matches(sf));
+
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+ fms.or(fm1);
+ assertTrue(fms.matches(sf));
+ fms.or(fm2);
+ assertTrue(fms.matches(sf)); // true or false makes true
+
+ fms = new FeatureMatcherSet();
+ fms.or(fm2);
+ assertFalse(fms.matches(sf));
+ fms.or(fm1);
+ assertTrue(fms.matches(sf)); // false or true makes true
+
+ try
+ {
+ fms.and(fm2);
+ fail("Expected exception");
+ } catch (IllegalStateException e)
+ {
+ // expected
+ }
+ }
+
+ @Test(groups = "Functional")
+ public void testIsEmpty()
+ {
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0",
+ "AF");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.isEmpty());
+ fms.and(fm);
+ assertFalse(fms.isEmpty());
+ }
+
+ @Test(groups = "Functional")
+ public void testGetMatchers()
+ {
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+
+ /*
+ * empty iterable:
+ */
+ Iterator<FeatureMatcherI> iterator = fms.getMatchers().iterator();
+ assertFalse(iterator.hasNext());
+
+ /*
+ * one matcher:
+ */
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "AF");
+ fms.and(fm1);
+ iterator = fms.getMatchers().iterator();
+ assertSame(fm1, iterator.next());
+ assertFalse(iterator.hasNext());
+
+ /*
+ * two matchers:
+ */
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f",
+ "AF");
+ fms.and(fm2);
+ iterator = fms.getMatchers().iterator();
+ assertSame(fm1, iterator.next());
+ assertSame(fm2, iterator.next());
+ assertFalse(iterator.hasNext());
+ }
+
+ /**
+ * Tests for the 'compound attribute' key i.e. where first key's value is a map
+ * from which we take the value for the second key, e.g. CSQ : Consequence
+ */
+ @Test(groups = "Functional")
+ public void testMatches_compoundKey()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "CSQ", "Consequence");
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ Map<String, String> csq = new HashMap<>();
+ sf.setValue("CSQ", csq);
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "-2");
+ assertTrue(fms.matches(sf));
+ csq.put("Consequence", "-1");
+ assertTrue(fms.matches(sf));
+ csq.put("Consequence", "-3");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "junk");
+ assertFalse(fms.matches(sf));
+
+ /*
+ * a string pattern matcher
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ",
+ "Consequence");
+ fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ csq.put("PolyPhen", "damaging");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "damaging");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "Catastrophic");
+ assertTrue(fms.matches(sf));
+ }
+}
--- /dev/null
+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");
+ }
+}
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;
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));
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"));
/*
features = fr.findFeaturesAtColumn(seq, 5);
assertEquals(features.size(), 1);
assertTrue(features.contains(sf8));
-
+
/*
* give "Type3" features a graduated colour scheme
* - first with no threshold
// 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
*/
// 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
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));