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