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