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); + } + } +}