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;
018import java.util.concurrent.atomic.AtomicBoolean;
019
020import javax.swing.AbstractAction;
021import javax.swing.Action;
022import javax.swing.JComponent;
023import javax.swing.JOptionPane;
024import javax.swing.JPopupMenu;
025import javax.swing.SwingUtilities;
026import javax.swing.event.TreeSelectionEvent;
027import javax.swing.event.TreeSelectionListener;
028import javax.swing.tree.DefaultMutableTreeNode;
029import javax.swing.tree.TreePath;
030
031import org.openstreetmap.josm.actions.AbstractSelectAction;
032import org.openstreetmap.josm.actions.AutoScaleAction;
033import org.openstreetmap.josm.actions.JosmAction;
034import org.openstreetmap.josm.actions.ValidateAction;
035import org.openstreetmap.josm.actions.relation.EditRelationAction;
036import org.openstreetmap.josm.command.Command;
037import org.openstreetmap.josm.command.SequenceCommand;
038import org.openstreetmap.josm.data.UndoRedoHandler;
039import org.openstreetmap.josm.data.osm.DataSelectionListener;
040import org.openstreetmap.josm.data.osm.DataSet;
041import org.openstreetmap.josm.data.osm.Node;
042import org.openstreetmap.josm.data.osm.OsmPrimitive;
043import org.openstreetmap.josm.data.osm.WaySegment;
044import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
045import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
046import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
047import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
048import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
049import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
050import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
051import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
052import org.openstreetmap.josm.data.validation.OsmValidator;
053import org.openstreetmap.josm.data.validation.Severity;
054import org.openstreetmap.josm.data.validation.TestError;
055import org.openstreetmap.josm.data.validation.ValidatorVisitor;
056import org.openstreetmap.josm.gui.MainApplication;
057import org.openstreetmap.josm.gui.PleaseWaitRunnable;
058import org.openstreetmap.josm.gui.PopupMenuHandler;
059import org.openstreetmap.josm.gui.SideButton;
060import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
061import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
062import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
063import org.openstreetmap.josm.gui.layer.OsmDataLayer;
064import org.openstreetmap.josm.gui.layer.ValidatorLayer;
065import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
066import org.openstreetmap.josm.gui.progress.ProgressMonitor;
067import org.openstreetmap.josm.gui.util.GuiHelper;
068import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
069import org.openstreetmap.josm.io.OsmTransferException;
070import org.openstreetmap.josm.spi.preferences.Config;
071import org.openstreetmap.josm.tools.ImageProvider;
072import org.openstreetmap.josm.tools.InputMapUtils;
073import org.openstreetmap.josm.tools.JosmRuntimeException;
074import org.openstreetmap.josm.tools.Pair;
075import org.openstreetmap.josm.tools.Shortcut;
076import org.xml.sax.SAXException;
077
078/**
079 * A small tool dialog for displaying the current errors. The selection manager
080 * respects clicks into the selection list. Ctrl-click will remove entries from
081 * the list while single click will make the clicked entry the only selection.
082 *
083 * @author frsantos
084 */
085public class ValidatorDialog extends ToggleDialog
086        implements DataSelectionListener, ActiveLayerChangeListener, DataSetListenerAdapter.Listener {
087
088    /** The display tree */
089    public final ValidatorTreePanel tree;
090
091    /** The validate action */
092    public static final ValidateAction validateAction = new ValidateAction();
093
094    /** The fix action */
095    private final transient Action fixAction;
096    /** The ignore action */
097    private final transient Action ignoreAction;
098    /** The ignore-list management action */
099    private final transient Action ignorelistManagementAction;
100    /** The select action */
101    private final transient Action selectAction;
102    /** The lookup action */
103    private final transient LookupAction lookupAction;
104    private final transient JosmAction ignoreForNowAction;
105
106    private final JPopupMenu popupMenu = new JPopupMenu();
107    private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
108    private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
109
110    /** Last selected element */
111    private DefaultMutableTreeNode lastSelectedNode;
112
113    /**
114     * Constructor
115     */
116    public ValidatorDialog() {
117        super(tr("Validation Results"), "validator", tr("Open the validation window."),
118                Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation Results")),
119                        KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150, false, ValidatorPreference.class);
120
121        tree = new ValidatorTreePanel();
122        tree.addMouseListener(new MouseEventHandler());
123        addTreeSelectionListener(new SelectionWatch());
124        InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
125
126        ignoreForNowAction = new JosmAction(tr("Ignore for now"), "dialogs/delete",
127                tr("Ignore and remove from tree."), Shortcut.registerShortcut("tools:validate:ignore-for-now",
128                        tr("Ignore and remove from tree."), KeyEvent.VK_MINUS, Shortcut.SHIFT),
129                false, false) {
130
131            @Override
132            public void actionPerformed(ActionEvent e) {
133                TestError error = getSelectedError();
134                if (error != null) {
135                    error.setIgnored(true);
136                    tree.resetErrors();
137                    invalidateValidatorLayers();
138                }
139            }
140        };
141
142        popupMenuHandler.addAction(MainApplication.getMenu().autoScaleActions.get("problem"));
143        popupMenuHandler.addAction(new EditRelationAction());
144        popupMenuHandler.addAction(ignoreForNowAction);
145
146        List<SideButton> buttons = new LinkedList<>();
147
148        selectAction = new AbstractSelectAction() {
149            @Override
150            public void actionPerformed(ActionEvent e) {
151                setSelectedItems();
152            }
153        };
154        selectAction.setEnabled(false);
155        InputMapUtils.addEnterAction(tree, selectAction);
156        buttons.add(new SideButton(selectAction));
157
158        lookupAction = new LookupAction();
159        buttons.add(new SideButton(lookupAction));
160
161        buttons.add(new SideButton(validateAction));
162
163        fixAction = new AbstractAction() {
164            {
165                putValue(NAME, tr("Fix"));
166                putValue(SHORT_DESCRIPTION, tr("Fix the selected issue."));
167                new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true);
168            }
169            @Override
170            public void actionPerformed(ActionEvent e) {
171                fixErrors();
172            }
173        };
174        fixAction.setEnabled(false);
175        buttons.add(new SideButton(fixAction));
176
177        if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
178            ignoreAction = new AbstractAction() {
179                {
180                    putValue(NAME, tr("Ignore"));
181                    putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time."));
182                    new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true);
183                }
184                @Override
185                public void actionPerformed(ActionEvent e) {
186                    ignoreErrors();
187                }
188            };
189            ignoreAction.setEnabled(false);
190            buttons.add(new SideButton(ignoreAction));
191
192            ignorelistManagementAction = new IgnorelistManagementAction();
193            buttons.add(new SideButton(ignorelistManagementAction));
194        } else {
195            ignoreAction = null;
196            ignorelistManagementAction = null;
197        }
198
199        createLayout(tree, true, buttons);
200    }
201
202    /**
203     * The action to manage the ignore list.
204     */
205    static class IgnorelistManagementAction extends AbstractAction {
206        IgnorelistManagementAction() {
207            putValue(NAME, tr("Manage Ignore"));
208            putValue(SHORT_DESCRIPTION, tr("Manage the ignore list"));
209            new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true);
210        }
211
212        @Override
213        public void actionPerformed(ActionEvent e) {
214            new ValidatorListManagementDialog("Ignore");
215        }
216    }
217
218    /**
219     * The action to lookup the selection in the error tree.
220     */
221    class LookupAction extends AbstractAction {
222        LookupAction() {
223            putValue(NAME, tr("Lookup"));
224            putValue(SHORT_DESCRIPTION, tr("Looks up the selected primitives in the error list."));
225            new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
226            updateEnabledState();
227        }
228
229        @Override
230        public void actionPerformed(ActionEvent e) {
231            final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
232            if (ds == null) {
233                return;
234            }
235            tree.selectRelatedErrors(ds.getSelected());
236        }
237
238        void updateEnabledState() {
239            boolean found = false;
240            final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
241            if (ds != null && !ds.selectionEmpty()) {
242                for (TestError e : tree.getErrors()) {
243                    for (OsmPrimitive p : e.getPrimitives()) {
244                        if (p.isSelected()) {
245                            found = true;
246                            break;
247                        }
248                    }
249                }
250            }
251            setEnabled(found);
252        }
253    }
254
255    @Override
256    public void showNotify() {
257        DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
258        SelectionEventManager.getInstance().addSelectionListener(this);
259        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
260        if (ds != null) {
261            updateSelection(ds.getAllSelected());
262        }
263        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
264
265    }
266
267    @Override
268    public void hideNotify() {
269        DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
270        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
271        SelectionEventManager.getInstance().removeSelectionListener(this);
272    }
273
274    @Override
275    public void setVisible(boolean v) {
276        if (tree != null) {
277            tree.setVisible(v);
278        }
279        super.setVisible(v);
280    }
281
282    /**
283     * Fix selected errors
284     */
285    private void fixErrors() {
286        TreePath[] selectionPaths = tree.getSelectionPaths();
287        if (selectionPaths == null)
288            return;
289
290        Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
291
292        List<TestError> errorsToFix = new LinkedList<>();
293        for (TreePath path : selectionPaths) {
294            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
295            if (node != null) {
296                ValidatorTreePanel.visitTestErrors(node, errorsToFix::add, processedNodes);
297            }
298        }
299
300        // run fix task asynchronously
301        MainApplication.worker.submit(new FixTask(errorsToFix));
302    }
303
304    /**
305     * Set selected errors to ignore state
306     */
307    private void ignoreErrors() {
308        int asked = JOptionPane.DEFAULT_OPTION;
309        AtomicBoolean changed = new AtomicBoolean();
310        TreePath[] selectionPaths = tree.getSelectionPaths();
311        if (selectionPaths == null)
312            return;
313
314        Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
315        for (TreePath path : selectionPaths) {
316            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
317            if (node == null) {
318                continue;
319            }
320
321            Object mainNodeInfo = node.getUserObject();
322            final int depth = node.getDepth();
323            if (!(mainNodeInfo instanceof TestError)) {
324                Set<Pair<String, String>> state = new HashSet<>();
325                // ask if the whole set should be ignored
326                if (asked == JOptionPane.DEFAULT_OPTION) {
327                    String[] a = new String[] {tr("Whole group"), tr("Single elements"), tr("Nothing")};
328                    asked = JOptionPane.showOptionDialog(MainApplication.getMainFrame(), tr("Ignore whole group or individual elements?"),
329                            tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
330                            a, a[1]);
331                }
332                if (asked == JOptionPane.YES_NO_OPTION) {
333                    ValidatorTreePanel.visitTestErrors(node, err -> {
334                        err.setIgnored(true);
335                        changed.set(true);
336                        state.add(new Pair<>(depth == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup(), err.getMessage()));
337                    }, processedNodes);
338                    for (Pair<String, String> s : state) {
339                        OsmValidator.addIgnoredError(s.a, s.b);
340                    }
341                    continue;
342                } else if (asked == JOptionPane.CANCEL_OPTION || asked == JOptionPane.CLOSED_OPTION) {
343                    continue;
344                }
345            }
346
347            ValidatorTreePanel.visitTestErrors(node, error -> {
348                String state = error.getIgnoreState();
349                if (state != null) {
350                    OsmValidator.addIgnoredError(state, error.getMessage());
351                }
352                changed.set(true);
353                error.setIgnored(true);
354            }, processedNodes);
355        }
356        if (changed.get()) {
357            tree.resetErrors();
358            OsmValidator.saveIgnoredErrors();
359            invalidateValidatorLayers();
360        }
361    }
362
363    /**
364     * Sets the selection of the map to the current selected items.
365     */
366    private void setSelectedItems() {
367        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
368        if (tree == null || ds == null)
369            return;
370
371        TreePath[] selectedPaths = tree.getSelectionPaths();
372        if (selectedPaths == null)
373            return;
374
375        Collection<OsmPrimitive> sel = new HashSet<>(40);
376        for (TreePath path : selectedPaths) {
377            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
378            Enumeration<?> children = node.breadthFirstEnumeration();
379            while (children.hasMoreElements()) {
380                DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
381                Object nodeInfo = childNode.getUserObject();
382                if (nodeInfo instanceof TestError) {
383                    TestError error = (TestError) nodeInfo;
384                    error.getPrimitives().stream()
385                            .filter(OsmPrimitive::isSelectable)
386                            .forEach(sel::add);
387                }
388            }
389        }
390        ds.setSelected(sel);
391    }
392
393    /**
394     * Checks for fixes in selected element and, if needed, adds to the sel
395     * parameter all selected elements
396     *
397     * @param sel
398     *            The collection where to add all selected elements
399     * @param addSelected
400     *            if true, add all selected elements to collection
401     * @return whether the selected elements has any fix
402     */
403    private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
404        AtomicBoolean hasFixes = new AtomicBoolean();
405
406        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
407        if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
408            ValidatorTreePanel.visitTestErrors(lastSelectedNode, error -> error.setSelected(false));
409        }
410
411        lastSelectedNode = node;
412        if (node != null) {
413            ValidatorTreePanel.visitTestErrors(node, error -> {
414                error.setSelected(true);
415
416                hasFixes.set(hasFixes.get() || error.isFixable());
417                if (addSelected) {
418                    error.getPrimitives().stream()
419                            .filter(OsmPrimitive::isSelectable)
420                            .forEach(sel::add);
421                }
422            });
423            selectAction.setEnabled(true);
424            if (ignoreAction != null) {
425                ignoreAction.setEnabled(!(node.getUserObject() instanceof Severity));
426            }
427        }
428
429        return hasFixes.get();
430    }
431
432    @Override
433    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
434        OsmDataLayer editLayer = e.getSource().getEditLayer();
435        if (editLayer == null) {
436            tree.setErrorList(new ArrayList<TestError>());
437        } else {
438            tree.setErrorList(editLayer.validationErrors);
439        }
440    }
441
442    /**
443     * Add a tree selection listener to the validator tree.
444     * @param listener the TreeSelectionListener
445     * @since 5958
446     */
447    public void addTreeSelectionListener(TreeSelectionListener listener) {
448        tree.addTreeSelectionListener(listener);
449    }
450
451    /**
452     * Remove the given tree selection listener from the validator tree.
453     * @param listener the TreeSelectionListener
454     * @since 5958
455     */
456    public void removeTreeSelectionListener(TreeSelectionListener listener) {
457        tree.removeTreeSelectionListener(listener);
458    }
459
460    /**
461     * Replies the popup menu handler.
462     * @return The popup menu handler
463     * @since 5958
464     */
465    public PopupMenuHandler getPopupMenuHandler() {
466        return popupMenuHandler;
467    }
468
469    /**
470     * Replies the currently selected error, or {@code null}.
471     * @return The selected error, if any.
472     * @since 5958
473     */
474    public TestError getSelectedError() {
475        Object comp = tree.getLastSelectedPathComponent();
476        if (comp instanceof DefaultMutableTreeNode) {
477            Object object = ((DefaultMutableTreeNode) comp).getUserObject();
478            if (object instanceof TestError) {
479                return (TestError) object;
480            }
481        }
482        return null;
483    }
484
485    /**
486     * Watches for double clicks and launches the popup menu.
487     */
488    class MouseEventHandler extends PopupMenuLauncher {
489
490        MouseEventHandler() {
491            super(popupMenu);
492        }
493
494        @Override
495        public void mouseClicked(MouseEvent e) {
496            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
497            if (selPath == null) {
498                tree.clearSelection();
499            }
500
501            fixAction.setEnabled(false);
502            if (ignoreAction != null) {
503                ignoreAction.setEnabled(false);
504            }
505            selectAction.setEnabled(false);
506
507            boolean isDblClick = isDoubleClick(e);
508
509            Collection<OsmPrimitive> sel = isDblClick ? new HashSet<>(40) : null;
510
511            boolean hasFixes = setSelection(sel, isDblClick);
512            fixAction.setEnabled(hasFixes);
513
514            if (isDblClick) {
515                DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
516                if (ds != null) {
517                    ds.setSelected(sel);
518                }
519                if (Config.getPref().getBoolean("validator.autozoom", false)) {
520                    AutoScaleAction.zoomTo(sel);
521                }
522            }
523        }
524
525        @Override
526        public void launch(MouseEvent e) {
527            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
528            if (selPath == null)
529                return;
530            DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
531            if (!(node.getUserObject() instanceof TestError))
532                return;
533            super.launch(e);
534        }
535    }
536
537    /**
538     * Watches for tree selection.
539     */
540    public class SelectionWatch implements TreeSelectionListener {
541        @Override
542        public void valueChanged(TreeSelectionEvent e) {
543            if (ignoreAction != null) {
544                ignoreAction.setEnabled(false);
545            }
546            selectAction.setEnabled(false);
547
548            Collection<OsmPrimitive> sel = new HashSet<>();
549            boolean hasFixes = setSelection(sel, true);
550            fixAction.setEnabled(hasFixes);
551            popupMenuHandler.setPrimitives(sel);
552            invalidateValidatorLayers();
553        }
554    }
555
556    /**
557     * A visitor that is used to compute the bounds of an error.
558     */
559    public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
560        @Override
561        public void visit(OsmPrimitive p) {
562            if (p.isUsable()) {
563                p.accept((PrimitiveVisitor) this);
564            }
565        }
566
567        @Override
568        public void visit(WaySegment ws) {
569            if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
570                return;
571            visit(ws.getFirstNode());
572            visit(ws.getSecondNode());
573        }
574
575        @Override
576        public void visit(List<Node> nodes) {
577            for (Node n: nodes) {
578                visit(n);
579            }
580        }
581
582        @Override
583        public void visit(TestError error) {
584            if (error != null) {
585                error.visitHighlighted(this);
586            }
587        }
588    }
589
590    /**
591     * Called when the selection was changed to update the list of displayed errors
592     * @param newSelection The new selection
593     */
594    public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
595        if (!Config.getPref().getBoolean(ValidatorPrefHelper.PREF_FILTER_BY_SELECTION, false))
596            return;
597        if (newSelection.isEmpty()) {
598            tree.setFilter(null);
599        }
600        tree.setFilter(new HashSet<>(newSelection));
601    }
602
603    @Override
604    public void selectionChanged(SelectionChangeEvent event) {
605        updateSelection(event.getSelection());
606        lookupAction.updateEnabledState();
607    }
608
609    /**
610     * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
611     */
612    class FixTask extends PleaseWaitRunnable {
613        private final Collection<TestError> testErrors;
614        private final List<Command> fixCommands = new ArrayList<>();
615        private boolean canceled;
616
617        FixTask(Collection<TestError> testErrors) {
618            super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
619            this.testErrors = testErrors == null ? new ArrayList<>() : testErrors;
620        }
621
622        @Override
623        protected void cancel() {
624            this.canceled = true;
625        }
626
627        @Override
628        protected void finish() {
629            // do nothing
630        }
631
632        protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
633            if (error.isFixable()) {
634                if (error.getPrimitives().stream().noneMatch(p -> p.isDeleted() || p.getDataSet() == null)) {
635                    final Command fixCommand = error.getFix();
636                    if (fixCommand != null) {
637                        SwingUtilities.invokeAndWait(fixCommand::executeCommand);
638                        fixCommands.add(fixCommand);
639                    }
640                }
641                // It is wanted to ignore an error if it said fixable, even if fixCommand was null
642                // This is to fix #5764 and #5773:
643                // a delete command, for example, may be null if all concerned primitives have already been deleted
644                error.setIgnored(true);
645            }
646        }
647
648        @Override
649        protected void realRun() throws SAXException, IOException, OsmTransferException {
650            ProgressMonitor monitor = getProgressMonitor();
651            try {
652                monitor.setTicksCount(testErrors.size());
653                final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
654                int i = 0;
655                SwingUtilities.invokeAndWait(ds::beginUpdate);
656                tree.setResetScheduled();
657                try {
658                    for (TestError error: testErrors) {
659                        i++;
660                        monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(), error.getMessage()));
661                        if (this.canceled)
662                            return;
663                        fixError(error);
664                        monitor.worked(1);
665                    }
666                } finally {
667                    SwingUtilities.invokeAndWait(ds::endUpdate);
668                }
669                monitor.subTask(tr("Updating map ..."));
670                SwingUtilities.invokeAndWait(() -> {
671                    if (!fixCommands.isEmpty()) {
672                        UndoRedoHandler.getInstance().add(
673                                fixCommands.size() > 1 ? new AutofixCommand(fixCommands) : fixCommands.get(0), false);
674                    }
675                    invalidateValidatorLayers();
676                });
677            } catch (InterruptedException e) {
678                tryUndo();
679                throw new JosmRuntimeException(e);
680            } catch (InvocationTargetException e) {
681                // FIXME: signature of realRun should have a generic checked exception we could throw here
682                throw new JosmRuntimeException(e);
683            } finally {
684                if (monitor.isCanceled()) {
685                    tryUndo();
686                }
687                GuiHelper.runInEDTAndWait(tree::resetErrors);
688                monitor.finishTask();
689            }
690        }
691
692        /**
693         * Undo commands as they were not yet added to the UndoRedo Handler
694         */
695        private void tryUndo() {
696            final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
697            int i = fixCommands.size() - 1;
698            ds.beginUpdate();
699            for (; i >= 0; i--) {
700                fixCommands.get(i).undoCommand();
701            }
702            ds.endUpdate();
703        }
704
705    }
706
707    private static void invalidateValidatorLayers() {
708        MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
709    }
710
711    @Override
712    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
713        validateAction.updateEnabledState();
714        lookupAction.updateEnabledState();
715    }
716
717    private static class AutofixCommand extends SequenceCommand {
718        AutofixCommand(Collection<Command> sequenz) {
719            super(tr("auto-fixed validator issues"), sequenz, true);
720            setSequenceComplete(true);
721        }
722
723        @Override
724        public void undoCommand() {
725            getAffectedDataSet().beginUpdate();
726            super.undoCommand();
727            getAffectedDataSet().endUpdate();
728        }
729
730        @Override
731        public boolean executeCommand() {
732            getAffectedDataSet().beginUpdate();
733            boolean rc = super.executeCommand();
734            getAffectedDataSet().endUpdate();
735            return rc;
736        }
737    }
738
739    @Override
740    public void destroy() {
741        super.destroy();
742        if (ignoreForNowAction != null) {
743            ignoreForNowAction.destroy();
744        }
745    }
746}