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