001    // License: GPL. See LICENSE file for details.
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.event.ActionEvent;
008    import java.awt.event.ActionListener;
009    import java.awt.event.KeyEvent;
010    import java.awt.event.MouseAdapter;
011    import java.awt.event.MouseEvent;
012    import java.io.IOException;
013    import java.lang.reflect.InvocationTargetException;
014    import java.util.ArrayList;
015    import java.util.Collection;
016    import java.util.Enumeration;
017    import java.util.HashSet;
018    import java.util.LinkedList;
019    import java.util.List;
020    import java.util.Set;
021    
022    import javax.swing.AbstractAction;
023    import javax.swing.JComponent;
024    import javax.swing.JMenuItem;
025    import javax.swing.JOptionPane;
026    import javax.swing.JPopupMenu;
027    import javax.swing.SwingUtilities;
028    import javax.swing.event.TreeSelectionEvent;
029    import javax.swing.event.TreeSelectionListener;
030    import javax.swing.tree.DefaultMutableTreeNode;
031    import javax.swing.tree.TreePath;
032    
033    import org.openstreetmap.josm.Main;
034    import org.openstreetmap.josm.actions.AutoScaleAction;
035    import org.openstreetmap.josm.command.Command;
036    import org.openstreetmap.josm.data.SelectionChangedListener;
037    import org.openstreetmap.josm.data.osm.DataSet;
038    import org.openstreetmap.josm.data.osm.Node;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.WaySegment;
041    import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
042    import org.openstreetmap.josm.data.validation.OsmValidator;
043    import org.openstreetmap.josm.data.validation.TestError;
044    import org.openstreetmap.josm.data.validation.ValidatorVisitor;
045    import org.openstreetmap.josm.gui.MapView;
046    import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
047    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
048    import org.openstreetmap.josm.gui.SideButton;
049    import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
050    import org.openstreetmap.josm.gui.layer.Layer;
051    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
052    import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
053    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
054    import org.openstreetmap.josm.io.OsmTransferException;
055    import org.openstreetmap.josm.tools.ImageProvider;
056    import org.openstreetmap.josm.tools.InputMapUtils;
057    import org.openstreetmap.josm.tools.Shortcut;
058    import org.xml.sax.SAXException;
059    
060    /**
061     * A small tool dialog for displaying the current errors. The selection manager
062     * respects clicks into the selection list. Ctrl-click will remove entries from
063     * the list while single click will make the clicked entry the only selection.
064     *
065     * @author frsantos
066     */
067    public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, LayerChangeListener {
068        /** Serializable ID */
069        private static final long serialVersionUID = 2952292777351992696L;
070    
071        /** The display tree */
072        public ValidatorTreePanel tree;
073    
074        /** The fix button */
075        private SideButton fixButton;
076        /** The ignore button */
077        private SideButton ignoreButton;
078        /** The select button */
079        private SideButton selectButton;
080    
081        private JPopupMenu popupMenu;
082        private TestError popupMenuError = null;
083    
084        /** Last selected element */
085        private DefaultMutableTreeNode lastSelectedNode = null;
086    
087        private OsmDataLayer linkedLayer;
088    
089        /**
090         * Constructor
091         */
092        public ValidatorDialog() {
093            super(tr("Validation Results"), "validator", tr("Open the validation window."),
094                    Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
095                            KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150);
096    
097            popupMenu = new JPopupMenu();
098    
099            JMenuItem zoomTo = new JMenuItem(tr("Zoom to problem"));
100            zoomTo.addActionListener(new ActionListener() {
101                @Override
102                public void actionPerformed(ActionEvent e) {
103                    zoomToProblem();
104                }
105            });
106            popupMenu.add(zoomTo);
107    
108            tree = new ValidatorTreePanel();
109            tree.addMouseListener(new ClickWatch());
110            tree.addTreeSelectionListener(new SelectionWatch());
111            InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
112    
113            List<SideButton> buttons = new LinkedList<SideButton>();
114    
115            selectButton = new SideButton(new AbstractAction() {
116                {
117                    putValue(NAME, marktr("Select"));
118                    putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
119                    putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
120                }
121                @Override
122                public void actionPerformed(ActionEvent e) {
123                    setSelectedItems();
124                }
125            });
126            InputMapUtils.addEnterAction(tree, selectButton.getAction());
127    
128            selectButton.setEnabled(false);
129            buttons.add(selectButton);
130    
131            buttons.add(new SideButton(Main.main.validator.validateAction));
132    
133            fixButton = new SideButton(new AbstractAction() {
134                {
135                    putValue(NAME, marktr("Fix"));
136                    putValue(SHORT_DESCRIPTION,  tr("Fix the selected issue."));
137                    putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
138                }
139                @Override
140                public void actionPerformed(ActionEvent e) {
141                    fixErrors(e);
142                }
143            });
144            fixButton.setEnabled(false);
145            buttons.add(fixButton);
146    
147            if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
148                ignoreButton = new SideButton(new AbstractAction() {
149                    {
150                        putValue(NAME, marktr("Ignore"));
151                        putValue(SHORT_DESCRIPTION,  tr("Ignore the selected issue next time."));
152                        putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
153                    }
154                    @Override
155                    public void actionPerformed(ActionEvent e) {
156                        ignoreErrors(e);
157                    }
158                });
159                ignoreButton.setEnabled(false);
160                buttons.add(ignoreButton);
161            } else {
162                ignoreButton = null;
163            }
164            createLayout(tree, true, buttons);
165        }
166    
167        @Override
168        public void showNotify() {
169            DataSet.addSelectionListener(this);
170            DataSet ds = Main.main.getCurrentDataSet();
171            if (ds != null) {
172                updateSelection(ds.getAllSelected());
173            }
174            MapView.addLayerChangeListener(this);
175            Layer activeLayer = Main.map.mapView.getActiveLayer();
176            if (activeLayer != null) {
177                activeLayerChange(null, activeLayer);
178            }
179        }
180    
181        @Override
182        public void hideNotify() {
183            MapView.removeLayerChangeListener(this);
184            DataSet.removeSelectionListener(this);
185        }
186    
187        @Override
188        public void setVisible(boolean v) {
189            if (tree != null) {
190                tree.setVisible(v);
191            }
192            super.setVisible(v);
193            Main.map.repaint();
194        }
195    
196        /**
197         * Fix selected errors
198         *
199         * @param e
200         */
201        @SuppressWarnings("unchecked")
202        private void fixErrors(ActionEvent e) {
203            TreePath[] selectionPaths = tree.getSelectionPaths();
204            if (selectionPaths == null)
205                return;
206    
207            Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
208    
209            LinkedList<TestError> errorsToFix = new LinkedList<TestError>();
210            for (TreePath path : selectionPaths) {
211                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
212                if (node == null) {
213                    continue;
214                }
215    
216                Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
217                while (children.hasMoreElements()) {
218                    DefaultMutableTreeNode childNode = children.nextElement();
219                    if (processedNodes.contains(childNode)) {
220                        continue;
221                    }
222    
223                    processedNodes.add(childNode);
224                    Object nodeInfo = childNode.getUserObject();
225                    if (nodeInfo instanceof TestError) {
226                        errorsToFix.add((TestError)nodeInfo);
227                    }
228                }
229            }
230    
231            // run fix task asynchronously
232            //
233            FixTask fixTask = new FixTask(errorsToFix);
234            Main.worker.submit(fixTask);
235        }
236    
237        /**
238         * Set selected errors to ignore state
239         *
240         * @param e
241         */
242        @SuppressWarnings("unchecked")
243        private void ignoreErrors(ActionEvent e) {
244            int asked = JOptionPane.DEFAULT_OPTION;
245            boolean changed = false;
246            TreePath[] selectionPaths = tree.getSelectionPaths();
247            if (selectionPaths == null)
248                return;
249    
250            Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
251            for (TreePath path : selectionPaths) {
252                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
253                if (node == null) {
254                    continue;
255                }
256    
257                Object mainNodeInfo = node.getUserObject();
258                if (!(mainNodeInfo instanceof TestError)) {
259                    Set<String> state = new HashSet<String>();
260                    // ask if the whole set should be ignored
261                    if (asked == JOptionPane.DEFAULT_OPTION) {
262                        String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
263                        asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
264                                tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
265                                a, a[1]);
266                    }
267                    if (asked == JOptionPane.YES_NO_OPTION) {
268                        Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
269                        while (children.hasMoreElements()) {
270                            DefaultMutableTreeNode childNode = children.nextElement();
271                            if (processedNodes.contains(childNode)) {
272                                continue;
273                            }
274    
275                            processedNodes.add(childNode);
276                            Object nodeInfo = childNode.getUserObject();
277                            if (nodeInfo instanceof TestError) {
278                                TestError err = (TestError) nodeInfo;
279                                err.setIgnored(true);
280                                changed = true;
281                                state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
282                            }
283                        }
284                        for (String s : state) {
285                            OsmValidator.addIgnoredError(s);
286                        }
287                        continue;
288                    } else if (asked == JOptionPane.CANCEL_OPTION) {
289                        continue;
290                    }
291                }
292    
293                Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
294                while (children.hasMoreElements()) {
295                    DefaultMutableTreeNode childNode = children.nextElement();
296                    if (processedNodes.contains(childNode)) {
297                        continue;
298                    }
299    
300                    processedNodes.add(childNode);
301                    Object nodeInfo = childNode.getUserObject();
302                    if (nodeInfo instanceof TestError) {
303                        TestError error = (TestError) nodeInfo;
304                        String state = error.getIgnoreState();
305                        if (state != null) {
306                            OsmValidator.addIgnoredError(state);
307                        }
308                        changed = true;
309                        error.setIgnored(true);
310                    }
311                }
312            }
313            if (changed) {
314                tree.resetErrors();
315                OsmValidator.saveIgnoredErrors();
316                Main.map.repaint();
317            }
318        }
319    
320        private void showPopupMenu(MouseEvent e) {
321            if (!e.isPopupTrigger())
322                return;
323            popupMenuError = null;
324            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
325            if (selPath == null)
326                return;
327            DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
328            if (!(node.getUserObject() instanceof TestError))
329                return;
330            popupMenuError = (TestError) node.getUserObject();
331            popupMenu.show(e.getComponent(), e.getX(), e.getY());
332        }
333    
334        private void zoomToProblem() {
335            if (popupMenuError == null)
336                return;
337            ValidatorBoundingXYVisitor bbox = new ValidatorBoundingXYVisitor();
338            popupMenuError.visitHighlighted(bbox);
339            if (bbox.getBounds() == null)
340                return;
341            bbox.enlargeBoundingBox(Main.pref.getDouble("validator.zoom-enlarge-bbox", 0.0002));
342            Main.map.mapView.recalculateCenterScale(bbox);
343        }
344    
345        /**
346         * Sets the selection of the map to the current selected items.
347         */
348        @SuppressWarnings("unchecked")
349        private void setSelectedItems() {
350            if (tree == null)
351                return;
352    
353            Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(40);
354    
355            TreePath[] selectedPaths = tree.getSelectionPaths();
356            if (selectedPaths == null)
357                return;
358    
359            for (TreePath path : selectedPaths) {
360                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
361                Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
362                while (children.hasMoreElements()) {
363                    DefaultMutableTreeNode childNode = children.nextElement();
364                    Object nodeInfo = childNode.getUserObject();
365                    if (nodeInfo instanceof TestError) {
366                        TestError error = (TestError) nodeInfo;
367                        sel.addAll(error.getSelectablePrimitives());
368                    }
369                }
370            }
371            DataSet ds = Main.main.getCurrentDataSet();
372            if (ds != null) {
373                ds.setSelected(sel);
374            }
375        }
376    
377        /**
378         * Checks for fixes in selected element and, if needed, adds to the sel
379         * parameter all selected elements
380         *
381         * @param sel
382         *            The collection where to add all selected elements
383         * @param addSelected
384         *            if true, add all selected elements to collection
385         * @return whether the selected elements has any fix
386         */
387        @SuppressWarnings("unchecked")
388        private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
389            boolean hasFixes = false;
390    
391            DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
392            if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
393                Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
394                while (children.hasMoreElements()) {
395                    DefaultMutableTreeNode childNode = children.nextElement();
396                    Object nodeInfo = childNode.getUserObject();
397                    if (nodeInfo instanceof TestError) {
398                        TestError error = (TestError) nodeInfo;
399                        error.setSelected(false);
400                    }
401                }
402            }
403    
404            lastSelectedNode = node;
405            if (node == null)
406                return hasFixes;
407    
408            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
409            while (children.hasMoreElements()) {
410                DefaultMutableTreeNode childNode = children.nextElement();
411                Object nodeInfo = childNode.getUserObject();
412                if (nodeInfo instanceof TestError) {
413                    TestError error = (TestError) nodeInfo;
414                    error.setSelected(true);
415    
416                    hasFixes = hasFixes || error.isFixable();
417                    if (addSelected) {
418                        //                    sel.addAll(error.getPrimitives()); // was selecting already deleted primitives! see #6640
419                        sel.addAll(error.getSelectablePrimitives());
420                    }
421                }
422            }
423            selectButton.setEnabled(true);
424            if (ignoreButton != null) {
425                ignoreButton.setEnabled(true);
426            }
427    
428            return hasFixes;
429        }
430    
431        @Override
432        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
433            if (newLayer instanceof OsmDataLayer) {
434                linkedLayer = (OsmDataLayer)newLayer;
435                tree.setErrorList(linkedLayer.validationErrors);
436            }
437        }
438    
439        @Override
440        public void layerAdded(Layer newLayer) {}
441    
442        @Override
443        public void layerRemoved(Layer oldLayer) {
444            if (oldLayer == linkedLayer) {
445                tree.setErrorList(new ArrayList<TestError>());
446            }
447        }
448    
449        /**
450         * Watches for clicks.
451         */
452        public class ClickWatch extends MouseAdapter {
453            @Override
454            public void mouseClicked(MouseEvent e) {
455                fixButton.setEnabled(false);
456                if (ignoreButton != null) {
457                    ignoreButton.setEnabled(false);
458                }
459                selectButton.setEnabled(false);
460    
461                boolean isDblClick = e.getClickCount() > 1;
462    
463                Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
464    
465                boolean hasFixes = setSelection(sel, isDblClick);
466                fixButton.setEnabled(hasFixes);
467    
468                if (isDblClick) {
469                    Main.main.getCurrentDataSet().setSelected(sel);
470                    if(Main.pref.getBoolean("validator.autozoom", false)) {
471                        AutoScaleAction.zoomTo(sel);
472                    }
473                }
474            }
475    
476            @Override
477            public void mousePressed(MouseEvent e) {
478                showPopupMenu(e);
479            }
480    
481            @Override
482            public void mouseReleased(MouseEvent e) {
483                showPopupMenu(e);
484            }
485    
486        }
487    
488        /**
489         * Watches for tree selection.
490         */
491        public class SelectionWatch implements TreeSelectionListener {
492            @Override
493            public void valueChanged(TreeSelectionEvent e) {
494                fixButton.setEnabled(false);
495                if (ignoreButton != null) {
496                    ignoreButton.setEnabled(false);
497                }
498                selectButton.setEnabled(false);
499    
500                boolean hasFixes = setSelection(null, false);
501                fixButton.setEnabled(hasFixes);
502                Main.map.repaint();
503            }
504        }
505    
506        public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
507            @Override
508            public void visit(OsmPrimitive p) {
509                if (p.isUsable()) {
510                    p.visit(this);
511                }
512            }
513    
514            @Override
515            public void visit(WaySegment ws) {
516                if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
517                    return;
518                visit(ws.way.getNodes().get(ws.lowerIndex));
519                visit(ws.way.getNodes().get(ws.lowerIndex + 1));
520            }
521    
522            @Override
523            public void visit(List<Node> nodes) {
524                for (Node n: nodes) {
525                    visit(n);
526                }
527            }
528        }
529    
530        public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
531            if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
532                return;
533            if (newSelection.isEmpty()) {
534                tree.setFilter(null);
535            }
536            HashSet<OsmPrimitive> filter = new HashSet<OsmPrimitive>(newSelection);
537            tree.setFilter(filter);
538        }
539    
540        @Override
541        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
542            updateSelection(newSelection);
543        }
544    
545        /**
546         * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
547         *
548         *
549         */
550        class FixTask extends PleaseWaitRunnable {
551            private Collection<TestError> testErrors;
552            private boolean canceled;
553    
554            public FixTask(Collection<TestError> testErrors) {
555                super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
556                this.testErrors = testErrors == null ? new ArrayList<TestError> (): testErrors;
557            }
558    
559            @Override
560            protected void cancel() {
561                this.canceled = true;
562            }
563    
564            @Override
565            protected void finish() {
566                // do nothing
567            }
568    
569            @Override
570            protected void realRun() throws SAXException, IOException,
571            OsmTransferException {
572                ProgressMonitor monitor = getProgressMonitor();
573                try {
574                    monitor.setTicksCount(testErrors.size());
575                    int i=0;
576                    for (TestError error: testErrors) {
577                        i++;
578                        monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(),error.getMessage()));
579                        if (this.canceled)
580                            return;
581                        if (error.isFixable()) {
582                            final Command fixCommand = error.getFix();
583                            if (fixCommand != null) {
584                                SwingUtilities.invokeAndWait(new Runnable() {
585                                    @Override
586                                    public void run() {
587                                        Main.main.undoRedo.addNoRedraw(fixCommand);
588                                    }
589                                });
590                            }
591                            // It is wanted to ignore an error if it said fixable, even if fixCommand was null
592                            // This is to fix #5764 and #5773: a delete command, for example, may be null if all concerned primitives have already been deleted
593                            error.setIgnored(true);
594                        }
595                        monitor.worked(1);
596                    }
597                    monitor.subTask(tr("Updating map ..."));
598                    SwingUtilities.invokeAndWait(new Runnable() {
599                        @Override
600                        public void run() {
601                            Main.main.undoRedo.afterAdd();
602                            Main.map.repaint();
603                            tree.resetErrors();
604                            Main.main.getCurrentDataSet().fireSelectionChanged();
605                        }
606                    });
607                } catch(InterruptedException e) {
608                    // FIXME: signature of realRun should have a generic checked exception we
609                    // could throw here
610                    throw new RuntimeException(e);
611                } catch(InvocationTargetException e) {
612                    throw new RuntimeException(e);
613                } finally {
614                    monitor.finishTask();
615                }
616            }
617        }
618    }