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("Windows: {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(AutoScaleAction.AutoScaleMode.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            final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
240            if (ds == null || ds.selectionEmpty()) {
241                setEnabled(false);
242            } else {
243                boolean found = tree.getErrors().stream()
244                        .anyMatch(e -> e.getPrimitives().stream().anyMatch(OsmPrimitive::isSelected));
245                setEnabled(found);
246            }
247        }
248    }
249
250    @Override
251    public void showNotify() {
252        DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
253        SelectionEventManager.getInstance().addSelectionListener(this);
254        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
255        if (ds != null) {
256            updateSelection(ds.getAllSelected());
257        }
258        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
259
260    }
261
262    @Override
263    public void hideNotify() {
264        DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
265        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
266        SelectionEventManager.getInstance().removeSelectionListener(this);
267    }
268
269    @Override
270    public void setVisible(boolean v) {
271        if (tree != null) {
272            tree.setVisible(v);
273        }
274        super.setVisible(v);
275    }
276
277    /**
278     * Fix selected errors
279     */
280    private void fixErrors() {
281        TreePath[] selectionPaths = tree.getSelectionPaths();
282        if (selectionPaths == null)
283            return;
284
285        Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
286
287        List<TestError> errorsToFix = new LinkedList<>();
288        for (TreePath path : selectionPaths) {
289            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
290            if (node != null) {
291                ValidatorTreePanel.visitTestErrors(node, errorsToFix::add, processedNodes);
292            }
293        }
294
295        // run fix task asynchronously
296        MainApplication.worker.submit(new FixTask(errorsToFix));
297    }
298
299    /**
300     * Set selected errors to ignore state
301     */
302    private void ignoreErrors() {
303        int asked = JOptionPane.DEFAULT_OPTION;
304        AtomicBoolean changed = new AtomicBoolean();
305        TreePath[] selectionPaths = tree.getSelectionPaths();
306        if (selectionPaths == null)
307            return;
308
309        Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
310        for (TreePath path : selectionPaths) {
311            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
312            if (node == null) {
313                continue;
314            }
315
316            Object mainNodeInfo = node.getUserObject();
317            final int depth = node.getDepth();
318            if (!(mainNodeInfo instanceof TestError)) {
319                Set<Pair<String, String>> state = new HashSet<>();
320                // ask if the whole set should be ignored
321                if (asked == JOptionPane.DEFAULT_OPTION) {
322                    String[] a = {tr("Whole group"), tr("Single elements"), tr("Nothing")};
323                    asked = JOptionPane.showOptionDialog(MainApplication.getMainFrame(), tr("Ignore whole group or individual elements?"),
324                            tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
325                            a, a[1]);
326                }
327                if (asked == JOptionPane.YES_NO_OPTION) {
328                    ValidatorTreePanel.visitTestErrors(node, err -> {
329                        err.setIgnored(true);
330                        changed.set(true);
331                        state.add(new Pair<>(depth == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup(), err.getMessage()));
332                    }, processedNodes);
333                    for (Pair<String, String> s : state) {
334                        OsmValidator.addIgnoredError(s.a, s.b);
335                    }
336                    continue;
337                } else if (asked == JOptionPane.CANCEL_OPTION || asked == JOptionPane.CLOSED_OPTION) {
338                    continue;
339                }
340            }
341
342            ValidatorTreePanel.visitTestErrors(node, error -> {
343                String state = error.getIgnoreState();
344                if (state != null) {
345                    OsmValidator.addIgnoredError(state, error.getMessage());
346                }
347                changed.set(true);
348                error.setIgnored(true);
349            }, processedNodes);
350        }
351        if (changed.get()) {
352            tree.resetErrors();
353            OsmValidator.saveIgnoredErrors();
354            invalidateValidatorLayers();
355        }
356    }
357
358    /**
359     * Sets the selection of the map to the current selected items.
360     */
361    private void setSelectedItems() {
362        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
363        if (tree == null || ds == null)
364            return;
365
366        TreePath[] selectedPaths = tree.getSelectionPaths();
367        if (selectedPaths == null)
368            return;
369
370        Collection<OsmPrimitive> sel = new HashSet<>(40);
371        for (TreePath path : selectedPaths) {
372            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
373            Enumeration<?> children = node.breadthFirstEnumeration();
374            while (children.hasMoreElements()) {
375                DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
376                Object nodeInfo = childNode.getUserObject();
377                if (nodeInfo instanceof TestError) {
378                    TestError error = (TestError) nodeInfo;
379                    error.getPrimitives().stream()
380                            .filter(OsmPrimitive::isSelectable)
381                            .forEach(sel::add);
382                }
383            }
384        }
385        ds.setSelected(sel);
386    }
387
388    /**
389     * Checks for fixes in selected element and, if needed, adds to the sel
390     * parameter all selected elements
391     *
392     * @param sel
393     *            The collection where to add all selected elements
394     * @param addSelected
395     *            if true, add all selected elements to collection
396     * @return whether the selected elements has any fix
397     */
398    private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
399        AtomicBoolean hasFixes = new AtomicBoolean();
400
401        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
402        if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
403            ValidatorTreePanel.visitTestErrors(lastSelectedNode, error -> error.setSelected(false));
404        }
405
406        lastSelectedNode = node;
407        if (node != null) {
408            ValidatorTreePanel.visitTestErrors(node, error -> {
409                error.setSelected(true);
410
411                hasFixes.set(hasFixes.get() || error.isFixable());
412                if (addSelected) {
413                    error.getPrimitives().stream()
414                            .filter(OsmPrimitive::isSelectable)
415                            .forEach(sel::add);
416                }
417            });
418            selectAction.setEnabled(true);
419            if (ignoreAction != null) {
420                ignoreAction.setEnabled(!(node.getUserObject() instanceof Severity));
421            }
422        }
423
424        return hasFixes.get();
425    }
426
427    @Override
428    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
429        OsmDataLayer editLayer = e.getSource().getEditLayer();
430        if (editLayer == null) {
431            tree.setErrorList(new ArrayList<TestError>());
432        } else {
433            tree.setErrorList(editLayer.validationErrors);
434        }
435    }
436
437    /**
438     * Add a tree selection listener to the validator tree.
439     * @param listener the TreeSelectionListener
440     * @since 5958
441     */
442    public void addTreeSelectionListener(TreeSelectionListener listener) {
443        tree.addTreeSelectionListener(listener);
444    }
445
446    /**
447     * Remove the given tree selection listener from the validator tree.
448     * @param listener the TreeSelectionListener
449     * @since 5958
450     */
451    public void removeTreeSelectionListener(TreeSelectionListener listener) {
452        tree.removeTreeSelectionListener(listener);
453    }
454
455    /**
456     * Replies the popup menu handler.
457     * @return The popup menu handler
458     * @since 5958
459     */
460    public PopupMenuHandler getPopupMenuHandler() {
461        return popupMenuHandler;
462    }
463
464    /**
465     * Replies the currently selected error, or {@code null}.
466     * @return The selected error, if any.
467     * @since 5958
468     */
469    public TestError getSelectedError() {
470        Object comp = tree.getLastSelectedPathComponent();
471        if (comp instanceof DefaultMutableTreeNode) {
472            Object object = ((DefaultMutableTreeNode) comp).getUserObject();
473            if (object instanceof TestError) {
474                return (TestError) object;
475            }
476        }
477        return null;
478    }
479
480    /**
481     * Watches for double clicks and launches the popup menu.
482     */
483    class MouseEventHandler extends PopupMenuLauncher {
484
485        MouseEventHandler() {
486            super(popupMenu);
487        }
488
489        @Override
490        public void mouseClicked(MouseEvent e) {
491            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
492            if (selPath == null) {
493                tree.clearSelection();
494            }
495
496            fixAction.setEnabled(false);
497            if (ignoreAction != null) {
498                ignoreAction.setEnabled(false);
499            }
500            selectAction.setEnabled(false);
501
502            boolean isDblClick = isDoubleClick(e);
503
504            Collection<OsmPrimitive> sel = isDblClick ? new HashSet<>(40) : null;
505
506            boolean hasFixes = setSelection(sel, isDblClick);
507            fixAction.setEnabled(hasFixes);
508
509            if (isDblClick) {
510                DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
511                if (ds != null) {
512                    ds.setSelected(sel);
513                }
514                if (Config.getPref().getBoolean("validator.autozoom", false)) {
515                    AutoScaleAction.zoomTo(sel);
516                }
517            }
518        }
519
520        @Override
521        public void launch(MouseEvent e) {
522            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
523            if (selPath == null)
524                return;
525            DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
526            if (!(node.getUserObject() instanceof TestError))
527                return;
528            super.launch(e);
529        }
530    }
531
532    /**
533     * Watches for tree selection.
534     */
535    public class SelectionWatch implements TreeSelectionListener {
536        @Override
537        public void valueChanged(TreeSelectionEvent e) {
538            if (ignoreAction != null) {
539                ignoreAction.setEnabled(false);
540            }
541            selectAction.setEnabled(false);
542
543            Collection<OsmPrimitive> sel = new HashSet<>();
544            boolean hasFixes = setSelection(sel, true);
545            fixAction.setEnabled(hasFixes);
546            popupMenuHandler.setPrimitives(sel);
547            invalidateValidatorLayers();
548        }
549    }
550
551    /**
552     * A visitor that is used to compute the bounds of an error.
553     */
554    public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
555        @Override
556        public void visit(OsmPrimitive p) {
557            if (p.isUsable()) {
558                p.accept((PrimitiveVisitor) this);
559            }
560        }
561
562        @Override
563        public void visit(WaySegment ws) {
564            if (ws.getLowerIndex() < 0 || ws.getUpperIndex() >= ws.getWay().getNodesCount())
565                return;
566            visit(ws.getFirstNode());
567            visit(ws.getSecondNode());
568        }
569
570        @Override
571        public void visit(List<Node> nodes) {
572            for (Node n: nodes) {
573                visit(n);
574            }
575        }
576
577        @Override
578        public void visit(TestError error) {
579            if (error != null) {
580                error.visitHighlighted(this);
581            }
582        }
583    }
584
585    /**
586     * Called when the selection was changed to update the list of displayed errors
587     * @param newSelection The new selection
588     */
589    public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
590        if (!Config.getPref().getBoolean(ValidatorPrefHelper.PREF_FILTER_BY_SELECTION, false))
591            return;
592        if (newSelection.isEmpty()) {
593            tree.setFilter(null);
594        }
595        tree.setFilter(new HashSet<>(newSelection));
596    }
597
598    @Override
599    public void selectionChanged(SelectionChangeEvent event) {
600        updateSelection(event.getSelection());
601        lookupAction.updateEnabledState();
602    }
603
604    /**
605     * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
606     */
607    class FixTask extends PleaseWaitRunnable {
608        private final Collection<TestError> testErrors;
609        private final List<Command> fixCommands = new ArrayList<>();
610        private boolean canceled;
611
612        FixTask(Collection<TestError> testErrors) {
613            super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
614            this.testErrors = testErrors == null ? new ArrayList<>() : testErrors;
615        }
616
617        @Override
618        protected void cancel() {
619            this.canceled = true;
620        }
621
622        @Override
623        protected void finish() {
624            // do nothing
625        }
626
627        protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
628            if (error.isFixable()) {
629                if (error.getPrimitives().stream().noneMatch(p -> p.isDeleted() || p.getDataSet() == null)) {
630                    final Command fixCommand = error.getFix();
631                    if (fixCommand != null) {
632                        SwingUtilities.invokeAndWait(fixCommand::executeCommand);
633                        fixCommands.add(fixCommand);
634                    }
635                }
636                // It is wanted to ignore an error if it said fixable, even if fixCommand was null
637                // This is to fix #5764 and #5773:
638                // a delete command, for example, may be null if all concerned primitives have already been deleted
639                error.setIgnored(true);
640            }
641        }
642
643        @Override
644        protected void realRun() throws SAXException, IOException, OsmTransferException {
645            ProgressMonitor monitor = getProgressMonitor();
646            try {
647                monitor.setTicksCount(testErrors.size());
648                final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
649                int i = 0;
650                SwingUtilities.invokeAndWait(ds::beginUpdate);
651                tree.setResetScheduled();
652                try {
653                    for (TestError error: testErrors) {
654                        i++;
655                        monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(), error.getMessage()));
656                        if (this.canceled)
657                            return;
658                        fixError(error);
659                        monitor.worked(1);
660                    }
661                } finally {
662                    SwingUtilities.invokeAndWait(ds::endUpdate);
663                }
664                monitor.subTask(tr("Updating map ..."));
665                SwingUtilities.invokeAndWait(() -> {
666                    if (!fixCommands.isEmpty()) {
667                        UndoRedoHandler.getInstance().add(
668                                fixCommands.size() > 1 ? new AutofixCommand(fixCommands) : fixCommands.get(0), false);
669                    }
670                    invalidateValidatorLayers();
671                });
672            } catch (InterruptedException e) {
673                tryUndo();
674                throw new JosmRuntimeException(e);
675            } catch (InvocationTargetException e) {
676                // FIXME: signature of realRun should have a generic checked exception we could throw here
677                throw new JosmRuntimeException(e);
678            } finally {
679                if (monitor.isCanceled()) {
680                    tryUndo();
681                }
682                GuiHelper.runInEDTAndWait(tree::resetErrors);
683                monitor.finishTask();
684            }
685        }
686
687        /**
688         * Undo commands as they were not yet added to the UndoRedo Handler
689         */
690        private void tryUndo() {
691            MainApplication.getLayerManager().getActiveDataSet().update(() -> {
692                for (int i = fixCommands.size() - 1; i >= 0; i--) {
693                    fixCommands.get(i).undoCommand();
694                }
695            });
696        }
697    }
698
699    private static void invalidateValidatorLayers() {
700        MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
701    }
702
703    @Override
704    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
705        validateAction.updateEnabledState();
706        lookupAction.updateEnabledState();
707    }
708
709    private static class AutofixCommand extends SequenceCommand {
710        AutofixCommand(Collection<Command> sequenz) {
711            super(tr("auto-fixed validator issues"), sequenz, true);
712            setSequenceComplete(true);
713        }
714
715        @Override
716        public void undoCommand() {
717            getAffectedDataSet().update(super::undoCommand);
718        }
719
720        @Override
721        public boolean executeCommand() {
722            return getAffectedDataSet().update(super::executeCommand);
723        }
724    }
725
726    @Override
727    public void destroy() {
728        super.destroy();
729        if (ignoreForNowAction != null) {
730            ignoreForNowAction.destroy();
731        }
732    }
733}