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    protected final String[] columnToolTips = {
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 columnToolTips[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) return;
183                filterModel.removeFilter(index);
184            }
185        });
186        SideButton upButton = new SideButton(new AbstractAction() {
187            {
188                putValue(NAME, tr("Up"));
189                putValue(SHORT_DESCRIPTION, tr("Move filter up."));
190                putValue(SMALL_ICON, ImageProvider.get("dialogs", "up"));
191            }
192
193            @Override
194            public void actionPerformed(ActionEvent e) {
195                int index = userTable.getSelectionModel().getMinSelectionIndex();
196                if (index < 0) return;
197                filterModel.moveUpFilter(index);
198                userTable.getSelectionModel().setSelectionInterval(index-1, index-1);
199            }
200
201        });
202        SideButton downButton = new SideButton(new AbstractAction() {
203            {
204                putValue(NAME, tr("Down"));
205                putValue(SHORT_DESCRIPTION, tr("Move filter down."));
206                putValue(SMALL_ICON, ImageProvider.get("dialogs", "down"));
207            }
208
209            @Override
210            public void actionPerformed(ActionEvent e) {
211                int index = userTable.getSelectionModel().getMinSelectionIndex();
212                if (index < 0) return;
213                filterModel.moveDownFilter(index);
214                userTable.getSelectionModel().setSelectionInterval(index+1, index+1);
215            }
216        });
217
218        // Toggle filter "enabled" on Enter
219        InputMapUtils.addEnterAction(userTable, new AbstractAction() {
220            @Override
221            public void actionPerformed(ActionEvent e) {
222                int index = userTable.getSelectedRow();
223                if (index < 0) return;
224                Filter filter = filterModel.getFilter(index);
225                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
226            }
227        });
228
229        // Toggle filter "hiding" on Spacebar
230        InputMapUtils.addSpacebarAction(userTable, new AbstractAction() {
231            @Override
232            public void actionPerformed(ActionEvent e) {
233                int index = userTable.getSelectedRow();
234                if (index < 0) return;
235                Filter filter = filterModel.getFilter(index);
236                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
237            }
238        });
239
240        createLayout(userTable, true, Arrays.asList(new SideButton[] {
241                addButton, editButton, deleteButton, upButton, downButton
242        }));
243    }
244
245    @Override
246    public void destroy() {
247        MultikeyActionsHandler.getInstance().removeAction(enableFilterAction);
248        MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction);
249        super.destroy();
250    }
251
252    static class StringRenderer extends DefaultTableCellRenderer {
253        @Override
254        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
255            FilterTableModel model = (FilterTableModel) table.getModel();
256            Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
257            cell.setEnabled(model.isCellEnabled(row, column));
258            return cell;
259        }
260    }
261
262    static class BooleanRenderer extends JCheckBox implements TableCellRenderer {
263        @Override
264        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
265            FilterTableModel model = (FilterTableModel) table.getModel();
266            setSelected(value != null && (Boolean) value);
267            setEnabled(model.isCellEnabled(row, column));
268            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
269            return this;
270        }
271    }
272
273    public void updateDialogHeader() {
274        SwingUtilities.invokeLater(new Runnable() {
275            @Override
276            public void run() {
277                setTitle(tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount));
278            }
279        });
280    }
281
282    public void drawOSDText(Graphics2D g) {
283        filterModel.drawOSDText(g);
284    }
285
286    /**
287     * Returns the list of primitives whose filtering can be affected by change in primitive
288     * @param primitives list of primitives to check
289     * @return List of primitives whose filtering can be affected by change in source primitives
290     */
291    private static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
292        // Filters can use nested parent/child expression so complete tree is necessary
293        Set<OsmPrimitive> result = new HashSet<>();
294        Stack<OsmPrimitive> stack = new Stack<>();
295        stack.addAll(primitives);
296
297        while (!stack.isEmpty()) {
298            OsmPrimitive p = stack.pop();
299
300            if (result.contains(p)) {
301                continue;
302            }
303
304            result.add(p);
305
306            if (p instanceof Way) {
307                for (OsmPrimitive n: ((Way) p).getNodes()) {
308                    stack.push(n);
309                }
310            } else if (p instanceof Relation) {
311                for (RelationMember rm: ((Relation) p).getMembers()) {
312                    stack.push(rm.getMember());
313                }
314            }
315
316            for (OsmPrimitive ref: p.getReferrers()) {
317                stack.push(ref);
318            }
319        }
320
321        return result;
322    }
323
324    @Override
325    public void dataChanged(DataChangedEvent event) {
326        filterModel.executeFilters();
327    }
328
329    @Override
330    public void nodeMoved(NodeMovedEvent event) {
331        // Do nothing
332    }
333
334    @Override
335    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
336        filterModel.executeFilters();
337    }
338
339    @Override
340    public void primitivesAdded(PrimitivesAddedEvent event) {
341        filterModel.executeFilters(event.getPrimitives());
342    }
343
344    @Override
345    public void primitivesRemoved(PrimitivesRemovedEvent event) {
346        filterModel.executeFilters();
347    }
348
349    @Override
350    public void relationMembersChanged(RelationMembersChangedEvent event) {
351        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
352    }
353
354    @Override
355    public void tagsChanged(TagsChangedEvent event) {
356        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
357    }
358
359    @Override
360    public void wayNodesChanged(WayNodesChangedEvent event) {
361        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
362    }
363
364    /**
365     * This method is intendet for Plugins getting the filtermodel and using .addFilter() to
366     * add a new filter.
367     * @return the filtermodel
368     */
369    public FilterTableModel getFilterModel() {
370        return filterModel;
371    }
372
373    abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction {
374
375        protected transient Filter lastFilter;
376
377        @Override
378        public void actionPerformed(ActionEvent e) {
379            throw new UnsupportedOperationException();
380        }
381
382        @Override
383        public List<MultikeyInfo> getMultikeyCombinations() {
384            List<MultikeyInfo> result = new ArrayList<>();
385
386            for (int i = 0; i < filterModel.getRowCount(); i++) {
387                Filter filter = filterModel.getFilter(i);
388                MultikeyInfo info = new MultikeyInfo(i, filter.text);
389                result.add(info);
390            }
391
392            return result;
393        }
394
395        protected boolean isLastFilterValid() {
396            return lastFilter != null && filterModel.getFilters().contains(lastFilter);
397        }
398
399        @Override
400        public MultikeyInfo getLastMultikeyAction() {
401            if (isLastFilterValid())
402                return new MultikeyInfo(-1, lastFilter.text);
403            else
404                return null;
405        }
406    }
407
408    private class EnableFilterAction extends AbstractFilterAction  {
409
410        EnableFilterAction() {
411            putValue(SHORT_DESCRIPTION, tr("Enable filter"));
412            ENABLE_FILTER_SHORTCUT.setAccelerator(this);
413        }
414
415        @Override
416        public Shortcut getMultikeyShortcut() {
417            return ENABLE_FILTER_SHORTCUT;
418        }
419
420        @Override
421        public void executeMultikeyAction(int index, boolean repeatLastAction) {
422            if (index >= 0 && index < filterModel.getRowCount()) {
423                Filter filter = filterModel.getFilter(index);
424                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
425                lastFilter = filter;
426            } else if (repeatLastAction && isLastFilterValid()) {
427                filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED);
428            }
429        }
430    }
431
432    private class HidingFilterAction extends AbstractFilterAction {
433
434        HidingFilterAction() {
435            putValue(SHORT_DESCRIPTION, tr("Hiding filter"));
436            HIDING_FILTER_SHORTCUT.setAccelerator(this);
437        }
438
439        @Override
440        public Shortcut getMultikeyShortcut() {
441            return HIDING_FILTER_SHORTCUT;
442        }
443
444        @Override
445        public void executeMultikeyAction(int index, boolean repeatLastAction) {
446            if (index >= 0 && index < filterModel.getRowCount()) {
447                Filter filter = filterModel.getFilter(index);
448                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
449                lastFilter = filter;
450            } else if (repeatLastAction && isLastFilterValid()) {
451                filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING);
452            }
453        }
454    }
455}