From c866d09f8d85f9b93d07239495d04f180b2ff61b Mon Sep 17 00:00:00 2001 From: Florian Kargl Date: Sun, 28 Aug 2022 19:35:01 +0200 Subject: [PATCH] Add tag list popup menu actions to create filters for selected tags Add two menu items to the tag list dialog popup menu that create filtersto either hide or exclusively show objects with selected tags. If an equivalent filter is already present in the filter list, re-uses and enables that one instead. --- .../dialogs/properties/PropertiesDialog.java | 155 ++++++++++++-- .../gui/dialogs/properties/TagEditHelper.java | 12 ++ .../PropertiesDialogFilterActionTest.java | 197 ++++++++++++++++++ 3 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialogFilterActionTest.java diff --git a/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java b/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java index 5fd62166f4e..ec4dddc656e 100644 --- a/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java +++ b/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java @@ -2,6 +2,7 @@ package org.openstreetmap.josm.gui.dialogs.properties; import static org.openstreetmap.josm.tools.I18n.tr; +import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.Component; import java.awt.Container; @@ -22,11 +23,13 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.OptionalInt; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.swing.AbstractAction; import javax.swing.JComponent; @@ -41,6 +44,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import javax.swing.event.RowSorterEvent; import javax.swing.event.RowSorterListener; import javax.swing.table.DefaultTableCellRenderer; @@ -63,6 +67,7 @@ import org.openstreetmap.josm.data.osm.DataSelectionListener; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.DefaultNameFormatter; +import org.openstreetmap.josm.data.osm.Filter; import org.openstreetmap.josm.data.osm.IPrimitive; import org.openstreetmap.josm.data.osm.IRelation; import org.openstreetmap.josm.data.osm.IRelationMember; @@ -90,6 +95,7 @@ import org.openstreetmap.josm.gui.PopupMenuHandler; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; +import org.openstreetmap.josm.gui.dialogs.FilterTableModel; import org.openstreetmap.josm.gui.dialogs.ToggleDialog; import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; import org.openstreetmap.josm.gui.dialogs.relation.RelationPopupMenus; @@ -211,6 +217,8 @@ public class PropertiesDialog extends ToggleDialog tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection).registerShortcut(); /* NO-SHORTCUT */ private final SearchAction searchActionSame = new SearchAction(true); private final SearchAction searchActionAny = new SearchAction(false); + private final FilterAction filterActionHide = new FilterAction(true); + private final FilterAction filterActionShowOnly = new FilterAction(false); private final AddAction addAction = new AddAction(); private final EditAction editAction = new EditAction(); private final DeleteAction deleteAction = new DeleteAction(); @@ -465,6 +473,10 @@ private void setupTagsMenu() { tagMenu.addSeparator(); tagMenu.add(searchActionAny); tagMenu.add(searchActionSame); + tagMenu.add(filterActionHide); + tagMenu.addPopupMenuListener(filterActionHide); + tagMenu.add(filterActionShowOnly); + tagMenu.addPopupMenuListener(filterActionShowOnly); tagMenu.addSeparator(); tagMenu.add(helpTagAction); tagMenu.add(tagHistoryAction); @@ -1347,38 +1359,137 @@ public void actionPerformed(ActionEvent e) { } } - static SearchSetting createSearchSetting(String key, Collection sel, boolean sameType) { - String sep = ""; - StringBuilder s = new StringBuilder(); - Set consideredTokens = new TreeSet<>(); - for (IPrimitive p : sel) { - String val = p.get(key); - if (val == null || (!sameType && consideredTokens.contains(val))) { - continue; + class FilterAction extends AbstractAction implements PopupMenuListener { + + private final boolean hideMatching; + + FilterAction(boolean hideMatching) { + setName(hideMatching, 0); + putValue(SHORT_DESCRIPTION, hideMatching ? tr("Add a filter that hides objects with the selected tags") : + tr("Add a filter that shows only objects with the selected tags")); + new ImageProvider("dialogs/filter").getResource().attachImageIcon(this, true); + this.hideMatching = hideMatching; + } + + private void setName(boolean hideMatching, long n) { + if (hideMatching) { + putValue(NAME, trn("Hide objects with selected tag", "Hide objects with selected tags", n)); + } else { + putValue(NAME, trn("Show only objects with selected tag", "Show only objects with selected tags", n)); } - String t = ""; - if (!sameType) { - t = ""; - } else if (p instanceof Node) { - t = "type:node "; - } else if (p instanceof Way) { - t = "type:way "; - } else if (p instanceof Relation) { - t = "type:relation "; + } + + @Override + public void actionPerformed(ActionEvent e) { + Filter newFilter = createFilterForSelectedTags(); + if (newFilter == null) + return; + + FilterTableModel filterModel = MainApplication.getMap().filterDialog.getFilterModel(); + + OptionalInt existingFilterIndex = IntStream.range(0, filterModel.getRowCount()) + .filter(i -> { + Filter f = filterModel.getValue(i); + return f.equals(newFilter) && f.inverted == !hideMatching; + }).findFirst(); + + if (existingFilterIndex.isPresent()) { + Logging.debug("Enabling existing equivalent filter: {0}", existingFilterIndex); + filterModel.setValueAt(true, existingFilterIndex.getAsInt(), FilterTableModel.COL_ENABLED); + } else { + newFilter.inverted = !hideMatching; + Logging.debug("Adding new filter {0}", newFilter); + filterModel.addFilter(newFilter); + } + } + + private Filter createFilterForSelectedTags() { + List keys = editHelper.getDataKeys(tagTable.getSelectedRows()); + Collection sel = OsmDataManager.getInstance().getInProgressISelection(); + + return createFilterForKeys(keys, sel); + } + + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + setName(hideMatching, tagTable.getSelectedRows().length); + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + // Do nothing + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // Do nothing + } + } + + static SearchSetting createSearchSetting(String key, Collection sel, boolean sameType) { + return createSearchSetting(Collections.singletonList(key), sel, sameType); + } + + static SearchSetting createSearchSetting(Collection keys, Collection sel, boolean sameType) { + StringBuilder complete = new StringBuilder(); + String keySep = ""; + for (String key : keys) { + StringBuilder keyString = new StringBuilder(); + Set consideredTokens = new TreeSet<>(); + String valueSep = ""; + int valueCount = 0; + + for (IPrimitive p : sel) { + String val = p.get(key); + if (val == null || (!sameType && consideredTokens.contains(val))) { + continue; + } + String t = ""; + if (!sameType) { + t = ""; + } else if (p instanceof Node) { + t = "type:node "; + } else if (p instanceof Way) { + t = "type:way "; + } else if (p instanceof Relation) { + t = "type:relation "; + } + String token = new StringBuilder(t).append(val).toString(); + if (consideredTokens.add(token)) { + keyString.append(valueSep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')'); + valueSep = " OR "; + valueCount++; + } } - String token = new StringBuilder(t).append(val).toString(); - if (consideredTokens.add(token)) { - s.append(sep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')'); - sep = " OR "; + + if (keys.size() > 1) { + complete.append(keySep); + keySep = " AND "; + + if (valueCount > 1) { + complete.append('(').append(keyString).append(')'); + } else { + complete.append(keyString); + } + } else { + complete.append(keyString); } } final SearchSetting ss = new SearchSetting(); - ss.text = s.toString(); + ss.text = complete.toString(); ss.caseSensitive = true; return ss; } + static Filter createFilterForKeys(List keys, Collection prims) { + if (prims == null || prims.isEmpty()) + return null; + + final SearchSetting ss = createSearchSetting(keys, prims, false); + return new Filter(ss); + } + /** * Clears the row selection when it is filtered away by the row sorter. */ diff --git a/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java b/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java index 7e18f27d3e7..4ff8d9f953b 100644 --- a/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java +++ b/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java @@ -243,6 +243,18 @@ public final String getDataKey(int viewRow) { return tagData.getValueAt(tagTable.convertRowIndexToModel(viewRow), 0).toString(); } + /** + * Finds the keys for given rows of tag editor. + * @param viewRows index of rows + * @return keys of tags + * @since xxx + */ + public final List getDataKeys(int[] viewRows) { + return Arrays.stream(viewRows).boxed() + .map(r -> tagData.getValueAt(tagTable.convertRowIndexToModel(r), 0).toString()) + .collect(Collectors.toList()); + } + /** * Determines if the given tag key is already used (by all selected primitives, not just some of them) * @param key the key to check diff --git a/test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialogFilterActionTest.java b/test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialogFilterActionTest.java new file mode 100644 index 00000000000..8c202bf1064 --- /dev/null +++ b/test/unit/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialogFilterActionTest.java @@ -0,0 +1,197 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.gui.dialogs.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.swing.JTable; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.openstreetmap.josm.data.coor.EastNorth; +import org.openstreetmap.josm.data.osm.DataSet; +import org.openstreetmap.josm.data.osm.Filter; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.OsmPrimitive; +import org.openstreetmap.josm.data.osm.Way; +import org.openstreetmap.josm.data.osm.search.SearchSetting; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.gui.dialogs.FilterDialog; +import org.openstreetmap.josm.gui.dialogs.FilterTableModel; +import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog.FilterAction; +import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog.ReadOnlyTableModel; +import org.openstreetmap.josm.gui.layer.OsmDataLayer; +import org.openstreetmap.josm.testutils.JOSMTestRules; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Tests of {@link PropertiesDialog.FilterAction} class + */ +public class PropertiesDialogFilterActionTest { + + /** + * Setup tests + */ + @RegisterExtension + @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") + public JOSMTestRules test = new JOSMTestRules().main().projection().preferences(); + + @Test + void createFilterForKeysTest() { + List prims = new ArrayList<>(); + Node n = new Node(); + n.put("foo", "bar"); + Way w = new Way(); + w.put("foo", "baz"); + w.put("blah", "blub"); + prims.add(n); + prims.add(w); + + Filter filter = PropertiesDialog.createFilterForKeys(Arrays.asList("foo", "blah"), prims); + SearchSetting expectedSearchSetting = new SearchSetting(); + expectedSearchSetting.text = "((\"foo\"=\"bar\") OR (\"foo\"=\"baz\")) AND (\"blah\"=\"blub\")"; + expectedSearchSetting.caseSensitive = true; + Filter expected = new Filter(expectedSearchSetting); + assertEquals(0, filter.compareTo(expected)); + } + + @Test + void filterActionHideTest() { + DataSet ds = new DataSet(); + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + MapFrame mf = MainApplication.getMap(); + PropertiesDialog propertiesDialog = mf.propertiesDialog; + FilterDialog filterDialog = mf.filterDialog; + FilterTableModel filterModel = filterDialog.getFilterModel(); + + Node n = new Node(new EastNorth(0, 0)); + n.put("natural", "tree"); + n.put("leaf_type", "broadleaved"); + ds.addPrimitive(n); + + Node n2 = new Node(new EastNorth(0, 0)); + n2.put("natural", "tree"); + n2.put("leaf_type", "needleleaved"); + ds.addPrimitive(n2); + + Node n3 = new Node(new EastNorth(0, 0)); + n3.put("natural", "tree"); + ds.addPrimitive(n3); + + try { + Field filterActionHideField = PropertiesDialog.class.getDeclaredField("filterActionHide"); + filterActionHideField.setAccessible(true); + FilterAction filterActionHide = (PropertiesDialog.FilterAction) filterActionHideField.get(propertiesDialog); + + Field tagTableField = PropertiesDialog.class.getDeclaredField("tagTable"); + tagTableField.setAccessible(true); + JTable tagTable = (JTable) tagTableField.get(propertiesDialog); + + Field tagDataField = PropertiesDialog.class.getDeclaredField("tagData"); + tagDataField.setAccessible(true); + ReadOnlyTableModel tagData = (ReadOnlyTableModel) tagDataField.get(propertiesDialog); + + assertEquals(0, tagData.getRowCount()); + ds.setSelected(n, n2); + assertEquals(2, tagData.getRowCount()); + + tagTable.selectAll(); + + assertEquals(0, filterModel.getFilters().size()); + filterActionHide.actionPerformed(null); + assertEquals(1, filterModel.getFilters().size()); + + SearchSetting expectedSearchSetting = new SearchSetting(); + expectedSearchSetting.text = "((\"leaf_type\"=\"broadleaved\") OR (\"leaf_type\"=\"needleleaved\")) AND (\"natural\"=\"tree\")"; + expectedSearchSetting.caseSensitive = true; + Filter expectedFilter = new Filter(expectedSearchSetting); + + Filter createdFilter = filterModel.getFilters().get(0); + + assertEquals(expectedFilter, createdFilter); + assertEquals(0, createdFilter.compareTo(expectedFilter)); + assertEquals(true, createdFilter.enable); + + assertEquals(true, n.isDisabled()); + assertEquals(true, n2.isDisabled()); + assertEquals(false, n3.isDisabled()); + } catch (Exception e) { + fail("Should not throw", e); + } + } + + @Test + void filterActionShowOnlyTest() { + DataSet ds = new DataSet(); + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + MapFrame mf = MainApplication.getMap(); + PropertiesDialog propertiesDialog = mf.propertiesDialog; + FilterDialog filterDialog = mf.filterDialog; + FilterTableModel filterModel = filterDialog.getFilterModel(); + + Node n = new Node(new EastNorth(0, 0)); + n.put("natural", "tree"); + n.put("leaf_type", "broadleaved"); + ds.addPrimitive(n); + + Node n2 = new Node(new EastNorth(0, 0)); + n2.put("natural", "tree"); + n2.put("leaf_type", "needleleaved"); + ds.addPrimitive(n2); + + Node n3 = new Node(new EastNorth(0, 0)); + n3.put("natural", "tree"); + ds.addPrimitive(n3); + + try { + Field filterActionShowOnlyField = PropertiesDialog.class.getDeclaredField("filterActionShowOnly"); + filterActionShowOnlyField.setAccessible(true); + FilterAction filterActionShowOnly = (PropertiesDialog.FilterAction) filterActionShowOnlyField.get(propertiesDialog); + + Field tagTableField = PropertiesDialog.class.getDeclaredField("tagTable"); + tagTableField.setAccessible(true); + JTable tagTable = (JTable) tagTableField.get(propertiesDialog); + + Field tagDataField = PropertiesDialog.class.getDeclaredField("tagData"); + tagDataField.setAccessible(true); + ReadOnlyTableModel tagData = (ReadOnlyTableModel) tagDataField.get(propertiesDialog); + + assertEquals(0, tagData.getRowCount()); + ds.setSelected(n, n2); + assertEquals(2, tagData.getRowCount()); + + tagTable.selectAll(); + + assertEquals(0, filterModel.getFilters().size()); + filterActionShowOnly.actionPerformed(null); + assertEquals(1, filterModel.getFilters().size()); + + SearchSetting expectedSearchSetting = new SearchSetting(); + expectedSearchSetting.text = "((\"leaf_type\"=\"broadleaved\") OR (\"leaf_type\"=\"needleleaved\")) AND (\"natural\"=\"tree\")"; + expectedSearchSetting.caseSensitive = true; + Filter expectedFilter = new Filter(expectedSearchSetting); + expectedFilter.inverted = true; + + Filter createdFilter = filterModel.getFilters().get(0); + + assertEquals(expectedFilter, createdFilter); + assertEquals(0, createdFilter.compareTo(expectedFilter)); + assertEquals(true, createdFilter.enable); + + assertEquals(false, n.isDisabled()); + assertEquals(false, n2.isDisabled()); + assertEquals(true, n3.isDisabled()); + } catch (Exception e) { + fail("Should not throw", e); + } + } +}