001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Graphics2D;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.awt.event.MouseEvent;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.HashSet;
015import java.util.List;
016import java.util.Set;
017import java.util.Stack;
018
019import javax.swing.AbstractAction;
020import javax.swing.JCheckBox;
021import javax.swing.JTable;
022import javax.swing.ListSelectionModel;
023import javax.swing.SwingUtilities;
024import javax.swing.table.DefaultTableCellRenderer;
025import javax.swing.table.JTableHeader;
026import javax.swing.table.TableCellRenderer;
027import javax.swing.table.TableColumnModel;
028import javax.swing.table.TableModel;
029
030import org.openstreetmap.josm.Main;
031import org.openstreetmap.josm.actions.search.SearchAction;
032import org.openstreetmap.josm.data.osm.Filter;
033import org.openstreetmap.josm.data.osm.OsmPrimitive;
034import org.openstreetmap.josm.data.osm.Relation;
035import org.openstreetmap.josm.data.osm.RelationMember;
036import org.openstreetmap.josm.data.osm.Way;
037import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
038import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
039import org.openstreetmap.josm.data.osm.event.DataSetListener;
040import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
041import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
042import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
043import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
044import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
045import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
046import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
047import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
048import org.openstreetmap.josm.gui.SideButton;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.InputMapUtils;
051import org.openstreetmap.josm.tools.MultikeyActionsHandler;
052import org.openstreetmap.josm.tools.MultikeyShortcutAction;
053import org.openstreetmap.josm.tools.Shortcut;
054
055/**
056 *
057 * @author Petr_DlouhĂ˝
058 */
059public class FilterDialog extends ToggleDialog implements DataSetListener {
060
061    private JTable userTable;
062    private final FilterTableModel filterModel = new FilterTableModel();
063
064    private final EnableFilterAction enableFilterAction;
065    private final HidingFilterAction hidingFilterAction;
066
067    /**
068     * Constructs a new {@code FilterDialog}
069     */
070    public FilterDialog() {
071        super(tr("Filter"), "filter", tr("Filter objects and hide/disable them."),
072                Shortcut.registerShortcut("subwindow:filter", tr("Toggle: {0}", tr("Filter")),
073                        KeyEvent.VK_F, Shortcut.ALT_SHIFT), 162);
074        build();
075        enableFilterAction = new EnableFilterAction();
076        hidingFilterAction = new HidingFilterAction();
077        MultikeyActionsHandler.getInstance().addAction(enableFilterAction);
078        MultikeyActionsHandler.getInstance().addAction(hidingFilterAction);
079    }
080
081    @Override
082    public void showNotify() {
083        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED);
084        filterModel.executeFilters();
085    }
086
087    @Override
088    public void hideNotify() {
089        DatasetEventManager.getInstance().removeDatasetListener(this);
090        filterModel.clearFilterFlags();
091        Main.map.mapView.repaint();
092    }
093
094    private static final Shortcut ENABLE_FILTER_SHORTCUT
095    = Shortcut.registerShortcut("core_multikey:enableFilter", tr("Multikey: {0}", tr("Enable filter")),
096            KeyEvent.VK_E, Shortcut.ALT_CTRL);
097
098    private static final Shortcut HIDING_FILTER_SHORTCUT
099    = Shortcut.registerShortcut("core_multikey:hidingFilter", tr("Multikey: {0}", tr("Hide filter")),
100            KeyEvent.VK_H, Shortcut.ALT_CTRL);
101
102    private static final String[] COLUMN_TOOLTIPS = {
103            Main.platform.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT),
104            Main.platform.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT),
105            null,
106            tr("Inverse filter"),
107            tr("Filter mode")
108    };
109
110    protected void build() {
111        userTable = new UserTable(filterModel);
112
113        userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
114
115        userTable.getColumnModel().getColumn(0).setMaxWidth(1);
116        userTable.getColumnModel().getColumn(1).setMaxWidth(1);
117        userTable.getColumnModel().getColumn(3).setMaxWidth(1);
118        userTable.getColumnModel().getColumn(4).setMaxWidth(1);
119
120        userTable.getColumnModel().getColumn(0).setResizable(false);
121        userTable.getColumnModel().getColumn(1).setResizable(false);
122        userTable.getColumnModel().getColumn(3).setResizable(false);
123        userTable.getColumnModel().getColumn(4).setResizable(false);
124
125        userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer());
126        userTable.setDefaultRenderer(String.class, new StringRenderer());
127
128        SideButton addButton = new SideButton(new AbstractAction() {
129            {
130                putValue(NAME, tr("Add"));
131                putValue(SHORT_DESCRIPTION, tr("Add filter."));
132                new ImageProvider("dialogs", "add").getResource().attachImageIcon(this, true);
133            }
134
135            @Override
136            public void actionPerformed(ActionEvent e) {
137                Filter filter = (Filter) SearchAction.showSearchDialog(new Filter());
138                if (filter != null) {
139                    filterModel.addFilter(filter);
140                }
141            }
142        });
143        SideButton editButton = new SideButton(new AbstractAction() {
144            {
145                putValue(NAME, tr("Edit"));
146                putValue(SHORT_DESCRIPTION, tr("Edit filter."));
147                new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
148            }
149
150            @Override
151            public void actionPerformed(ActionEvent e) {
152                int index = userTable.getSelectionModel().getMinSelectionIndex();
153                if (index < 0) return;
154                Filter f = filterModel.getFilter(index);
155                Filter filter = (Filter) SearchAction.showSearchDialog(f);
156                if (filter != null) {
157                    filterModel.setFilter(index, filter);
158                }
159            }
160        });
161        SideButton deleteButton = new SideButton(new AbstractAction() {
162            {
163                putValue(NAME, tr("Delete"));
164                putValue(SHORT_DESCRIPTION, tr("Delete filter."));
165                new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true);
166            }
167
168            @Override
169            public void actionPerformed(ActionEvent e) {
170                int index = userTable.getSelectionModel().getMinSelectionIndex();
171                if (index >= 0) {
172                    filterModel.removeFilter(index);
173                }
174            }
175        });
176        SideButton upButton = new SideButton(new AbstractAction() {
177            {
178                putValue(NAME, tr("Up"));
179                putValue(SHORT_DESCRIPTION, tr("Move filter up."));
180                new ImageProvider("dialogs", "up").getResource().attachImageIcon(this, true);
181            }
182
183            @Override
184            public void actionPerformed(ActionEvent e) {
185                int index = userTable.getSelectionModel().getMinSelectionIndex();
186                if (index >= 0) {
187                    filterModel.moveUpFilter(index);
188                    userTable.getSelectionModel().setSelectionInterval(index-1, index-1);
189                }
190            }
191        });
192        SideButton downButton = new SideButton(new AbstractAction() {
193            {
194                putValue(NAME, tr("Down"));
195                putValue(SHORT_DESCRIPTION, tr("Move filter down."));
196                new ImageProvider("dialogs", "down").getResource().attachImageIcon(this, true);
197            }
198
199            @Override
200            public void actionPerformed(ActionEvent e) {
201                int index = userTable.getSelectionModel().getMinSelectionIndex();
202                if (index >= 0) {
203                    filterModel.moveDownFilter(index);
204                    userTable.getSelectionModel().setSelectionInterval(index+1, index+1);
205                }
206            }
207        });
208
209        // Toggle filter "enabled" on Enter
210        InputMapUtils.addEnterAction(userTable, new AbstractAction() {
211            @Override
212            public void actionPerformed(ActionEvent e) {
213                int index = userTable.getSelectedRow();
214                if (index >= 0) {
215                    Filter filter = filterModel.getFilter(index);
216                    filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
217                }
218            }
219        });
220
221        // Toggle filter "hiding" on Spacebar
222        InputMapUtils.addSpacebarAction(userTable, new AbstractAction() {
223            @Override
224            public void actionPerformed(ActionEvent e) {
225                int index = userTable.getSelectedRow();
226                if (index >= 0) {
227                    Filter filter = filterModel.getFilter(index);
228                    filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
229                }
230            }
231        });
232
233        createLayout(userTable, true, Arrays.asList(new SideButton[] {
234                addButton, editButton, deleteButton, upButton, downButton
235        }));
236    }
237
238    @Override
239    public void destroy() {
240        MultikeyActionsHandler.getInstance().removeAction(enableFilterAction);
241        MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction);
242        super.destroy();
243    }
244
245    static final class UserTable extends JTable {
246        static final class UserTableHeader extends JTableHeader {
247            UserTableHeader(TableColumnModel cm) {
248                super(cm);
249            }
250
251            @Override
252            public String getToolTipText(MouseEvent e) {
253                int index = columnModel.getColumnIndexAtX(e.getPoint().x);
254                int realIndex = columnModel.getColumn(index).getModelIndex();
255                return COLUMN_TOOLTIPS[realIndex];
256            }
257        }
258
259        UserTable(TableModel dm) {
260            super(dm);
261        }
262
263        @Override
264        protected JTableHeader createDefaultTableHeader() {
265            return new UserTableHeader(columnModel);
266        }
267    }
268
269    static class StringRenderer extends DefaultTableCellRenderer {
270        @Override
271        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
272            FilterTableModel model = (FilterTableModel) table.getModel();
273            Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
274            cell.setEnabled(model.isCellEnabled(row, column));
275            return cell;
276        }
277    }
278
279    static class BooleanRenderer extends JCheckBox implements TableCellRenderer {
280        @Override
281        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
282            FilterTableModel model = (FilterTableModel) table.getModel();
283            setSelected(value != null && (Boolean) value);
284            setEnabled(model.isCellEnabled(row, column));
285            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
286            return this;
287        }
288    }
289
290    public void updateDialogHeader() {
291        SwingUtilities.invokeLater(() -> setTitle(
292                tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount)));
293    }
294
295    public void drawOSDText(Graphics2D g) {
296        filterModel.drawOSDText(g);
297    }
298
299    /**
300     * Returns the list of primitives whose filtering can be affected by change in primitive
301     * @param primitives list of primitives to check
302     * @return List of primitives whose filtering can be affected by change in source primitives
303     */
304    private static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
305        // Filters can use nested parent/child expression so complete tree is necessary
306        Set<OsmPrimitive> result = new HashSet<>();
307        Stack<OsmPrimitive> stack = new Stack<>();
308        stack.addAll(primitives);
309
310        while (!stack.isEmpty()) {
311            OsmPrimitive p = stack.pop();
312
313            if (result.contains(p)) {
314                continue;
315            }
316
317            result.add(p);
318
319            if (p instanceof Way) {
320                for (OsmPrimitive n: ((Way) p).getNodes()) {
321                    stack.push(n);
322                }
323            } else if (p instanceof Relation) {
324                for (RelationMember rm: ((Relation) p).getMembers()) {
325                    stack.push(rm.getMember());
326                }
327            }
328
329            for (OsmPrimitive ref: p.getReferrers()) {
330                stack.push(ref);
331            }
332        }
333
334        return result;
335    }
336
337    @Override
338    public void dataChanged(DataChangedEvent event) {
339        filterModel.executeFilters();
340    }
341
342    @Override
343    public void nodeMoved(NodeMovedEvent event) {
344        filterModel.executeFilters();
345    }
346
347    @Override
348    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
349        filterModel.executeFilters();
350    }
351
352    @Override
353    public void primitivesAdded(PrimitivesAddedEvent event) {
354        filterModel.executeFilters(event.getPrimitives());
355    }
356
357    @Override
358    public void primitivesRemoved(PrimitivesRemovedEvent event) {
359        filterModel.executeFilters();
360    }
361
362    @Override
363    public void relationMembersChanged(RelationMembersChangedEvent event) {
364        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
365    }
366
367    @Override
368    public void tagsChanged(TagsChangedEvent event) {
369        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
370    }
371
372    @Override
373    public void wayNodesChanged(WayNodesChangedEvent event) {
374        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
375    }
376
377    /**
378     * This method is intendet for Plugins getting the filtermodel and using .addFilter() to
379     * add a new filter.
380     * @return the filtermodel
381     */
382    public FilterTableModel getFilterModel() {
383        return filterModel;
384    }
385
386    abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction {
387
388        protected transient Filter lastFilter;
389
390        @Override
391        public void actionPerformed(ActionEvent e) {
392            throw new UnsupportedOperationException();
393        }
394
395        @Override
396        public List<MultikeyInfo> getMultikeyCombinations() {
397            List<MultikeyInfo> result = new ArrayList<>();
398
399            for (int i = 0; i < filterModel.getRowCount(); i++) {
400                Filter filter = filterModel.getFilter(i);
401                MultikeyInfo info = new MultikeyInfo(i, filter.text);
402                result.add(info);
403            }
404
405            return result;
406        }
407
408        protected final boolean isLastFilterValid() {
409            return lastFilter != null && filterModel.getFilters().contains(lastFilter);
410        }
411
412        @Override
413        public MultikeyInfo getLastMultikeyAction() {
414            if (isLastFilterValid())
415                return new MultikeyInfo(-1, lastFilter.text);
416            else
417                return null;
418        }
419    }
420
421    private class EnableFilterAction extends AbstractFilterAction {
422
423        EnableFilterAction() {
424            putValue(SHORT_DESCRIPTION, tr("Enable filter"));
425            ENABLE_FILTER_SHORTCUT.setAccelerator(this);
426        }
427
428        @Override
429        public Shortcut getMultikeyShortcut() {
430            return ENABLE_FILTER_SHORTCUT;
431        }
432
433        @Override
434        public void executeMultikeyAction(int index, boolean repeatLastAction) {
435            if (index >= 0 && index < filterModel.getRowCount()) {
436                Filter filter = filterModel.getFilter(index);
437                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
438                lastFilter = filter;
439            } else if (repeatLastAction && isLastFilterValid()) {
440                filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED);
441            }
442        }
443    }
444
445    private class HidingFilterAction extends AbstractFilterAction {
446
447        HidingFilterAction() {
448            putValue(SHORT_DESCRIPTION, tr("Hiding filter"));
449            HIDING_FILTER_SHORTCUT.setAccelerator(this);
450        }
451
452        @Override
453        public Shortcut getMultikeyShortcut() {
454            return HIDING_FILTER_SHORTCUT;
455        }
456
457        @Override
458        public void executeMultikeyAction(int index, boolean repeatLastAction) {
459            if (index >= 0 && index < filterModel.getRowCount()) {
460                Filter filter = filterModel.getFilter(index);
461                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
462                lastFilter = filter;
463            } else if (repeatLastAction && isLastFilterValid()) {
464                filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING);
465            }
466        }
467    }
468}