From: gmungoc Date: Thu, 2 Nov 2017 15:53:06 +0000 (+0000) Subject: JAL-2808 spike updated with latest X-Git-Tag: Release_2_11_0~62^2~18 X-Git-Url: http://source.jalview.org/gitweb/?p=jalview.git;a=commitdiff_plain;h=baa077bd19420018433d78927aad3ad139e47351 JAL-2808 spike updated with latest --- diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 9ffe2ae..851585a 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -1324,3 +1324,19 @@ label.overview = Overview label.reset_to_defaults = Reset to defaults label.oview_calc = Recalculating overview... label.feature_details = Feature details +label.matchCondition_contains = Contains +label.matchCondition_notcontains = Does not contain +label.matchCondition_matches = Matches +label.matchCondition_notmatches = Does not match +label.matchCondition_eq = = +label.matchCondition_ne = not = +label.matchCondition_lt = < +label.matchCondition_le = <= +label.matchCondition_gt = > +label.matchCondition_ge = >= +label.numeric_required = The value should be numeric +label.no_attributes_known = No attributes known +label.filters = Filters +label.match_condition = Match condition +label.join_conditions = Join conditions with +label.feature_to_filter = Feature to filter diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 9d2d7f4..40c7d4d 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -22,6 +22,7 @@ package jalview.api; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.util.matcher.KeyedMatcherSetI; import java.awt.Color; import java.awt.Graphics; @@ -215,4 +216,53 @@ public interface FeatureRenderer */ float getTransparency(); + /** + * Answers the filters applied to the given feature type, or null if none is + * set + * + * @param featureType + * @return + */ + KeyedMatcherSetI getFeatureFilter(String featureType); + + /** + * Answers a shallow copy of the feature filters map + * + * @return + */ + public Map getFeatureFilters(); + + /** + * Sets the filters for the feature type, or removes them if a null or empty + * filter is passed + * + * @param featureType + * @param filter + */ + void setFeatureFilter(String featureType, KeyedMatcherSetI filter); + + /** + * Replaces all feature filters with the given map + * + * @param filters + */ + void setFeatureFilters(Map filters); + + /** + * Returns the colour for a particular feature instance. This includes + * calculation of 'colour by label', or of a graduated score colour, if + * applicable. + *

+ * Returns null if + *

    + *
  • feature type is not visible, or
  • + *
  • feature group is not visible, or
  • + *
  • feature values lie outside any colour threshold, or
  • + *
  • feature is excluded by filter conditions
  • + *
+ * + * @param feature + * @return + */ + Color getColour(SequenceFeature feature); } diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index 55320ed..9a61f5f 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -417,7 +417,6 @@ public class SeqPanel extends Panel implements MouseMotionListener, * alignment column * @param seq * index of sequence in alignment - * @return position of column in sequence or -1 if at gap */ void setStatusMessage(SequenceI sequence, int column, int seq) { diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index 460c2b3..5662d0c 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -53,20 +53,19 @@ public class AlignViewController implements AlignViewControllerI private AlignViewControllerGuiI avcg; public AlignViewController(AlignViewControllerGuiI alignFrame, - AlignViewportI viewport, AlignmentViewPanel alignPanel) + AlignViewportI vp, AlignmentViewPanel ap) { this.avcg = alignFrame; - this.viewport = viewport; - this.alignPanel = alignPanel; + this.viewport = vp; + this.alignPanel = ap; } @Override - public void setViewportAndAlignmentPanel(AlignViewportI viewport, - AlignmentViewPanel alignPanel) + public void setViewportAndAlignmentPanel(AlignViewportI vp, + AlignmentViewPanel ap) { - this.alignPanel = alignPanel; - this.viewport = viewport; - + this.alignPanel = ap; + this.viewport = vp; } @Override @@ -215,17 +214,21 @@ public class AlignViewController implements AlignViewControllerI /** * Sets a bit in the BitSet for each column (base 0) in the sequence - * collection which includes the specified feature type. Returns the number of - * sequences which have the feature in the selected range. + * collection which includes a visible feature of the specified feature type. + * Returns the number of sequences which have the feature visible in the + * selected range. * * @param featureType * @param sqcol * @param bs * @return */ - static int findColumnsWithFeature(String featureType, + int findColumnsWithFeature(String featureType, SequenceCollectionI sqcol, BitSet bs) { + FeatureRenderer fr = alignPanel == null ? null : alignPanel + .getFeatureRenderer(); + final int startColumn = sqcol.getStartRes() + 1; // converted to base 1 final int endColumn = sqcol.getEndRes() + 1; List seqs = sqcol.getSequences(); @@ -238,13 +241,19 @@ public class AlignViewController implements AlignViewControllerI List sfs = sq.findFeatures(startColumn, endColumn, featureType); - if (!sfs.isEmpty()) - { - nseq++; - } - + boolean found = false; for (SequenceFeature sf : sfs) { + if (fr.getColour(sf) == null) + { + continue; + } + if (!found) + { + nseq++; + } + found = true; + int sfStartCol = sq.findIndex(sf.getBegin()); int sfEndCol = sq.findIndex(sf.getEnd()); diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 8f82a1a..ffbd497 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -21,6 +21,7 @@ package jalview.datamodel; import jalview.datamodel.features.FeatureAttributeType; +import jalview.datamodel.features.FeatureAttributes; import jalview.datamodel.features.FeatureLocationI; import jalview.datamodel.features.FeatureSourceI; import jalview.datamodel.features.FeatureSources; @@ -392,6 +393,23 @@ public class SequenceFeature implements FeatureLocationI } /** + * Answers the value of the specified attribute as string, or null if no such + * value + * + * @param key + * @return + */ + public String getValueAsString(String key) + { + if (otherDetails == null) + { + return null; + } + Object value = otherDetails.get(key); + return value == null ? null : value.toString(); + } + + /** * Returns a property value for the given key if known, else the specified * default value * @@ -424,6 +442,7 @@ public class SequenceFeature implements FeatureLocationI } otherDetails.put(key, value); + FeatureAttributes.getInstance().addAttribute(this.type, key); } } diff --git a/src/jalview/datamodel/features/FeatureAttributes.java b/src/jalview/datamodel/features/FeatureAttributes.java new file mode 100644 index 0000000..d4e9fb0 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureAttributes.java @@ -0,0 +1,93 @@ +package jalview.datamodel.features; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * A singleton class to hold the set of attributes known for each feature type + */ +public class FeatureAttributes +{ + private static FeatureAttributes instance = new FeatureAttributes(); + + private Map> attributes; + + /** + * Answers the singleton instance of this class + * + * @return + */ + public static FeatureAttributes getInstance() + { + return instance; + } + + private FeatureAttributes() + { + attributes = new HashMap<>(); + } + + /** + * Answers the attributes known for the given feature type, in alphabetical + * order (not case sensitive), or an empty set if no attributes are known + * + * @param featureType + * @return + */ + public List getAttributes(String featureType) + { + if (!attributes.containsKey(featureType)) + { + return Collections. emptyList(); + } + + return new ArrayList<>(attributes.get(featureType)); + } + + /** + * Answers true if at least one attribute is known for the given feature type, + * else false + * + * @param featureType + * @return + */ + public boolean hasAttributes(String featureType) + { + + if (attributes.containsKey(featureType)) + { + if (!attributes.get(featureType).isEmpty()) + { + return true; + } + } + return false; + } + + /** + * Records the given attribute name for the given feature type + * + * @param featureType + * @param attName + */ + public void addAttribute(String featureType, String attName) + { + if (featureType == null || attName == null) + { + return; + } + + if (!attributes.containsKey(featureType)) + { + attributes.put(featureType, new TreeSet( + String.CASE_INSENSITIVE_ORDER)); + } + + attributes.get(featureType).add(attName); + } +} diff --git a/src/jalview/datamodel/features/FeatureSources.java b/src/jalview/datamodel/features/FeatureSources.java index 96efb41..1be1b82 100644 --- a/src/jalview/datamodel/features/FeatureSources.java +++ b/src/jalview/datamodel/features/FeatureSources.java @@ -3,6 +3,13 @@ package jalview.datamodel.features; import java.util.HashMap; import java.util.Map; +/** + * A singleton to hold metadata about feature attributes, keyed by a unique + * feature source identifier + * + * @author gmcarstairs + * + */ public class FeatureSources { private static FeatureSources instance = new FeatureSources(); @@ -10,7 +17,7 @@ public class FeatureSources private Map sources; /** - * Answers the singelton instance of this class + * Answers the singleton instance of this class * * @return */ diff --git a/src/jalview/gui/FeatureColourChooser.java b/src/jalview/gui/FeatureColourChooser.java index d8db546..89b64a7 100644 --- a/src/jalview/gui/FeatureColourChooser.java +++ b/src/jalview/gui/FeatureColourChooser.java @@ -29,6 +29,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; +import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; @@ -221,54 +222,35 @@ public class FeatureColourChooser extends JalviewDialog private void jbInit() throws Exception { + this.setLayout(new GridLayout(4, 1)); - minColour.setFont(JvSwingUtils.getLabelFont()); - minColour.setBorder(BorderFactory.createLineBorder(Color.black)); - minColour.setPreferredSize(new Dimension(40, 20)); - minColour.setToolTipText(MessageManager.getString("label.min_colour")); - minColour.addMouseListener(new MouseAdapter() - { - @Override - public void mousePressed(MouseEvent e) - { - if (minColour.isEnabled()) - { - minColour_actionPerformed(); - } - } - }); - maxColour.setFont(JvSwingUtils.getLabelFont()); - maxColour.setBorder(BorderFactory.createLineBorder(Color.black)); - maxColour.setPreferredSize(new Dimension(40, 20)); - maxColour.setToolTipText(MessageManager.getString("label.max_colour")); - maxColour.addMouseListener(new MouseAdapter() - { - @Override - public void mousePressed(MouseEvent e) - { - if (maxColour.isEnabled()) - { - maxColour_actionPerformed(); - } - } - }); - maxColour.setBorder(new LineBorder(Color.black)); - JLabel minText = new JLabel(MessageManager.getString("label.min")); - minText.setFont(JvSwingUtils.getLabelFont()); - JLabel maxText = new JLabel(MessageManager.getString("label.max")); - maxText.setFont(JvSwingUtils.getLabelFont()); - this.setLayout(new BorderLayout()); - JPanel jPanel1 = new JPanel(); - jPanel1.setBackground(Color.white); - JPanel jPanel2 = new JPanel(); - jPanel2.setLayout(new FlowLayout()); - jPanel2.setBackground(Color.white); + JPanel colourByPanel = initColoursPanel(); + + JPanel thresholdPanel = initThresholdPanel(); + + JPanel okCancelPanel = initOkCancelPanel(); + + this.add(colourByPanel); + this.add(thresholdPanel); + + this.add(okCancelPanel); + } + + /** + * Lay out fields for threshold options + * + * @return + */ + protected JPanel initThresholdPanel() + { + JPanel thresholdPanel = new JPanel(); + thresholdPanel.setLayout(new FlowLayout()); threshold.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - threshold_actionPerformed(); + changeColour(true); } }); threshold.setToolTipText(MessageManager @@ -280,8 +262,6 @@ public class FeatureColourChooser extends JalviewDialog threshold.addItem(MessageManager .getString("label.threshold_feature_below_threshold")); // index 2 - JPanel jPanel3 = new JPanel(); - jPanel3.setLayout(new FlowLayout()); thresholdValue.addActionListener(new ActionListener() { @Override @@ -308,7 +288,7 @@ public class FeatureColourChooser extends JalviewDialog MessageManager.getString("label.adjust_threshold")); thresholdValue.setEnabled(false); thresholdValue.setColumns(7); - jPanel3.setBackground(Color.white); + thresholdPanel.setBackground(Color.white); thresholdIsMin.setBackground(Color.white); thresholdIsMin .setText(MessageManager.getString("label.threshold_minmax")); @@ -319,40 +299,101 @@ public class FeatureColourChooser extends JalviewDialog @Override public void actionPerformed(ActionEvent actionEvent) { - thresholdIsMin_actionPerformed(); + changeColour(true); } }); - colourByLabel.setBackground(Color.white); - colourByLabel - .setText(MessageManager.getString("label.colour_by_label")); - colourByLabel.setToolTipText(MessageManager.getString( - "label.display_features_same_type_different_label_using_different_colour")); - colourByLabel.addActionListener(new ActionListener() + thresholdPanel.add(threshold); + thresholdPanel.add(slider); + thresholdPanel.add(thresholdValue); + thresholdPanel.add(thresholdIsMin); + return thresholdPanel; + } + + /** + * Lay out OK and Cancel buttons + * + * @return + */ + protected JPanel initOkCancelPanel() + { + JPanel okCancelPanel = new JPanel(); + okCancelPanel.setBackground(Color.white); + okCancelPanel.add(ok); + okCancelPanel.add(cancel); + return okCancelPanel; + } + + /** + * Lay out Colour by Label and min/max colour widgets + * + * @return + */ + protected JPanel initColoursPanel() + { + JPanel colourByPanel = new JPanel(); + colourByPanel.setLayout(new FlowLayout()); + colourByPanel.setBackground(Color.white); + minColour.setFont(JvSwingUtils.getLabelFont()); + minColour.setBorder(BorderFactory.createLineBorder(Color.black)); + minColour.setPreferredSize(new Dimension(40, 20)); + minColour.setToolTipText(MessageManager.getString("label.min_colour")); + minColour.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent actionEvent) + public void mousePressed(MouseEvent e) { - colourByLabel_actionPerformed(); + if (minColour.isEnabled()) + { + minColour_actionPerformed(); + } } }); + maxColour.setFont(JvSwingUtils.getLabelFont()); + maxColour.setBorder(BorderFactory.createLineBorder(Color.black)); + maxColour.setPreferredSize(new Dimension(40, 20)); + maxColour.setToolTipText(MessageManager.getString("label.max_colour")); + maxColour.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent e) + { + if (maxColour.isEnabled()) + { + maxColour_actionPerformed(); + } + } + }); + maxColour.setBorder(new LineBorder(Color.black)); + JLabel minText = new JLabel(MessageManager.getString("label.min")); + minText.setFont(JvSwingUtils.getLabelFont()); + JLabel maxText = new JLabel(MessageManager.getString("label.max")); + maxText.setFont(JvSwingUtils.getLabelFont()); JPanel colourPanel = new JPanel(); colourPanel.setBackground(Color.white); - jPanel1.add(ok); - jPanel1.add(cancel); - jPanel2.add(colourByLabel, BorderLayout.WEST); - jPanel2.add(colourPanel, BorderLayout.EAST); colourPanel.add(minText); colourPanel.add(minColour); colourPanel.add(maxText); colourPanel.add(maxColour); - this.add(jPanel3, BorderLayout.CENTER); - jPanel3.add(threshold); - jPanel3.add(slider); - jPanel3.add(thresholdValue); - jPanel3.add(thresholdIsMin); - this.add(jPanel1, BorderLayout.SOUTH); - this.add(jPanel2, BorderLayout.NORTH); + colourByPanel.add(colourByLabel, BorderLayout.WEST); + colourByPanel.add(colourPanel, BorderLayout.EAST); + + colourByLabel.setBackground(Color.white); + colourByLabel + .setText(MessageManager.getString("label.colour_by_label")); + colourByLabel + .setToolTipText(MessageManager + .getString("label.display_features_same_type_different_label_using_different_colour")); + colourByLabel.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent actionEvent) + { + changeColour(true); + } + }); + + return colourByPanel; } /** @@ -505,6 +546,7 @@ public class FeatureColourChooser extends JalviewDialog maxColour.setForeground(oldmaxColour); minColour.setForeground(oldminColour); } + fr.setColour(type, acg); cs = acg; ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview); @@ -544,14 +586,6 @@ public class FeatureColourChooser extends JalviewDialog } /** - * Action on change of choice of No / Above / Below Threshold - */ - protected void threshold_actionPerformed() - { - changeColour(true); - } - - /** * Action on text entry of a threshold value */ protected void thresholdValue_actionPerformed() @@ -594,16 +628,6 @@ public class FeatureColourChooser extends JalviewDialog changeColour(false); } - protected void thresholdIsMin_actionPerformed() - { - changeColour(true); - } - - protected void colourByLabel_actionPerformed() - { - changeColour(true); - } - void addActionListener(ActionListener graduatedColorEditor) { if (colourEditor != null) diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 3f1d9c7..d724b8c 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -25,6 +25,7 @@ import jalview.api.FeatureSettingsControllerI; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureAttributes; import jalview.gui.Help.HelpId; import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; @@ -34,19 +35,29 @@ import jalview.util.Format; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.util.QuickSort; +import jalview.util.ReverseListIterator; +import jalview.util.matcher.Condition; +import jalview.util.matcher.KeyedMatcher; +import jalview.util.matcher.KeyedMatcherI; +import jalview.util.matcher.KeyedMatcherSet; +import jalview.util.matcher.KeyedMatcherSetI; import jalview.viewmodel.AlignmentViewport; +import jalview.ws.DasSequenceFeatureFetcher; import jalview.ws.dbsources.das.api.jalviewSourceI; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; @@ -60,6 +71,7 @@ import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Hashtable; @@ -72,11 +84,13 @@ import java.util.Vector; import javax.help.HelpSetException; import javax.swing.AbstractCellEditor; import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JColorChooser; +import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JInternalFrame; import javax.swing.JLabel; @@ -84,15 +98,19 @@ import javax.swing.JLayeredPane; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicArrowButton; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; @@ -100,11 +118,13 @@ import javax.swing.table.TableCellRenderer; public class FeatureSettings extends JPanel implements FeatureSettingsControllerI { - DasSourceBrowser dassourceBrowser; + private static final int MIN_WIDTH = 400; - jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher; + private static final int MIN_HEIGHT = 400; + + DasSourceBrowser dassourceBrowser; - JPanel settingsPane = new JPanel(); + DasSequenceFeatureFetcher dasFeatureFetcher; JPanel dasSettingsPane = new JPanel(); @@ -112,10 +132,15 @@ public class FeatureSettings extends JPanel public final AlignFrame af; + /* + * 'original' fields hold settings to restore on Cancel + */ Object[][] originalData; private float originalTransparency; + private Map originalFilters; + final JInternalFrame frame; JScrollPane scrollPane = new JScrollPane(); @@ -125,30 +150,68 @@ public class FeatureSettings extends JPanel JPanel groupPanel; JSlider transparency = new JSlider(); - - JPanel transPanel = new JPanel(new GridLayout(1, 2)); - - private static final int MIN_WIDTH = 400; - - private static final int MIN_HEIGHT = 400; - /** + /* * when true, constructor is still executing - so ignore UI events */ protected volatile boolean inConstruction = true; + int selectedRow = -1; + + JButton fetchDAS = new JButton(); + + JButton saveDAS = new JButton(); + + JButton cancelDAS = new JButton(); + + boolean resettingTable = false; + + /* + * true when Feature Settings are updating from feature renderer + */ + private boolean handlingUpdate = false; + + /* + * holds {featureCount, totalExtent} for each feature type + */ + Map typeWidth = null; + + /* + * fields of the feature filters tab + */ + private JPanel filtersPane; + + private JPanel chooseFiltersPanel; + + private JComboBox filteredFeatureChoice; + + private JRadioButton andFilters; + + private JRadioButton orFilters; + + /* + * filters for the currently selected feature type + */ + private List filters; + + private JTextArea filtersAsText; + /** * Constructor * * @param af */ - public FeatureSettings(AlignFrame af) + public FeatureSettings(AlignFrame alignFrame) { - this.af = af; + this.af = alignFrame; fr = af.getFeatureRenderer(); - // allow transparency to be recovered - transparency.setMaximum(100 - - (int) ((originalTransparency = fr.getTransparency()) * 100)); + + // save transparency for restore on Cancel + originalTransparency = fr.getTransparency(); + int originalTransparencyAsPercent = (int) (originalTransparency * 100); + transparency.setMaximum(100 - originalTransparencyAsPercent); + + originalFilters = fr.getFeatureFilters(); try { @@ -286,13 +349,13 @@ public class FeatureSettings extends JPanel { Desktop.addInternalFrame(frame, MessageManager.getString("label.sequence_feature_settings"), - 475, 480); + 600, 480); } else { Desktop.addInternalFrame(frame, MessageManager.getString("label.sequence_feature_settings"), - 400, 450); + 600, 450); } frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT)); @@ -478,16 +541,6 @@ public class FeatureSettings extends JPanel men.show(table, x, y); } - /** - * true when Feature Settings are updating from feature renderer - */ - private boolean handlingUpdate = false; - - /** - * holds {featureCount, totalExtent} for each feature type - */ - Map typeWidth = null; - @Override synchronized public void discoverAllFeatureData() { @@ -507,6 +560,8 @@ public class FeatureSettings extends JPanel } } + populateFilterableFeatures(); + resetTable(null); validate(); @@ -549,8 +604,6 @@ public class FeatureSettings extends JPanel return visible; } - boolean resettingTable = false; - synchronized void resetTable(String[] groupChanged) { if (resettingTable) @@ -1063,62 +1116,26 @@ public class FeatureSettings extends JPanel } } - int selectedRow = -1; - - JTabbedPane tabbedPane = new JTabbedPane(); - - BorderLayout borderLayout1 = new BorderLayout(); - - BorderLayout borderLayout2 = new BorderLayout(); - - BorderLayout borderLayout3 = new BorderLayout(); - - JPanel bigPanel = new JPanel(); - - BorderLayout borderLayout4 = new BorderLayout(); - - JButton invert = new JButton(); - - JPanel buttonPanel = new JPanel(); - - JButton cancel = new JButton(); - - JButton ok = new JButton(); - - JButton loadColours = new JButton(); - - JButton saveColours = new JButton(); - - JPanel dasButtonPanel = new JPanel(); - - JButton fetchDAS = new JButton(); - - JButton saveDAS = new JButton(); - - JButton cancelDAS = new JButton(); - - JButton optimizeOrder = new JButton(); - - JButton sortByScore = new JButton(); + private void jbInit() throws Exception + { + this.setLayout(new BorderLayout()); - JButton sortByDens = new JButton(); + JPanel settingsPane = new JPanel(); + settingsPane.setLayout(new BorderLayout()); - JButton help = new JButton(); + filtersPane = new JPanel(); - JPanel transbuttons = new JPanel(new GridLayout(5, 1)); + dasSettingsPane.setLayout(new BorderLayout()); - private void jbInit() throws Exception - { - this.setLayout(borderLayout1); - settingsPane.setLayout(borderLayout2); - dasSettingsPane.setLayout(borderLayout3); - bigPanel.setLayout(borderLayout4); + JPanel bigPanel = new JPanel(); + bigPanel.setLayout(new BorderLayout()); groupPanel = new JPanel(); bigPanel.add(groupPanel, BorderLayout.NORTH); + JButton invert = new JButton( + MessageManager.getString("label.invert_selection")); invert.setFont(JvSwingUtils.getLabelFont()); - invert.setText(MessageManager.getString("label.invert_selection")); invert.addActionListener(new ActionListener() { @Override @@ -1127,8 +1144,10 @@ public class FeatureSettings extends JPanel invertSelection(); } }); + + JButton optimizeOrder = new JButton( + MessageManager.getString("label.optimise_order")); optimizeOrder.setFont(JvSwingUtils.getLabelFont()); - optimizeOrder.setText(MessageManager.getString("label.optimise_order")); optimizeOrder.addActionListener(new ActionListener() { @Override @@ -1137,9 +1156,10 @@ public class FeatureSettings extends JPanel orderByAvWidth(); } }); + + JButton sortByScore = new JButton( + MessageManager.getString("label.seq_sort_by_score")); sortByScore.setFont(JvSwingUtils.getLabelFont()); - sortByScore - .setText(MessageManager.getString("label.seq_sort_by_score")); sortByScore.addActionListener(new ActionListener() { @Override @@ -1148,9 +1168,9 @@ public class FeatureSettings extends JPanel af.avc.sortAlignmentByFeatureScore(null); } }); - sortByDens.setFont(JvSwingUtils.getLabelFont()); - sortByDens.setText( + JButton sortByDens = new JButton( MessageManager.getString("label.sequence_sort_by_density")); + sortByDens.setFont(JvSwingUtils.getLabelFont()); sortByDens.addActionListener(new ActionListener() { @Override @@ -1159,8 +1179,9 @@ public class FeatureSettings extends JPanel af.avc.sortAlignmentByFeatureDensity(null); } }); + + JButton help = new JButton(MessageManager.getString("action.help")); help.setFont(JvSwingUtils.getLabelFont()); - help.setText(MessageManager.getString("action.help")); help.addActionListener(new ActionListener() { @Override @@ -1191,20 +1212,23 @@ public class FeatureSettings extends JPanel } } }); + + JButton cancel = new JButton(MessageManager.getString("action.cancel")); cancel.setFont(JvSwingUtils.getLabelFont()); - cancel.setText(MessageManager.getString("action.cancel")); cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { fr.setTransparency(originalTransparency); + fr.setFeatureFilters(originalFilters); updateFeatureRenderer(originalData); close(); } }); + + JButton ok = new JButton(MessageManager.getString("action.ok")); ok.setFont(JvSwingUtils.getLabelFont()); - ok.setText(MessageManager.getString("action.ok")); ok.addActionListener(new ActionListener() { @Override @@ -1213,8 +1237,10 @@ public class FeatureSettings extends JPanel close(); } }); + + JButton loadColours = new JButton( + MessageManager.getString("label.load_colours")); loadColours.setFont(JvSwingUtils.getLabelFont()); - loadColours.setText(MessageManager.getString("label.load_colours")); loadColours.addActionListener(new ActionListener() { @Override @@ -1223,8 +1249,10 @@ public class FeatureSettings extends JPanel load(); } }); + + JButton saveColours = new JButton( + MessageManager.getString("label.save_colours")); saveColours.setFont(JvSwingUtils.getLabelFont()); - saveColours.setText(MessageManager.getString("label.save_colours")); saveColours.addActionListener(new ActionListener() { @Override @@ -1267,6 +1295,8 @@ public class FeatureSettings extends JPanel saveDAS_actionPerformed(e); } }); + + JPanel dasButtonPanel = new JPanel(); dasButtonPanel.setBorder(BorderFactory.createEtchedBorder()); dasSettingsPane.setBorder(null); cancelDAS.setEnabled(false); @@ -1279,32 +1309,455 @@ public class FeatureSettings extends JPanel cancelDAS_actionPerformed(e); } }); - this.add(tabbedPane, java.awt.BorderLayout.CENTER); + + JTabbedPane tabbedPane = new JTabbedPane(); + this.add(tabbedPane, BorderLayout.CENTER); tabbedPane.addTab(MessageManager.getString("label.feature_settings"), settingsPane); - tabbedPane.addTab(MessageManager.getString("label.das_settings"), - dasSettingsPane); - bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH); + tabbedPane.addTab(MessageManager.getString("label.filters"), + filtersPane); + // tabbedPane.addTab(MessageManager.getString("label.das_settings"), + // dasSettingsPane); + + JPanel transPanel = new JPanel(new GridLayout(1, 2)); + bigPanel.add(transPanel, BorderLayout.SOUTH); + + JPanel transbuttons = new JPanel(new GridLayout(5, 1)); transbuttons.add(optimizeOrder); transbuttons.add(invert); transbuttons.add(sortByScore); transbuttons.add(sortByDens); transbuttons.add(help); - JPanel sliderPanel = new JPanel(); - sliderPanel.add(transparency); transPanel.add(transparency); transPanel.add(transbuttons); + + JPanel buttonPanel = new JPanel(); buttonPanel.add(ok); buttonPanel.add(cancel); buttonPanel.add(loadColours); buttonPanel.add(saveColours); - bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER); - dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH); + bigPanel.add(scrollPane, BorderLayout.CENTER); + dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH); dasButtonPanel.add(fetchDAS); dasButtonPanel.add(cancelDAS); dasButtonPanel.add(saveDAS); - settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER); - settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH); + settingsPane.add(bigPanel, BorderLayout.CENTER); + settingsPane.add(buttonPanel, BorderLayout.SOUTH); + + initFiltersTab(); + } + + /** + * Populates initial layout of the feature attribute filters panel + */ + protected void initFiltersTab() + { + filters = new ArrayList<>(); + + /* + * choose feature type + */ + JPanel chooseTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + chooseTypePanel.setBackground(Color.white); + chooseTypePanel.setBorder(BorderFactory + .createTitledBorder(MessageManager + .getString("label.feature_type"))); + filteredFeatureChoice = new JComboBox<>(); + filteredFeatureChoice.addItemListener(new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + refreshFiltersDisplay(); + } + }); + chooseTypePanel.add(new JLabel(MessageManager + .getString("label.feature_to_filter"))); + chooseTypePanel.add(filteredFeatureChoice); + populateFilterableFeatures(); + + /* + * the panel with the filters for the selected feature type + */ + JPanel filtersPanel = new JPanel(new GridLayout(0, 1)); + filtersPanel.setBackground(Color.white); + filtersPanel.setBorder(BorderFactory + .createTitledBorder(MessageManager.getString("label.filters"))); + + /* + * add AND or OR radio buttons + */ + JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + andOrPanel.setBackground(Color.white); + andFilters = new JRadioButton("And"); + orFilters = new JRadioButton("Or"); + ActionListener actionListener = new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + filtersChanged(); + } + }; + andFilters.addActionListener(actionListener); + orFilters.addActionListener(actionListener); + ButtonGroup andOr = new ButtonGroup(); + andOr.add(andFilters); + andOr.add(orFilters); + andFilters.setSelected(true); + andOrPanel.add(new JLabel(MessageManager + .getString("label.join_conditions"))); + andOrPanel.add(andFilters); + andOrPanel.add(orFilters); + filtersPanel.add(andOrPanel); + + /* + * panel with filters - populated by refreshFiltersDisplay + */ + chooseFiltersPanel = new JPanel(new GridLayout(0, 1)); + filtersPanel.add(chooseFiltersPanel); + + /* + * a read-only text view of the current filters + */ + JPanel showFiltersPanel = new JPanel(new BorderLayout(5, 5)); + showFiltersPanel.setBackground(Color.white); + showFiltersPanel.setBorder(BorderFactory + .createTitledBorder(MessageManager + .getString("label.match_condition"))); + filtersAsText = new JTextArea(); + filtersAsText.setLineWrap(true); + filtersAsText.setWrapStyleWord(true); + showFiltersPanel.add(filtersAsText); + + filtersPane.setLayout(new BorderLayout()); + filtersPane.add(chooseTypePanel, BorderLayout.NORTH); + filtersPane.add(filtersPanel, BorderLayout.CENTER); + filtersPane.add(showFiltersPanel, BorderLayout.SOUTH); + + /* + * update display for initial feature type selection + */ + refreshFiltersDisplay(); + } + + /** + * Adds entries to the 'choose feature to filter' drop-down choice. Only + * feature types which have known attributes (so can be filtered) are + * included, so recall this method to update the list (check for newly added + * attributes). + */ + protected void populateFilterableFeatures() + { + /* + * suppress action handler while updating the list + */ + ItemListener listener = filteredFeatureChoice.getItemListeners()[0]; + filteredFeatureChoice.removeItemListener(listener); + + filteredFeatureChoice.removeAllItems(); + ReverseListIterator types = new ReverseListIterator<>( + fr.getRenderOrder()); + + boolean found = false; + while (types.hasNext()) + { + String type = types.next(); + if (FeatureAttributes.getInstance().hasAttributes(type)) + { + filteredFeatureChoice.addItem(type); + found = true; + } + } + if (!found) + { + filteredFeatureChoice + .addItem("No filterable feature attributes known"); + } + + filteredFeatureChoice.addItemListener(listener); + + } + + /** + * Refreshes the display to show any filters currently configured for the + * selected feature type (editable, with 'remove' option), plus one extra row + * for adding a condition. This should be called on change of selected feature + * type, or after a filter has been removed, added or amended. + */ + protected void refreshFiltersDisplay() + { + /* + * clear the panel and list of filter conditions + */ + chooseFiltersPanel.removeAll(); + + String selectedType = (String) filteredFeatureChoice.getSelectedItem(); + + filters.clear(); + + /* + * look up attributes known for feature type + */ + List attNames = FeatureAttributes.getInstance().getAttributes( + selectedType); + + /* + * if this feature type has filters set, load them first + */ + KeyedMatcherSetI featureFilters = fr.getFeatureFilter(selectedType); + filtersAsText.setText(""); + if (featureFilters != null) + { + filtersAsText.setText(featureFilters.toString()); + if (!featureFilters.isAnded()) + { + orFilters.setSelected(true); + } + Iterator matchers = featureFilters.getMatchers(); + while (matchers.hasNext()) + { + filters.add(matchers.next()); + } + } + + /* + * and an empty filter for the user to populate (add) + */ + KeyedMatcherI noFilter = new KeyedMatcher("", Condition.values()[0], ""); + filters.add(noFilter); + + /* + * render the conditions in rows, each in its own JPanel + */ + int i = 0; + for (KeyedMatcherI filter : filters) + { + String key = filter.getKey(); + Condition condition = filter.getMatcher() + .getCondition(); + String pattern = filter.getMatcher().getPattern(); + JPanel row = addFilter(key, attNames, condition, pattern, i); + chooseFiltersPanel.add(row); + i++; + } + + filtersPane.validate(); + filtersPane.repaint(); + } + + /** + * A helper method that constructs a panel with one filter condition: + *
    + *
  • a drop-down list of attribute names to choose from
  • + *
  • a drop-down list of conditions to choose from
  • + *
  • a text field for input of a match pattern
  • + *
  • optionally, a 'remove' button
  • + *
+ * If attribute, condition or pattern are not null, they are set as defaults + * for the input fields. The 'remove' button is added unless the pattern is + * null or empty (incomplete filter condition). + * + * @param attribute + * @param attNames + * @param cond + * @param pattern + * @param i + * @return + */ + protected JPanel addFilter(String attribute, List attNames, + Condition cond, String pattern, int i) + { + JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT)); + filterRow.setBackground(Color.white); + + /* + * inputs for attribute, condition, pattern + */ + JComboBox attCombo = new JComboBox<>(); + JComboBox condCombo = new JComboBox<>(); + JTextField patternField = new JTextField(8); + + /* + * action handlers that validate and (if valid) apply changes + */ + ActionListener actionListener = new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + if (attCombo.getSelectedItem() != null) + { + if (validateFilter(patternField, condCombo)) + { + updateFilter(attCombo, condCombo, patternField, i); + filtersChanged(); + } + } + } + }; + ItemListener itemListener = new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + actionListener.actionPerformed(null); + } + }; + + /* + * drop-down choice of attribute + */ + if (attNames.isEmpty()) + { + attCombo.addItem("---"); + attCombo.setToolTipText(MessageManager + .getString("label.no_attributes_known")); + } + else + { + attCombo.setToolTipText(""); + for (String attName : attNames) + { + attCombo.addItem(attName); + } + if ("".equals(attribute)) + { + attCombo.setSelectedItem(null); + } + else + { + attCombo.setSelectedItem(attribute); + } + attCombo.addItemListener(itemListener); + } + filterRow.add(attCombo); + + /* + * drop-down choice of test condition + */ + for (Condition c : Condition.values()) + { + condCombo.addItem(c); + } + if (cond != null) + { + condCombo.setSelectedItem(cond); + } + condCombo.addItemListener(itemListener); + filterRow.add(condCombo); + + /* + * pattern to match against + */ + patternField.setText(pattern); + patternField.addActionListener(actionListener); + patternField.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + actionListener.actionPerformed(null); + } + }); + filterRow.add(patternField); + + /* + * add remove button if filter is populated (non-empty pattern) + */ + if (pattern != null && pattern.trim().length() > 0) + { + // todo: gif for - button + JButton removeCondition = new BasicArrowButton(SwingConstants.WEST); + removeCondition.setToolTipText(MessageManager + .getString("label.delete_row")); + removeCondition.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + filters.remove(i); + filtersChanged(); + } + }); + filterRow.add(removeCondition); + } + + return filterRow; + } + + /** + * Action on any change to feature filtering, namely + *
    + *
  • change of selected attribute
  • + *
  • change of selected condition
  • + *
  • change of match pattern
  • + *
  • removal of a condition
  • + *
+ * The action should be to + *
    + *
  • parse and validate the filters
  • + *
  • if valid, update the filter text box
  • + *
  • and apply the filters to the viewport
  • + *
+ */ + protected void filtersChanged() + { + /* + * update the filter conditions for the feature type + */ + String featureType = (String) filteredFeatureChoice.getSelectedItem(); + boolean anded = andFilters.isSelected(); + KeyedMatcherSetI combined = new KeyedMatcherSet(); + + for (KeyedMatcherI filter : filters) + { + String pattern = filter.getMatcher().getPattern(); + if (pattern.trim().length() > 0) + { + if (anded) + { + combined.and(filter); + } + else + { + combined.or(filter); + } + } + } + + /* + * save the filter conditions in the FeatureRenderer + * (note this might now be an empty filter with no conditions) + */ + fr.setFeatureFilter(featureType, combined); + + filtersAsText.setText(combined.toString()); + + refreshFiltersDisplay(); + + af.alignPanel.paintAlignment(true, true); + } + + /** + * Constructs a filter condition from the given input fields, and replaces the + * condition at filterIndex with the new one + * + * @param attCombo + * @param condCombo + * @param valueField + * @param filterIndex + */ + protected void updateFilter(JComboBox 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(attName, cond, pattern); + + filters.set(filterIndex, km); } public void fetchDAS_actionPerformed(ActionEvent e) @@ -1464,6 +1917,51 @@ public class FeatureSettings extends JPanel JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE); } + /** + * Answers true unless a numeric condition has been selected with a + * non-numeric value. Sets the value field to RED with a tooltip if in error. + *

+ * If the pattern entered is empty, this method returns false, but does not + * mark the field as invalid. This supports selecting an attribute for a new + * condition before a match pattern has been entered. + * + * @param value + * @param condCombo + */ + protected boolean validateFilter(JTextField value, + JComboBox condCombo) + { + if (value == null || condCombo == null) + { + return true; // fields not populated + } + + Condition cond = (Condition) condCombo.getSelectedItem(); + value.setBackground(Color.white); + value.setToolTipText(""); + String v1 = value.getText().trim(); + if (v1.length() == 0) + { + return false; + } + + if (cond.isNumeric()) + { + try + { + Float.valueOf(v1); + } catch (NumberFormatException e) + { + value.setBackground(Color.red); + value.setToolTipText(MessageManager + .getString("label.numeric_required")); + return false; + } + } + + return true; + } + // /////////////////////////////////////////////////////////////////////// // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html // /////////////////////////////////////////////////////////////////////// diff --git a/src/jalview/gui/JalviewDialog.java b/src/jalview/gui/JalviewDialog.java index 05f5ffc..1008203 100644 --- a/src/jalview/gui/JalviewDialog.java +++ b/src/jalview/gui/JalviewDialog.java @@ -27,8 +27,8 @@ import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; import javax.swing.JButton; import javax.swing.JDialog; @@ -118,55 +118,14 @@ public abstract class JalviewDialog extends JPanel closeDialog(); } }); - frame.addWindowListener(new WindowListener() + frame.addWindowListener(new WindowAdapter() { - - @Override - public void windowOpened(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowIconified(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowDeiconified(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowDeactivated(WindowEvent e) - { - // TODO Auto-generated method stub - - } - @Override public void windowClosing(WindowEvent e) { // user has cancelled the dialog closeDialog(); } - - @Override - public void windowClosed(WindowEvent e) - { - } - - @Override - public void windowActivated(WindowEvent e) - { - // TODO Auto-generated method stub - - } }); } diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 1f47da3..6687e6a 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -304,14 +304,16 @@ public class FeatureRenderer extends FeatureRendererModel List overlaps = seq.getFeatures().findFeatures( visiblePositions.getBegin(), visiblePositions.getEnd(), type); - filterFeaturesForDisplay(overlaps, fc); + // filterFeaturesForDisplay(overlaps, fc); for (SequenceFeature sf : overlaps) { - Color featureColour = fc.getColor(sf); + Color featureColour = getColor(sf, fc); if (featureColour == null) { - // score feature outwith threshold for colouring + /* + * feature excluded by visibility settings, filters, or colour threshold + */ continue; } diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java new file mode 100644 index 0000000..455f805 --- /dev/null +++ b/src/jalview/util/matcher/Condition.java @@ -0,0 +1,57 @@ +package jalview.util.matcher; + +import jalview.util.MessageManager; + +import java.util.HashMap; +import java.util.Map; + +/** + * An enumeration for binary conditions that a user might choose from when + * setting filter or match conditions for values + */ +public enum Condition +{ + Contains(false), NotContains(false), Matches(false), NotMatches(false), + EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true); + + private static Map displayNames = new HashMap<>(); + + private boolean numeric; + + Condition(boolean isNumeric) + { + numeric = isNumeric; + } + + /** + * Answers true if the condition does a numerical comparison, else false + * (string comparison) + * + * @return + */ + public boolean isNumeric() + { + return numeric; + } + + /** + * Answers a display name for the match condition, suitable for showing in + * drop-down menus. The value may be internationalized using the resource key + * "label.matchCondition_" with the enum name appended. + * + * @return + */ + @Override + public String toString() + { + String name = displayNames.get(this); + if (name != null) + { + return name; + } + name = MessageManager + .getStringOrReturn("label.matchCondition_", name()); + displayNames.put(this, name); + return name; + } +} diff --git a/src/jalview/util/matcher/KeyedMatcher.java b/src/jalview/util/matcher/KeyedMatcher.java new file mode 100644 index 0000000..5e42e1c --- /dev/null +++ b/src/jalview/util/matcher/KeyedMatcher.java @@ -0,0 +1,86 @@ +package jalview.util.matcher; + +import java.util.function.Function; + +/** + * An immutable class that models one or more match conditions, each of which is + * applied to the value obtained by lookup given the match key. + *

+ * 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 +{ + final private String key; + + final private MatcherI matcher; + + /** + * Constructor given a key, a test condition and a match pattern + * + * @param theKey + * @param cond + * @param pattern + */ + public KeyedMatcher(String theKey, Condition cond, String pattern) + { + key = theKey; + matcher = new Matcher(cond, pattern); + } + + /** + * Constructor given a key, a test condition and a numerical value to compare + * to. Note that if a non-numerical condition is specified, the float will be + * converted to a string. + * + * @param theKey + * @param cond + * @param value + */ + public KeyedMatcher(String theKey, Condition cond, float value) + { + key = theKey; + matcher = new Matcher(cond, value); + } + + @Override + public boolean matches(Function valueProvider) + { + String value = valueProvider.apply(key); + return matcher.matches(value); + } + + @Override + public String getKey() + { + return key; + } + + @Override + public MatcherI getMatcher() + { + return matcher; + } + + /** + * Answers a string description of this matcher, suitable for debugging or + * logging. The format may change in future. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(key).append(" ").append(matcher.getCondition().toString()) + .append(" ").append(matcher.getPattern()); + + return sb.toString(); + } +} diff --git a/src/jalview/util/matcher/KeyedMatcherI.java b/src/jalview/util/matcher/KeyedMatcherI.java new file mode 100644 index 0000000..e9fe014 --- /dev/null +++ b/src/jalview/util/matcher/KeyedMatcherI.java @@ -0,0 +1,36 @@ +package jalview.util.matcher; + +import java.util.function.Function; + +/** + * An interface for an object that can apply one or more match conditions, given + * a key-value provider. The match conditions are stored against key values, and + * applied to the value obtained by a key-value lookup. + * + * @author gmcarstairs + */ +public interface KeyedMatcherI +{ + /** + * Answers true if the value provided for this matcher's key passes this + * matcher's match condition + * + * @param valueProvider + * @return + */ + boolean matches(Function valueProvider); + + /** + * Answers the value key this matcher operates on + * + * @return + */ + String getKey(); + + /** + * Answers the match condition that is applied + * + * @return + */ + MatcherI getMatcher(); +} diff --git a/src/jalview/util/matcher/KeyedMatcherSet.java b/src/jalview/util/matcher/KeyedMatcherSet.java new file mode 100644 index 0000000..adc04ba --- /dev/null +++ b/src/jalview/util/matcher/KeyedMatcherSet.java @@ -0,0 +1,122 @@ +package jalview.util.matcher; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +public class KeyedMatcherSet implements KeyedMatcherSetI +{ + List matchConditions; + + boolean andConditions; + + /** + * Constructor + */ + public KeyedMatcherSet() + { + matchConditions = new ArrayList<>(); + } + + @Override + public boolean matches(Function valueProvider) + { + /* + * no conditions matches anything + */ + if (matchConditions.isEmpty()) + { + return true; + } + + /* + * AND until failure + */ + if (andConditions) + { + for (KeyedMatcherI m : matchConditions) + { + if (!m.matches(valueProvider)) + { + return false; + } + } + return true; + } + + /* + * OR until match + */ + for (KeyedMatcherI m : matchConditions) + { + if (m.matches(valueProvider)) + { + return true; + } + } + return false; + } + + @Override + public KeyedMatcherSetI and(KeyedMatcherI m) + { + if (!andConditions && matchConditions.size() > 1) + { + throw new IllegalStateException("Can't add an AND to OR conditions"); + } + matchConditions.add(m); + andConditions = true; + + return this; + } + + @Override + public KeyedMatcherSetI or(KeyedMatcherI m) + { + if (andConditions && matchConditions.size() > 1) + { + throw new IllegalStateException("Can't add an OR to AND conditions"); + } + matchConditions.add(m); + andConditions = false; + + return this; + } + + @Override + public boolean isAnded() + { + return andConditions; + } + + @Override + public Iterator getMatchers() + { + return matchConditions.iterator(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (KeyedMatcherI matcher : matchConditions) + { + if (!first) + { + sb.append(andConditions ? " AND " : " OR "); + } + first = false; + sb.append("(").append(matcher.toString()).append(")"); + } + return sb.toString(); + } + + @Override + public boolean isEmpty() + { + return matchConditions == null || matchConditions.isEmpty(); + } + +} diff --git a/src/jalview/util/matcher/KeyedMatcherSetI.java b/src/jalview/util/matcher/KeyedMatcherSetI.java new file mode 100644 index 0000000..7cbebab --- /dev/null +++ b/src/jalview/util/matcher/KeyedMatcherSetI.java @@ -0,0 +1,65 @@ +package jalview.util.matcher; + +import java.util.Iterator; +import java.util.function.Function; + +/** + * An interface to describe a set of one or more key-value match conditions, + * where all conditions are combined with either AND or OR + * + * @author gmcarstairs + * + */ +public interface KeyedMatcherSetI +{ + /** + * Answers true if the value provided for this matcher's key passes this + * matcher's match condition + * + * @param valueProvider + * @return + */ + boolean matches(Function valueProvider); + + /** + * Answers a new object that matches the logical AND of this and m + * + * @param m + * @return + * @throws IllegalStateException + * if an attempt is made to AND to existing OR-ed conditions + */ + KeyedMatcherSetI and(KeyedMatcherI m); + + /** + * Answers true if any second condition is AND-ed with this one, false if it + * is OR-ed + * + * @return + */ + boolean isAnded(); + + /** + * Answers a new object that matches the logical OR of this and m + * + * @param m + * @return + * @throws IllegalStateException + * if an attempt is made to OR to existing AND-ed conditions + */ + KeyedMatcherSetI or(KeyedMatcherI m); + + /** + * Answers an iterator over the combined match conditions + * + * @return + */ + Iterator getMatchers(); + + /** + * Answers true if this object contains no conditions + * + * @return + */ + boolean isEmpty(); +} diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java new file mode 100644 index 0000000..d8c9361 --- /dev/null +++ b/src/jalview/util/matcher/Matcher.java @@ -0,0 +1,218 @@ +package jalview.util.matcher; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A bean to describe one attribute-based filter + */ +public class Matcher implements MatcherI +{ + /* + * the comparison condition + */ + Condition condition; + + /* + * the string value (upper-cased), or the regex, to compare to + * also holds the string form of float value if a numeric condition + */ + String pattern; + + /* + * the compiled regex if using a pattern match condition + * (reserved for possible future enhancement) + */ + Pattern regexPattern; + + /* + * the value to compare to for a numerical condition + */ + float value; + + /** + * Constructor + * + * @param cond + * @param compareTo + * @return + * @throws NumberFormatException + * if a numerical condition is specified with a non-numeric + * comparision value + * @throws NullPointerException + * if a null condition or comparison string is specified + */ + public Matcher(Condition cond, String compareTo) + { + condition = cond; + if (cond.isNumeric()) + { + value = Float.valueOf(compareTo); + pattern = String.valueOf(value); + } + else + { + // pattern matches will be non-case-sensitive + pattern = compareTo.toUpperCase(); + } + + // if we add regex conditions (e.g. matchesPattern), then + // pattern should hold the raw regex, and + // regexPattern = Pattern.compile(compareTo); + } + + /** + * Constructor for a numerical match condition. Note that if a string + * comparison condition is specified, this will be converted to a comparison + * with the float value as string + * + * @param cond + * @param compareTo + */ + public Matcher(Condition cond, float compareTo) + { + Objects.requireNonNull(cond); + condition = cond; + value = compareTo; + pattern = String.valueOf(compareTo).toUpperCase(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("incomplete-switch") + @Override + public boolean matches(String val) + { + if (condition.isNumeric()) + { + try + { + /* + * treat a null value (no such attribute) as + * failing any numerical filter condition + */ + return val == null ? false : matches(Float.valueOf(val)); + } catch (NumberFormatException e) + { + return false; + } + } + + /* + * a null value matches a negative condition, fails a positive test + */ + if (val == null) + { + return condition == Condition.NotContains + || condition == Condition.NotMatches; + } + + String upper = val.toUpperCase().trim(); + boolean matched = false; + switch(condition) { + case Matches: + matched = upper.equals(pattern); + break; + case NotMatches: + matched = !upper.equals(pattern); + break; + case Contains: + matched = upper.indexOf(pattern) > -1; + break; + case NotContains: + matched = upper.indexOf(pattern) == -1; + break; + } + return matched; + } + + /** + * Applies a numerical comparison match condition + * + * @param f + * @return + */ + @SuppressWarnings("incomplete-switch") + boolean matches(float f) + { + if (!condition.isNumeric()) + { + return matches(String.valueOf(f)); + } + + boolean matched = false; + switch (condition) { + case LT: + matched = f < value; + break; + case LE: + matched = f <= value; + break; + case EQ: + matched = f == value; + break; + case NE: + matched = f != value; + break; + case GT: + matched = f > value; + break; + case GE: + matched = f >= value; + break; + } + + return matched; + } + + /** + * A simple hash function that guarantees that when two objects are equal, + * they have the same hashcode + */ + @Override + public int hashCode() + { + return pattern.hashCode() + condition.hashCode() + (int) value; + } + + /** + * equals is overridden so that we can safely remove Matcher objects from + * collections (e.g. delete an attribut match condition for a feature colour) + */ + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof Matcher)) + { + return false; + } + Matcher m = (Matcher) obj; + return condition == m.condition && value == m.value + && pattern.equals(m.pattern); + } + + @Override + public Condition getCondition() + { + return condition; + } + + @Override + public String getPattern() + { + return pattern; + } + + @Override + public float getFloatValue() + { + return value; + } + + @Override + public String toString() + { + return condition.name() + " " + pattern; + } +} diff --git a/src/jalview/util/matcher/MatcherI.java b/src/jalview/util/matcher/MatcherI.java new file mode 100644 index 0000000..ca6d44c --- /dev/null +++ b/src/jalview/util/matcher/MatcherI.java @@ -0,0 +1,18 @@ +package jalview.util.matcher; + +public interface MatcherI +{ + /** + * Answers true if the given value is matched, else false + * + * @param s + * @return + */ + boolean matches(String s); + + Condition getCondition(); + + String getPattern(); + + float getFloatValue(); +} diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 2f30e94..6461748 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -30,6 +30,7 @@ import jalview.datamodel.features.SequenceFeatures; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; +import jalview.util.matcher.KeyedMatcherSetI; import java.awt.Color; import java.beans.PropertyChangeListener; @@ -49,14 +50,25 @@ public abstract class FeatureRendererModel implements jalview.api.FeatureRenderer { - /** + /* * global transparency for feature */ protected float transparency = 1.0f; - protected Map featureColours = new ConcurrentHashMap(); + /* + * colour scheme for each feature type + */ + protected Map featureColours = new ConcurrentHashMap<>(); + + /* + * visibility flag for each feature group + */ + protected Map featureGroups = new ConcurrentHashMap<>(); - protected Map featureGroups = new ConcurrentHashMap(); + /* + * filters for each feature type + */ + protected Map featureFilters = new HashMap<>(); protected String[] renderOrder; @@ -100,6 +112,7 @@ public abstract class FeatureRendererModel this.renderOrder = frs.renderOrder; this.featureGroups = frs.featureGroups; this.featureColours = frs.featureColours; + this.featureFilters = frs.featureFilters; this.transparency = frs.transparency; this.featureOrder = frs.featureOrder; if (av != null && av != fr.getViewport()) @@ -557,20 +570,11 @@ public abstract class FeatureRendererModel return fc; } - /** - * Returns the configured colour for a particular feature instance. This - * includes calculation of 'colour by label', or of a graduated score colour, - * if applicable. It does not take into account feature visibility or colour - * transparency. Returns null for a score feature whose score value lies - * outside any colour threshold. - * - * @param feature - * @return - */ + @Override public Color getColour(SequenceFeature feature) { FeatureColourI fc = getFeatureStyle(feature.getType()); - return fc.getColor(feature); + return getColor(feature, fc); } /** @@ -995,11 +999,11 @@ public abstract class FeatureRendererModel } /** - * Removes from the list of features any that have a feature group that is not - * displayed, or duplicate the location of a feature of the same type (unless - * a graduated colour scheme or colour by label is applied). Should be used - * only for features of the same feature colour (which normally implies the - * same feature type). + * Removes from the list of features any that duplicate the location of a + * feature of the same type (unless feature is filtered out, or a graduated + * colour scheme or colour by label is applied). Should be used only for + * features of the same feature colour (which normally implies the same + * feature type). * * @param features * @param fc @@ -1019,11 +1023,6 @@ public abstract class FeatureRendererModel while (it.hasNext()) { SequenceFeature sf = it.next(); - if (featureGroupNotShown(sf)) - { - it.remove(); - continue; - } /* * a feature is redundant for rendering purposes if it has the @@ -1045,4 +1044,88 @@ public abstract class FeatureRendererModel } } + @Override + public Map getFeatureFilters() + { + return new HashMap<>(featureFilters); + } + + @Override + public void setFeatureFilters(Map filters) + { + featureFilters = filters; + } + + @Override + public KeyedMatcherSetI getFeatureFilter(String featureType) + { + return featureFilters.get(featureType); + } + + @Override + public void setFeatureFilter(String featureType, KeyedMatcherSetI filter) + { + if (filter == null || filter.isEmpty()) + { + featureFilters.remove(featureType); + } + else + { + featureFilters.put(featureType, filter); + } + } + + /** + * Answers the colour for the feature, or null if the feature is excluded by + * feature type or group visibility, by filters, or by colour threshold + * settings + * + * @param sf + * @param fc + * @return + */ + public Color getColor(SequenceFeature sf, FeatureColourI fc) + { + /* + * is the feature type displayed? + */ + if (!showFeatureOfType(sf.getType())) + { + return null; + } + + /* + * is the feature group displayed? + */ + if (featureGroupNotShown(sf)) + { + return null; + } + + /* + * does the feature pass filters? + */ + if (!featureMatchesFilters(sf)) + { + return null; + } + + return fc.getColor(sf); + } + + /** + * Answers true if there no are filters defined for the feature type, or this + * feature matches the filters. Answers false if the feature fails to match + * filters. + * + * @param sf + * @return + */ + protected boolean featureMatchesFilters(SequenceFeature sf) + { + KeyedMatcherSetI filter = featureFilters.get(sf.getType()); + return filter == null ? true : filter.matches(key -> sf + .getValueAsString(key)); + } + } diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java index dc2ae11..6afaa54 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java @@ -22,8 +22,10 @@ package jalview.viewmodel.seqfeatures; import jalview.api.FeatureColourI; import jalview.schemes.FeatureColour; +import jalview.util.matcher.KeyedMatcherSetI; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -42,6 +44,11 @@ public class FeatureRendererSettings implements Cloneable */ Map featureColours; + /* + * map of {featureType, filters} + */ + Map featureFilters; + float transparency; Map featureOrder; @@ -72,7 +79,9 @@ public class FeatureRendererSettings implements Cloneable renderOrder = null; featureGroups = new ConcurrentHashMap(); featureColours = new ConcurrentHashMap(); + featureFilters = new HashMap<>(); featureOrder = new ConcurrentHashMap(); + if (fr.renderOrder != null) { this.renderOrder = new String[fr.renderOrder.length]; @@ -100,6 +109,12 @@ public class FeatureRendererSettings implements Cloneable featureColours.put(next, new FeatureColour((FeatureColour) val)); } } + + if (fr.featureFilters != null) + { + this.featureFilters.putAll(fr.featureFilters); + } + this.transparency = fr.transparency; if (fr.featureOrder != null) { diff --git a/test/jalview/controller/AlignViewControllerTest.java b/test/jalview/controller/AlignViewControllerTest.java index 2e89b0e..efee93b 100644 --- a/test/jalview/controller/AlignViewControllerTest.java +++ b/test/jalview/controller/AlignViewControllerTest.java @@ -25,6 +25,8 @@ import static org.testng.AssertJUnit.assertTrue; import jalview.analysis.Finder; import jalview.api.AlignViewControllerI; +import jalview.api.FeatureColourI; +import jalview.datamodel.Alignment; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; import jalview.datamodel.Sequence; @@ -35,7 +37,9 @@ import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.io.DataSourceType; import jalview.io.FileLoader; +import jalview.schemes.FeatureColour; +import java.awt.Color; import java.util.Arrays; import java.util.BitSet; @@ -67,13 +71,14 @@ public class AlignViewControllerTest null)); seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f, null)); - seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f, + seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, + 10f, null)); seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15, - 0f, null)); + 10f, null)); // disulfide bond is a 'contact feature' - only select its 'start' and 'end' - seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12, - 0f, null)); + seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", + 8, 12, 0f, null)); /* * select the first five columns --> Metal in seq1 cols 4-5 @@ -86,9 +91,18 @@ public class AlignViewControllerTest sg.addSequence(seq3, false); sg.addSequence(seq4, false); + /* + * set features visible on a viewport as only visible features are selected + */ + AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1, + seq2, seq3, seq4 }), 100, 100); + af.getFeatureRenderer().findAllFeatures(true); + + AlignViewController avc = new AlignViewController(af, af.getViewport(), + af.alignPanel); + BitSet bs = new BitSet(); - int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, - bs); + int seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(1, seqCount); assertEquals(2, bs.cardinality()); assertTrue(bs.get(3)); // base 0 @@ -99,7 +113,7 @@ public class AlignViewControllerTest */ sg.setEndRes(6); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(2, seqCount); assertEquals(4, bs.cardinality()); assertTrue(bs.get(3)); @@ -113,7 +127,7 @@ public class AlignViewControllerTest sg.setStartRes(13); sg.setEndRes(13); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(1, seqCount); assertEquals(1, bs.cardinality()); assertTrue(bs.get(13)); @@ -124,18 +138,35 @@ public class AlignViewControllerTest sg.setStartRes(17); sg.setEndRes(19); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); /* + * threshold Metal to hide where score < 5 + * seq1 feature in columns 4-6 is hidden + * seq2 feature in columns 6-7 is shown + */ + FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f); + fc.setAboveThreshold(true); + fc.setThreshold(5f); + af.getFeatureRenderer().setColour("Metal", fc); + sg.setStartRes(0); + sg.setEndRes(6); + bs.clear(); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); + assertEquals(1, seqCount); + assertEquals(2, bs.cardinality()); + assertTrue(bs.get(5)); + assertTrue(bs.get(6)); + + /* * columns 11-13 should not match disulfide bond at 8/12 */ sg.setStartRes(10); sg.setEndRes(12); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("disulfide bond", - sg, bs); + seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); @@ -145,8 +176,7 @@ public class AlignViewControllerTest sg.setStartRes(5); sg.setEndRes(17); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("disulfide bond", - sg, bs); + seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs); assertEquals(1, seqCount); assertEquals(2, bs.cardinality()); assertTrue(bs.get(8)); @@ -158,7 +188,7 @@ public class AlignViewControllerTest sg.setStartRes(0); sg.setEndRes(19); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs); + seqCount = avc.findColumnsWithFeature("Pfam", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); } diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index 8c9cbc9..c955979 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -279,30 +279,30 @@ public class SequenceFeatureTest { // single locus, no group, no score SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null); - String expected = "
" - + "" - + "
Typevariant
Start/end22
DescriptionG,C
"; + String expected = "
" + + "" + + "
Typevariant
Start/end22
DescriptionG,C
"; assertEquals(expected, sf.getDetailsReport()); // contact feature sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31, null); - expected = "
" - + "" - + "
TypeDisulphide Bond
Start/end28:31
Descriptiona description
"; + expected = "
" + + "" + + "
TypeDisulphide Bond
Start/end28:31
Descriptiona description
"; assertEquals(expected, sf.getDetailsReport()); sf = new SequenceFeature("variant", "G,C", 22, 33, 12.5f, "group"); sf.setValue("Parent", "ENSG001"); sf.setValue("Child", "ENSP002"); - expected = "
" - + "" - + "" - + "" - + "" - + "" - + "
Typevariant
Start/end22-33
DescriptionG,C
Score12.5
Groupgroup
ChildENSP002
ParentENSG001
"; + expected = "
" + + "" + + "" + + "" + + "" + + "" + + "
Typevariant
Start/end22-33
DescriptionG,C
Score12.5
Groupgroup
ChildENSP002
ParentENSG001
"; assertEquals(expected, sf.getDetailsReport()); /* @@ -310,10 +310,10 @@ public class SequenceFeatureTest */ String desc = "Fer2 Status: True Positive Pfam 8_8"; sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot"); - expected = "
" - + "" - + "" - + "
TypePfam
Start/end8-83
DescriptionFer2 Status: True Positive Pfam 8_8
GroupUniprot
"; + expected = "
" + + "" + + "" + + "
TypePfam
Start/end8-83
DescriptionFer2 Status: True Positive Pfam 8_8
GroupUniprot
"; assertEquals(expected, sf.getDetailsReport()); } } diff --git a/test/jalview/gui/AlignFrameTest.java b/test/jalview/gui/AlignFrameTest.java index af9c045..1ee25c7 100644 --- a/test/jalview/gui/AlignFrameTest.java +++ b/test/jalview/gui/AlignFrameTest.java @@ -26,6 +26,7 @@ import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; +import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.bin.Jalview; import jalview.datamodel.Alignment; @@ -39,6 +40,7 @@ import jalview.io.FileLoader; import jalview.io.Jalview2xmlTests; import jalview.renderer.ResidueShaderI; import jalview.schemes.BuriedColourScheme; +import jalview.schemes.FeatureColour; import jalview.schemes.HelixColourScheme; import jalview.schemes.JalviewColourScheme; import jalview.schemes.StrandColourScheme; @@ -69,16 +71,21 @@ public class AlignFrameTest { SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ"); SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ"); - seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, - Float.NaN, null)); - seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, - Float.NaN, null)); + seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null)); + seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f, + null)); seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4, Float.NaN, null)); seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9, Float.NaN, null)); AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 }); - AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight()); + AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), + al.getHeight()); + + /* + * make all features visible (select feature columns checks visibility) + */ + alignFrame.getFeatureRenderer().findAllFeatures(true); /* * hiding a feature not present does nothing @@ -86,13 +93,11 @@ public class AlignFrameTest assertFalse(alignFrame.hideFeatureColumns("exon", true)); assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty()); assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns() - .getHiddenColumnsCopy() - .isEmpty()); + .getHiddenColumnsCopy().isEmpty()); assertFalse(alignFrame.hideFeatureColumns("exon", false)); assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty()); assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns() - .getHiddenColumnsCopy() - .isEmpty()); + .getHiddenColumnsCopy().isEmpty()); /* * hiding a feature in all columns does nothing @@ -100,15 +105,31 @@ public class AlignFrameTest assertFalse(alignFrame.hideFeatureColumns("Metal", true)); assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty()); List hidden = alignFrame.getViewport().getAlignment() - .getHiddenColumns() - .getHiddenColumnsCopy(); + .getHiddenColumns().getHiddenColumnsCopy(); assertTrue(hidden.isEmpty()); /* + * threshold Metal to hide features where score < 5 + * seq1 feature in columns 1-5 is hidden + * seq2 feature in columns 6-10 is shown + */ + FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f); + fc.setAboveThreshold(true); + fc.setThreshold(5f); + alignFrame.getFeatureRenderer().setColour("Metal", fc); + assertTrue(alignFrame.hideFeatureColumns("Metal", true)); + hidden = alignFrame.getViewport().getAlignment().getHiddenColumns() + .getHiddenColumnsCopy(); + assertEquals(hidden.size(), 1); + assertEquals(hidden.get(0)[0], 5); + assertEquals(hidden.get(0)[1], 9); + + /* * hide a feature present in some columns * sequence positions [2-4], [7-9] are column positions * [1-3], [6-8] base zero */ + alignFrame.getViewport().showAllHiddenColumns(); assertTrue(alignFrame.hideFeatureColumns("Turn", true)); hidden = alignFrame.getViewport().getAlignment().getHiddenColumns() .getHiddenColumnsCopy(); diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java new file mode 100644 index 0000000..270aa2a --- /dev/null +++ b/test/jalview/util/matcher/ConditionTest.java @@ -0,0 +1,31 @@ +package jalview.util.matcher; + +import static org.testng.Assert.assertEquals; + +import java.util.Locale; + +import org.testng.annotations.Test; + +public class ConditionTest +{ + @Test + public void testToString() + { + Locale.setDefault(Locale.UK); + assertEquals(Condition.Contains.toString(), "Contains"); + assertEquals(Condition.NotContains.toString(), "Does not contain"); + assertEquals(Condition.Matches.toString(), "Matches"); + assertEquals(Condition.NotMatches.toString(), "Does not match"); + assertEquals(Condition.LT.toString(), "<"); + assertEquals(Condition.LE.toString(), "<="); + assertEquals(Condition.GT.toString(), ">"); + assertEquals(Condition.GE.toString(), ">="); + assertEquals(Condition.EQ.toString(), "="); + assertEquals(Condition.NE.toString(), "not ="); + + /* + * repeat call to get coverage of value caching + */ + assertEquals(Condition.NE.toString(), "not ="); + } +} diff --git a/test/jalview/util/matcher/KeyedMatcherSetTest.java b/test/jalview/util/matcher/KeyedMatcherSetTest.java new file mode 100644 index 0000000..0d2767d --- /dev/null +++ b/test/jalview/util/matcher/KeyedMatcherSetTest.java @@ -0,0 +1,124 @@ +package jalview.util.matcher; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.function.Function; + +import org.testng.annotations.Test; + +public class KeyedMatcherSetTest +{ + @Test + public void testMatches() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F); + KeyedMatcherSetI kms = new KeyedMatcherSet(); + kms.and(km); + assertTrue(kms.matches(key -> "-2")); + assertTrue(kms.matches(key -> "-1")); + assertFalse(kms.matches(key -> "-3")); + assertFalse(kms.matches(key -> "")); + assertFalse(kms.matches(key -> "junk")); + assertFalse(kms.matches(key -> null)); + + /* + * a string pattern matcher + */ + km = new KeyedMatcher("AF", Condition.Contains, "Cat"); + kms = new KeyedMatcherSet(); + kms.and(km); + assertTrue(kms + .matches(key -> "AF".equals(key) ? "raining cats and dogs" + : "showers")); + } + + @Test + public void testAnd() + { + // condition1: AF value contains "dog" (matches) + KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog"); + // condition 2: CSQ value does not contain "how" (does not match) + KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains, + "how"); + + Function vp = key -> "AF".equals(key) ? "raining cats and dogs" + : "showers"; + assertTrue(km1.matches(vp)); + assertFalse(km2.matches(vp)); + + KeyedMatcherSetI kms = new KeyedMatcherSet(); + assertTrue(kms.matches(vp)); // if no conditions, then 'all' pass + kms.and(km1); + assertTrue(kms.matches(vp)); + kms.and(km2); + assertFalse(kms.matches(vp)); + } + + @Test + public void testToString() + { + KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.LT, 1.2f); + assertEquals(km1.toString(), "AF < 1.2"); + + KeyedMatcher km2 = new KeyedMatcher("CLIN_SIG", Condition.NotContains, "path"); + assertEquals(km2.toString(), "CLIN_SIG Does not contain PATH"); + + /* + * AND them + */ + KeyedMatcherSetI kms = new KeyedMatcherSet(); + assertEquals(kms.toString(), ""); + kms.and(km1); + assertEquals(kms.toString(), "(AF < 1.2)"); + kms.and(km2); + assertEquals(kms.toString(), + "(AF < 1.2) AND (CLIN_SIG Does not contain PATH)"); + + /* + * OR them + */ + kms = new KeyedMatcherSet(); + assertEquals(kms.toString(), ""); + kms.or(km1); + assertEquals(kms.toString(), "(AF < 1.2)"); + kms.or(km2); + assertEquals(kms.toString(), + "(AF < 1.2) OR (CLIN_SIG Does not contain PATH)"); + } + + @Test + public void testOr() + { + // condition1: AF value contains "dog" (matches) + KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog"); + // condition 2: CSQ value does not contain "how" (does not match) + KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains, + "how"); + + Function vp = key -> "AF".equals(key) ? "raining cats and dogs" + : "showers"; + assertTrue(km1.matches(vp)); + assertFalse(km2.matches(vp)); + + KeyedMatcherSetI kms = new KeyedMatcherSet(); + kms.or(km2); + assertFalse(kms.matches(vp)); + kms.or(km1); + assertTrue(kms.matches(vp)); + } + + @Test + public void testIsEmpty() + { + KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F); + KeyedMatcherSetI kms = new KeyedMatcherSet(); + assertTrue(kms.isEmpty()); + kms.and(km); + assertFalse(kms.isEmpty()); + } +} diff --git a/test/jalview/util/matcher/KeyedMatcherTest.java b/test/jalview/util/matcher/KeyedMatcherTest.java new file mode 100644 index 0000000..164b8eb --- /dev/null +++ b/test/jalview/util/matcher/KeyedMatcherTest.java @@ -0,0 +1,58 @@ +package jalview.util.matcher; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import org.testng.annotations.Test; + +public class KeyedMatcherTest +{ + @Test + public void testMatches() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F); + assertTrue(km.matches(key -> "-2")); + assertTrue(km.matches(key -> "-1")); + assertFalse(km.matches(key -> "-3")); + assertFalse(km.matches(key -> "")); + assertFalse(km.matches(key -> "junk")); + assertFalse(km.matches(key -> null)); + + /* + * a string pattern matcher + */ + km = new KeyedMatcher("AF", Condition.Contains, "Cat"); + assertTrue(km.matches(key -> "AF".equals(key) ? "raining cats and dogs" + : "showers")); + } + + @Test + public void testToString() + { + /* + * toString uses the i18n translation of the enum conditions + */ + KeyedMatcherI km = new KeyedMatcher("AF", Condition.LT, 1.2f); + assertEquals(km.toString(), "AF < 1.2"); + } + + @Test + public void testGetKey() + { + KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F); + assertEquals(km.getKey(), "AF"); + } + + @Test + public void testGetMatcher() + { + KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F); + assertEquals(km.getMatcher().getCondition(), Condition.GE); + assertEquals(km.getMatcher().getFloatValue(), -2F); + assertEquals(km.getMatcher().getPattern(), "-2.0"); + } +} diff --git a/test/jalview/util/matcher/MatcherTest.java b/test/jalview/util/matcher/MatcherTest.java new file mode 100644 index 0000000..d988c3a --- /dev/null +++ b/test/jalview/util/matcher/MatcherTest.java @@ -0,0 +1,250 @@ +package jalview.util.matcher; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import org.testng.annotations.Test; + +public class MatcherTest +{ + @Test + public void testConstructor() + { + MatcherI m = new Matcher(Condition.Contains, "foo"); + assertEquals(m.getCondition(), Condition.Contains); + assertEquals(m.getPattern(), "FOO"); // all comparisons upper-cased + assertEquals(m.getFloatValue(), 0f); + + m = new Matcher(Condition.GT, -2.1f); + assertEquals(m.getCondition(), Condition.GT); + assertEquals(m.getPattern(), "-2.1"); + assertEquals(m.getFloatValue(), -2.1f); + + m = new Matcher(Condition.NotContains, "-1.2f"); + assertEquals(m.getCondition(), Condition.NotContains); + assertEquals(m.getPattern(), "-1.2F"); + assertEquals(m.getFloatValue(), 0f); + + m = new Matcher(Condition.GE, "-1.2f"); + assertEquals(m.getCondition(), Condition.GE); + assertEquals(m.getPattern(), "-1.2"); + assertEquals(m.getFloatValue(), -1.2f); + + try + { + new Matcher(null, 0f); + fail("Expected exception"); + } catch (NullPointerException e) + { + // expected + } + + try + { + new Matcher(Condition.LT, "123,456"); + fail("Expected exception"); + } catch (NumberFormatException e) + { + // expected + } + } + + /** + * Tests for float comparison conditions + */ + @Test + public void testMatches_float() + { + /* + * EQUALS test + */ + MatcherI m = new Matcher(Condition.EQ, 2f); + assertTrue(m.matches("2")); + assertTrue(m.matches("2.0")); + assertFalse(m.matches("2.01")); + + /* + * NOT EQUALS test + */ + m = new Matcher(Condition.NE, 2f); + assertFalse(m.matches("2")); + assertFalse(m.matches("2.0")); + assertTrue(m.matches("2.01")); + + /* + * >= test + */ + m = new Matcher(Condition.GE, 2f); + assertTrue(m.matches("2")); + assertTrue(m.matches("2.1")); + assertFalse(m.matches("1.9")); + + /* + * > test + */ + m = new Matcher(Condition.GT, 2f); + assertFalse(m.matches("2")); + assertTrue(m.matches("2.1")); + assertFalse(m.matches("1.9")); + + /* + * <= test + */ + m = new Matcher(Condition.LE, 2f); + assertTrue(m.matches("2")); + assertFalse(m.matches("2.1")); + assertTrue(m.matches("1.9")); + + /* + * < test + */ + m = new Matcher(Condition.LT, 2f); + assertFalse(m.matches("2")); + assertFalse(m.matches("2.1")); + assertTrue(m.matches("1.9")); + } + + @Test + public void testMatches_floatNullOrInvalid() + { + for (Condition cond : Condition.values()) + { + if (cond.isNumeric()) + { + MatcherI m = new Matcher(cond, 2f); + assertFalse(m.matches(null)); + assertFalse(m.matches("")); + assertFalse(m.matches("two")); + } + } + } + + /** + * Tests for string comparison conditions + */ + @Test + public void testMatches_pattern() + { + /* + * Contains + */ + MatcherI m = new Matcher(Condition.Contains, "benign"); + assertTrue(m.matches("benign")); + assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive + assertFalse(m.matches("pathogenic")); + assertFalse(m.matches(null)); + + /* + * does not contain + */ + m = new Matcher(Condition.NotContains, "benign"); + assertFalse(m.matches("benign")); + assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive + assertTrue(m.matches("pathogenic")); + assertTrue(m.matches(null)); // null value passes this condition + + /* + * matches + */ + m = new Matcher(Condition.Matches, "benign"); + assertTrue(m.matches("benign")); + assertTrue(m.matches(" Benign ")); // trim before testing + assertFalse(m.matches("MOSTLY BENIGN")); + assertFalse(m.matches("pathogenic")); + assertFalse(m.matches(null)); + + /* + * does not match + */ + m = new Matcher(Condition.NotMatches, "benign"); + assertFalse(m.matches("benign")); + assertFalse(m.matches(" Benign ")); // trim before testing + assertTrue(m.matches("MOSTLY BENIGN")); + assertTrue(m.matches("pathogenic")); + assertTrue(m.matches(null)); + + /* + * a float with a string match condition will be treated as string + */ + Matcher m1 = new Matcher(Condition.Contains, "32"); + assertFalse(m1.matches(-203f)); + assertTrue(m1.matches(-4321.0f)); + } + + /** + * If a float is passed with a string condition it gets converted to a string + */ + @Test + public void testMatches_floatWithStringCondition() + { + MatcherI m = new Matcher(Condition.Contains, 1.2e-6f); + assertTrue(m.matches("1.2e-6")); + + m = new Matcher(Condition.Contains, 0.0000001f); + assertTrue(m.matches("1.0e-7")); + assertTrue(m.matches("1.0E-7")); + assertFalse(m.matches("0.0000001f")); + } + + @Test + public void testToString() + { + MatcherI m = new Matcher(Condition.LT, 1.2e-6f); + assertEquals(m.toString(), "LT 1.2E-6"); + + m = new Matcher(Condition.NotMatches, "ABC"); + assertEquals(m.toString(), "NotMatches ABC"); + + m = new Matcher(Condition.Contains, -1.2f); + assertEquals(m.toString(), "Contains -1.2"); + } + + @Test + public void testEquals() + { + /* + * string condition + */ + MatcherI m = new Matcher(Condition.NotMatches, "ABC"); + assertFalse(m.equals(null)); + assertFalse(m.equals("foo")); + assertTrue(m.equals(m)); + assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC"))); + // not case-sensitive: + assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc"))); + assertFalse(m.equals(new Matcher(Condition.Matches, "ABC"))); + assertFalse(m.equals(new Matcher(Condition.NotMatches, "def"))); + + /* + * numeric conditions + */ + m = new Matcher(Condition.LT, -1f); + assertFalse(m.equals(null)); + assertFalse(m.equals("foo")); + assertTrue(m.equals(m)); + assertTrue(m.equals(new Matcher(Condition.LT, -1f))); + assertTrue(m.equals(new Matcher(Condition.LT, "-1f"))); + assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f"))); + assertFalse(m.equals(new Matcher(Condition.LE, -1f))); + assertFalse(m.equals(new Matcher(Condition.GE, -1f))); + assertFalse(m.equals(new Matcher(Condition.NE, -1f))); + assertFalse(m.equals(new Matcher(Condition.LT, 1f))); + assertFalse(m.equals(new Matcher(Condition.LT, -1.1f))); + } + + @Test + public void testHashCode() + { + MatcherI m1 = new Matcher(Condition.NotMatches, "ABC"); + MatcherI m2 = new Matcher(Condition.NotMatches, "ABC"); + MatcherI m3 = new Matcher(Condition.NotMatches, "AB"); + MatcherI m4 = new Matcher(Condition.Matches, "ABC"); + assertEquals(m1.hashCode(), m2.hashCode()); + assertNotEquals(m1.hashCode(), m3.hashCode()); + assertNotEquals(m1.hashCode(), m4.hashCode()); + assertNotEquals(m3.hashCode(), m4.hashCode()); + } +}