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