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