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