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.command.Command;
032import org.openstreetmap.josm.data.SelectionChangedListener;
033import org.openstreetmap.josm.data.osm.DataSet;
034import org.openstreetmap.josm.data.osm.Node;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.WaySegment;
037import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
038import org.openstreetmap.josm.data.validation.OsmValidator;
039import org.openstreetmap.josm.data.validation.TestError;
040import org.openstreetmap.josm.data.validation.ValidatorVisitor;
041import org.openstreetmap.josm.gui.MapView;
042import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
043import org.openstreetmap.josm.gui.PleaseWaitRunnable;
044import org.openstreetmap.josm.gui.PopupMenuHandler;
045import org.openstreetmap.josm.gui.SideButton;
046import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
047import org.openstreetmap.josm.gui.layer.Layer;
048import org.openstreetmap.josm.gui.layer.OsmDataLayer;
049import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
050import org.openstreetmap.josm.gui.progress.ProgressMonitor;
051import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
052import org.openstreetmap.josm.io.OsmTransferException;
053import org.openstreetmap.josm.tools.ImageProvider;
054import org.openstreetmap.josm.tools.InputMapUtils;
055import org.openstreetmap.josm.tools.Shortcut;
056import org.xml.sax.SAXException;
057
058/**
059 * A small tool dialog for displaying the current errors. The selection manager
060 * respects clicks into the selection list. Ctrl-click will remove entries from
061 * the list while single click will make the clicked entry the only selection.
062 *
063 * @author frsantos
064 */
065public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, LayerChangeListener {
066
067    /** The display tree */
068    public ValidatorTreePanel tree;
069
070    /** The fix button */
071    private SideButton fixButton;
072    /** The ignore button */
073    private SideButton ignoreButton;
074    /** The select button */
075    private SideButton selectButton;
076
077    private final JPopupMenu popupMenu = new JPopupMenu();
078    private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
079
080    /** Last selected element */
081    private DefaultMutableTreeNode lastSelectedNode = null;
082
083    private OsmDataLayer linkedLayer;
084
085    /**
086     * Constructor
087     */
088    public ValidatorDialog() {
089        super(tr("Validation Results"), "validator", tr("Open the validation window."),
090                Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
091                        KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150, false, ValidatorPreference.class);
092
093        popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("problem"));
094
095        tree = new ValidatorTreePanel();
096        tree.addMouseListener(new MouseEventHandler());
097        addTreeSelectionListener(new SelectionWatch());
098        InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
099
100        List<SideButton> buttons = new LinkedList<SideButton>();
101
102        selectButton = new SideButton(new AbstractAction() {
103            {
104                putValue(NAME, tr("Select"));
105                putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
106                putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
107            }
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        buttons.add(new SideButton(Main.main.validator.validateAction));
119
120        fixButton = new SideButton(new AbstractAction() {
121            {
122                putValue(NAME, tr("Fix"));
123                putValue(SHORT_DESCRIPTION,  tr("Fix the selected issue."));
124                putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
125            }
126            @Override
127            public void actionPerformed(ActionEvent e) {
128                fixErrors();
129            }
130        });
131        fixButton.setEnabled(false);
132        buttons.add(fixButton);
133
134        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
135            ignoreButton = new SideButton(new AbstractAction() {
136                {
137                    putValue(NAME, tr("Ignore"));
138                    putValue(SHORT_DESCRIPTION,  tr("Ignore the selected issue next time."));
139                    putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
140                }
141                @Override
142                public void actionPerformed(ActionEvent e) {
143                    ignoreErrors();
144                }
145            });
146            ignoreButton.setEnabled(false);
147            buttons.add(ignoreButton);
148        } else {
149            ignoreButton = null;
150        }
151        createLayout(tree, true, buttons);
152    }
153
154    @Override
155    public void showNotify() {
156        DataSet.addSelectionListener(this);
157        DataSet ds = Main.main.getCurrentDataSet();
158        if (ds != null) {
159            updateSelection(ds.getAllSelected());
160        }
161        MapView.addLayerChangeListener(this);
162        Layer activeLayer = Main.map.mapView.getActiveLayer();
163        if (activeLayer != null) {
164            activeLayerChange(null, activeLayer);
165        }
166    }
167
168    @Override
169    public void hideNotify() {
170        MapView.removeLayerChangeListener(this);
171        DataSet.removeSelectionListener(this);
172    }
173
174    @Override
175    public void setVisible(boolean v) {
176        if (tree != null) {
177            tree.setVisible(v);
178        }
179        super.setVisible(v);
180        Main.map.repaint();
181    }
182
183    /**
184     * Fix selected errors
185     */
186    @SuppressWarnings("unchecked")
187    private void fixErrors() {
188        TreePath[] selectionPaths = tree.getSelectionPaths();
189        if (selectionPaths == null)
190            return;
191
192        Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
193
194        LinkedList<TestError> errorsToFix = new LinkedList<TestError>();
195        for (TreePath path : selectionPaths) {
196            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
197            if (node == null) {
198                continue;
199            }
200
201            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
202            while (children.hasMoreElements()) {
203                DefaultMutableTreeNode childNode = children.nextElement();
204                if (processedNodes.contains(childNode)) {
205                    continue;
206                }
207
208                processedNodes.add(childNode);
209                Object nodeInfo = childNode.getUserObject();
210                if (nodeInfo instanceof TestError) {
211                    errorsToFix.add((TestError)nodeInfo);
212                }
213            }
214        }
215
216        // run fix task asynchronously
217        //
218        FixTask fixTask = new FixTask(errorsToFix);
219        Main.worker.submit(fixTask);
220    }
221
222    /**
223     * Set selected errors to ignore state
224     */
225    @SuppressWarnings("unchecked")
226    private void ignoreErrors() {
227        int asked = JOptionPane.DEFAULT_OPTION;
228        boolean changed = false;
229        TreePath[] selectionPaths = tree.getSelectionPaths();
230        if (selectionPaths == null)
231            return;
232
233        Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
234        for (TreePath path : selectionPaths) {
235            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
236            if (node == null) {
237                continue;
238            }
239
240            Object mainNodeInfo = node.getUserObject();
241            if (!(mainNodeInfo instanceof TestError)) {
242                Set<String> state = new HashSet<String>();
243                // ask if the whole set should be ignored
244                if (asked == JOptionPane.DEFAULT_OPTION) {
245                    String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
246                    asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
247                            tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
248                            a, a[1]);
249                }
250                if (asked == JOptionPane.YES_NO_OPTION) {
251                    Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
252                    while (children.hasMoreElements()) {
253                        DefaultMutableTreeNode childNode = children.nextElement();
254                        if (processedNodes.contains(childNode)) {
255                            continue;
256                        }
257
258                        processedNodes.add(childNode);
259                        Object nodeInfo = childNode.getUserObject();
260                        if (nodeInfo instanceof TestError) {
261                            TestError err = (TestError) nodeInfo;
262                            err.setIgnored(true);
263                            changed = true;
264                            state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
265                        }
266                    }
267                    for (String s : state) {
268                        OsmValidator.addIgnoredError(s);
269                    }
270                    continue;
271                } else if (asked == JOptionPane.CANCEL_OPTION) {
272                    continue;
273                }
274            }
275
276            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
277            while (children.hasMoreElements()) {
278                DefaultMutableTreeNode childNode = children.nextElement();
279                if (processedNodes.contains(childNode)) {
280                    continue;
281                }
282
283                processedNodes.add(childNode);
284                Object nodeInfo = childNode.getUserObject();
285                if (nodeInfo instanceof TestError) {
286                    TestError error = (TestError) nodeInfo;
287                    String state = error.getIgnoreState();
288                    if (state != null) {
289                        OsmValidator.addIgnoredError(state);
290                    }
291                    changed = true;
292                    error.setIgnored(true);
293                }
294            }
295        }
296        if (changed) {
297            tree.resetErrors();
298            OsmValidator.saveIgnoredErrors();
299            Main.map.repaint();
300        }
301    }
302
303    /**
304     * Sets the selection of the map to the current selected items.
305     */
306    @SuppressWarnings("unchecked")
307    private void setSelectedItems() {
308        if (tree == null)
309            return;
310
311        Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(40);
312
313        TreePath[] selectedPaths = tree.getSelectionPaths();
314        if (selectedPaths == null)
315            return;
316
317        for (TreePath path : selectedPaths) {
318            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
319            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
320            while (children.hasMoreElements()) {
321                DefaultMutableTreeNode childNode = children.nextElement();
322                Object nodeInfo = childNode.getUserObject();
323                if (nodeInfo instanceof TestError) {
324                    TestError error = (TestError) nodeInfo;
325                    sel.addAll(error.getSelectablePrimitives());
326                }
327            }
328        }
329        DataSet ds = Main.main.getCurrentDataSet();
330        if (ds != null) {
331            ds.setSelected(sel);
332        }
333    }
334
335    /**
336     * Checks for fixes in selected element and, if needed, adds to the sel
337     * parameter all selected elements
338     *
339     * @param sel
340     *            The collection where to add all selected elements
341     * @param addSelected
342     *            if true, add all selected elements to collection
343     * @return whether the selected elements has any fix
344     */
345    @SuppressWarnings("unchecked")
346    private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
347        boolean hasFixes = false;
348
349        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
350        if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
351            Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
352            while (children.hasMoreElements()) {
353                DefaultMutableTreeNode childNode = children.nextElement();
354                Object nodeInfo = childNode.getUserObject();
355                if (nodeInfo instanceof TestError) {
356                    TestError error = (TestError) nodeInfo;
357                    error.setSelected(false);
358                }
359            }
360        }
361
362        lastSelectedNode = node;
363        if (node == null)
364            return hasFixes;
365
366        Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
367        while (children.hasMoreElements()) {
368            DefaultMutableTreeNode childNode = children.nextElement();
369            Object nodeInfo = childNode.getUserObject();
370            if (nodeInfo instanceof TestError) {
371                TestError error = (TestError) nodeInfo;
372                error.setSelected(true);
373
374                hasFixes = hasFixes || error.isFixable();
375                if (addSelected) {
376                    sel.addAll(error.getSelectablePrimitives());
377                }
378            }
379        }
380        selectButton.setEnabled(true);
381        if (ignoreButton != null) {
382            ignoreButton.setEnabled(true);
383        }
384
385        return hasFixes;
386    }
387
388    @Override
389    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
390        if (newLayer instanceof OsmDataLayer) {
391            linkedLayer = (OsmDataLayer)newLayer;
392            tree.setErrorList(linkedLayer.validationErrors);
393        }
394    }
395
396    @Override
397    public void layerAdded(Layer newLayer) {}
398
399    @Override
400    public void layerRemoved(Layer oldLayer) {
401        if (oldLayer == linkedLayer) {
402            tree.setErrorList(new ArrayList<TestError>());
403        }
404    }
405
406    /**
407     * Add a tree selection listener to the validator tree.
408     * @param listener the TreeSelectionListener
409     * @since 5958
410     */
411    public void addTreeSelectionListener(TreeSelectionListener listener) {
412        tree.addTreeSelectionListener(listener);
413    }
414
415    /**
416     * Remove the given tree selection listener from the validator tree.
417     * @param listener the TreeSelectionListener
418     * @since 5958
419     */
420    public void removeTreeSelectionListener(TreeSelectionListener listener) {
421        tree.removeTreeSelectionListener(listener);
422    }
423
424    /**
425     * Replies the popup menu handler.
426     * @return The popup menu handler
427     * @since 5958
428     */
429    public PopupMenuHandler getPopupMenuHandler() {
430        return popupMenuHandler;
431    }
432
433    /**
434     * Replies the currently selected error, or {@code null}.
435     * @return The selected error, if any.
436     * @since 5958
437     */
438    public TestError getSelectedError() {
439        Object comp = tree.getLastSelectedPathComponent();
440        if (comp instanceof DefaultMutableTreeNode) {
441            Object object = ((DefaultMutableTreeNode)comp).getUserObject();
442            if (object instanceof TestError) {
443                return (TestError) object;
444            }
445        }
446        return null;
447    }
448
449    /**
450     * Watches for double clicks and launches the popup menu.
451     */
452    class MouseEventHandler extends PopupMenuLauncher {
453
454        public MouseEventHandler() {
455            super(popupMenu);
456        }
457
458        @Override
459        public void mouseClicked(MouseEvent e) {
460            fixButton.setEnabled(false);
461            if (ignoreButton != null) {
462                ignoreButton.setEnabled(false);
463            }
464            selectButton.setEnabled(false);
465
466            boolean isDblClick = isDoubleClick(e);
467
468            Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
469
470            boolean hasFixes = setSelection(sel, isDblClick);
471            fixButton.setEnabled(hasFixes);
472
473            if (isDblClick) {
474                Main.main.getCurrentDataSet().setSelected(sel);
475                if (Main.pref.getBoolean("validator.autozoom", false)) {
476                    AutoScaleAction.zoomTo(sel);
477                }
478            }
479        }
480
481        @Override public void launch(MouseEvent e) {
482            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
483            if (selPath == null)
484                return;
485            DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
486            if (!(node.getUserObject() instanceof TestError))
487                return;
488            super.launch(e);
489        }
490
491    }
492
493    /**
494     * Watches for tree selection.
495     */
496    public class SelectionWatch implements TreeSelectionListener {
497        @Override
498        public void valueChanged(TreeSelectionEvent e) {
499            fixButton.setEnabled(false);
500            if (ignoreButton != null) {
501                ignoreButton.setEnabled(false);
502            }
503            selectButton.setEnabled(false);
504
505            boolean hasFixes = setSelection(null, false);
506            fixButton.setEnabled(hasFixes);
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<OsmPrimitive>(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 e) {
641                // FIXME: signature of realRun should have a generic checked exception we
642                // could throw here
643                throw new RuntimeException(e);
644            } catch(InvocationTargetException e) {
645                throw new RuntimeException(e);
646            } finally {
647                monitor.finishTask();
648            }
649        }
650    }
651}