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 FilterTableModel filterModel = new FilterTableModel();
061
062    private EnableFilterAction enableFilterAction;
063    private 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
101    protected final String[] columnToolTips = {
102            Main.platform.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT),
103            Main.platform.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT),
104            null,
105            tr("Inverse filter"),
106            tr("Filter mode")
107    };
108
109    protected void build() {
110        userTable = new JTable(filterModel){
111            @Override
112            protected JTableHeader createDefaultTableHeader() {
113                return new JTableHeader(columnModel) {
114                    @Override
115                    public String getToolTipText(MouseEvent e) {
116                        java.awt.Point p = e.getPoint();
117                        int index = columnModel.getColumnIndexAtX(p.x);
118                        int realIndex = columnModel.getColumn(index).getModelIndex();
119                        return columnToolTips[realIndex];
120                    }
121                };
122            }
123        };
124
125        userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
126
127        userTable.getColumnModel().getColumn(0).setMaxWidth(1);
128        userTable.getColumnModel().getColumn(1).setMaxWidth(1);
129        userTable.getColumnModel().getColumn(3).setMaxWidth(1);
130        userTable.getColumnModel().getColumn(4).setMaxWidth(1);
131
132        userTable.getColumnModel().getColumn(0).setResizable(false);
133        userTable.getColumnModel().getColumn(1).setResizable(false);
134        userTable.getColumnModel().getColumn(3).setResizable(false);
135        userTable.getColumnModel().getColumn(4).setResizable(false);
136
137        userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer());
138        userTable.setDefaultRenderer(String.class, new StringRenderer());
139
140        SideButton addButton = new SideButton(new AbstractAction() {
141            {
142                putValue(NAME, tr("Add"));
143                putValue(SHORT_DESCRIPTION,  tr("Add filter."));
144                putValue(SMALL_ICON, ImageProvider.get("dialogs","add"));
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        SideButton editButton = new SideButton(new AbstractAction() {
154            {
155                putValue(NAME, tr("Edit"));
156                putValue(SHORT_DESCRIPTION, tr("Edit filter."));
157                putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
158            }
159            @Override
160            public void actionPerformed(ActionEvent e) {
161                int index = userTable.getSelectionModel().getMinSelectionIndex();
162                if(index < 0) return;
163                Filter f = filterModel.getFilter(index);
164                Filter filter = (Filter)SearchAction.showSearchDialog(f);
165                if(filter != null){
166                    filterModel.setFilter(index, filter);
167                }
168            }
169        });
170        SideButton deleteButton = new SideButton(new AbstractAction() {
171            {
172                putValue(NAME, tr("Delete"));
173                putValue(SHORT_DESCRIPTION, tr("Delete filter."));
174                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
175            }
176            @Override
177            public void actionPerformed(ActionEvent e) {
178                int index = userTable.getSelectionModel().getMinSelectionIndex();
179                if(index < 0) return;
180                filterModel.removeFilter(index);
181            }
182        });
183        SideButton upButton = new SideButton(new AbstractAction() {
184            {
185                putValue(NAME, tr("Up"));
186                putValue(SHORT_DESCRIPTION, tr("Move filter up."));
187                putValue(SMALL_ICON, ImageProvider.get("dialogs", "up"));
188            }
189            @Override
190            public void actionPerformed(ActionEvent e) {
191                int index = userTable.getSelectionModel().getMinSelectionIndex();
192                if(index < 0) return;
193                filterModel.moveUpFilter(index);
194                userTable.getSelectionModel().setSelectionInterval(index-1, index-1);
195            }
196
197        });
198        SideButton downButton = new SideButton(new AbstractAction() {
199            {
200                putValue(NAME, tr("Down"));
201                putValue(SHORT_DESCRIPTION, tr("Move filter down."));
202                putValue(SMALL_ICON, ImageProvider.get("dialogs", "down"));
203            }
204            @Override
205            public void actionPerformed(ActionEvent e) {
206                int index = userTable.getSelectionModel().getMinSelectionIndex();
207                if(index < 0) return;
208                filterModel.moveDownFilter(index);
209                userTable.getSelectionModel().setSelectionInterval(index+1, index+1);
210            }
211        });
212
213        // Toggle filter "enabled" on Enter
214        InputMapUtils.addEnterAction(userTable, new AbstractAction() {
215            @Override
216            public void actionPerformed(ActionEvent e) {
217                int index = userTable.getSelectedRow();
218                if (index<0) return;
219                Filter filter = filterModel.getFilter(index);
220                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
221            }
222        });
223
224        // Toggle filter "hiding" on Spacebar
225        InputMapUtils.addSpacebarAction(userTable, new AbstractAction() {
226            @Override
227            public void actionPerformed(ActionEvent e) {
228                int index = userTable.getSelectedRow();
229                if (index<0) return;
230                Filter filter = filterModel.getFilter(index);
231                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
232            }
233        });
234
235        createLayout(userTable, true, Arrays.asList(new SideButton[] {
236                addButton, editButton, deleteButton, upButton, downButton
237        }));
238    }
239
240    @Override
241    public void destroy() {
242        MultikeyActionsHandler.getInstance().removeAction(enableFilterAction);
243        MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction);
244        super.destroy();
245    }
246
247    static class StringRenderer extends DefaultTableCellRenderer {
248        @Override
249        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,int row,int column) {
250            FilterTableModel model = (FilterTableModel)table.getModel();
251            Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
252            cell.setEnabled(model.isCellEnabled(row, column));
253            return cell;
254        }
255    }
256
257    static class BooleanRenderer extends JCheckBox implements TableCellRenderer {
258        @Override
259        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,int row,int column) {
260            FilterTableModel model = (FilterTableModel)table.getModel();
261            setSelected(value != null && (Boolean)value);
262            setEnabled(model.isCellEnabled(row, column));
263            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
264            return this;
265        }
266    }
267
268    public void updateDialogHeader() {
269        SwingUtilities.invokeLater(new Runnable() {
270            @Override
271            public void run() {
272                setTitle(tr("Filter Hidden:{0} Disabled:{1}", filterModel.disabledAndHiddenCount, filterModel.disabledCount));
273            }
274        });
275    }
276
277    public void drawOSDText(Graphics2D g) {
278        filterModel.drawOSDText(g);
279    }
280
281    /**
282     * Returns the list of primitives whose filtering can be affected by change in primitive
283     * @param primitives list of primitives to check
284     * @return List of primitives whose filtering can be affected by change in source primitives
285     */
286    private Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
287        // Filters can use nested parent/child expression so complete tree is necessary
288        Set<OsmPrimitive> result = new HashSet<>();
289        Stack<OsmPrimitive> stack = new Stack<>();
290        stack.addAll(primitives);
291
292        while (!stack.isEmpty()) {
293            OsmPrimitive p = stack.pop();
294
295            if (result.contains(p)) {
296                continue;
297            }
298
299            result.add(p);
300
301            if (p instanceof Way) {
302                for (OsmPrimitive n: ((Way)p).getNodes()) {
303                    stack.push(n);
304                }
305            } else if (p instanceof Relation) {
306                for (RelationMember rm: ((Relation)p).getMembers()) {
307                    stack.push(rm.getMember());
308                }
309            }
310
311            for (OsmPrimitive ref: p.getReferrers()) {
312                stack.push(ref);
313            }
314        }
315
316        return result;
317    }
318
319    @Override
320    public void dataChanged(DataChangedEvent event) {
321        filterModel.executeFilters();
322    }
323
324    @Override
325    public void nodeMoved(NodeMovedEvent event) {
326        // Do nothing
327    }
328
329    @Override
330    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
331        filterModel.executeFilters();
332    }
333
334    @Override
335    public void primitivesAdded(PrimitivesAddedEvent event) {
336        filterModel.executeFilters(event.getPrimitives());
337    }
338
339    @Override
340    public void primitivesRemoved(PrimitivesRemovedEvent event) {
341        filterModel.executeFilters();
342    }
343
344    @Override
345    public void relationMembersChanged(RelationMembersChangedEvent event) {
346        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
347    }
348
349    @Override
350    public void tagsChanged(TagsChangedEvent event) {
351        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
352    }
353
354    @Override
355    public void wayNodesChanged(WayNodesChangedEvent event) {
356        filterModel.executeFilters(getAffectedPrimitives(event.getPrimitives()));
357    }
358
359    abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction {
360
361        protected Filter lastFilter;
362
363        @Override
364        public void actionPerformed(ActionEvent e) {
365            throw new UnsupportedOperationException();
366        }
367
368        @Override
369        public List<MultikeyInfo> getMultikeyCombinations() {
370            List<MultikeyInfo> result = new ArrayList<>();
371
372            for (int i=0; i<filterModel.getRowCount(); i++) {
373                Filter filter = filterModel.getFilter(i);
374                MultikeyInfo info = new MultikeyInfo(i, filter.text);
375                result.add(info);
376            }
377
378            return result;
379        }
380
381        protected boolean isLastFilterValid() {
382            return lastFilter != null && filterModel.getFilters().contains(lastFilter);
383        }
384
385        @Override
386        public MultikeyInfo getLastMultikeyAction() {
387            if (isLastFilterValid())
388                return new MultikeyInfo(-1, lastFilter.text);
389            else
390                return null;
391        }
392
393    }
394
395    private class EnableFilterAction extends AbstractFilterAction  {
396
397        EnableFilterAction() {
398            putValue(SHORT_DESCRIPTION, tr("Enable filter"));
399            ENABLE_FILTER_SHORTCUT.setAccelerator(this);
400        }
401
402        @Override
403        public Shortcut getMultikeyShortcut() {
404            return ENABLE_FILTER_SHORTCUT;
405        }
406
407        @Override
408        public void executeMultikeyAction(int index, boolean repeatLastAction) {
409            if (index >= 0 && index < filterModel.getRowCount()) {
410                Filter filter = filterModel.getFilter(index);
411                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
412                lastFilter = filter;
413            } else if (repeatLastAction && isLastFilterValid()) {
414                filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED);
415            }
416        }
417    }
418
419    private class HidingFilterAction extends AbstractFilterAction {
420
421        public HidingFilterAction() {
422            putValue(SHORT_DESCRIPTION, tr("Hiding filter"));
423            HIDING_FILTER_SHORTCUT.setAccelerator(this);
424        }
425
426        @Override
427        public Shortcut getMultikeyShortcut() {
428            return HIDING_FILTER_SHORTCUT;
429        }
430
431        @Override
432        public void executeMultikeyAction(int index, boolean repeatLastAction) {
433            if (index >= 0 && index < filterModel.getRowCount()) {
434                Filter filter = filterModel.getFilter(index);
435                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
436                lastFilter = filter;
437            } else if (repeatLastAction && isLastFilterValid()) {
438                filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING);
439            }
440        }
441
442    }
443}