001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.awt.event.KeyEvent;
008import java.io.IOException;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.List;
012import java.util.Optional;
013
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
016import org.openstreetmap.josm.data.validation.OsmValidator;
017import org.openstreetmap.josm.data.validation.Test;
018import org.openstreetmap.josm.data.validation.TestError;
019import org.openstreetmap.josm.data.validation.util.AggregatePrimitivesVisitor;
020import org.openstreetmap.josm.gui.MainApplication;
021import org.openstreetmap.josm.gui.MapFrame;
022import org.openstreetmap.josm.gui.PleaseWaitRunnable;
023import org.openstreetmap.josm.gui.layer.ValidatorLayer;
024import org.openstreetmap.josm.gui.util.GuiHelper;
025import org.openstreetmap.josm.io.OsmTransferException;
026import org.openstreetmap.josm.tools.Shortcut;
027import org.xml.sax.SAXException;
028
029/**
030 * The action that does the validate thing.
031 * <p>
032 * This action iterates through all active tests and give them the data, so that
033 * each one can test it.
034 *
035 * @author frsantos
036 */
037public class ValidateAction extends JosmAction {
038
039    /** Last selection used to validate */
040    private transient Collection<OsmPrimitive> lastSelection;
041
042    /**
043     * Constructor
044     */
045    public ValidateAction() {
046        super(tr("Validation"), "dialogs/validator", tr("Performs the data validation"),
047                Shortcut.registerShortcut("tools:validate", tr("Tool: {0}", tr("Validation")),
048                        KeyEvent.VK_V, Shortcut.SHIFT), true);
049    }
050
051    @Override
052    public void actionPerformed(ActionEvent ev) {
053        doValidate(true);
054    }
055
056    /**
057     * Does the validation.
058     * <p>
059     * If getSelectedItems is true, the selected items (or all items, if no one
060     * is selected) are validated. If it is false, last selected items are revalidated
061     *
062     * @param getSelectedItems If selected or last selected items must be validated
063     */
064    public void doValidate(boolean getSelectedItems) {
065        MapFrame map = MainApplication.getMap();
066        if (map == null || !map.isVisible())
067            return;
068
069        OsmValidator.initializeTests();
070        OsmValidator.initializeErrorLayer();
071
072        Collection<Test> tests = OsmValidator.getEnabledTests(false);
073        if (tests.isEmpty())
074            return;
075
076        Collection<OsmPrimitive> selection;
077        if (getSelectedItems) {
078            selection = getLayerManager().getActiveDataSet().getAllSelected();
079            if (selection.isEmpty()) {
080                selection = getLayerManager().getActiveDataSet().allNonDeletedPrimitives();
081                lastSelection = null;
082            } else {
083                AggregatePrimitivesVisitor v = new AggregatePrimitivesVisitor();
084                selection = v.visit(selection);
085                lastSelection = selection;
086            }
087        } else {
088            selection = Optional.ofNullable(lastSelection).orElseGet(
089                    () -> getLayerManager().getActiveDataSet().allNonDeletedPrimitives());
090        }
091
092        MainApplication.worker.submit(new ValidationTask(tests, selection, lastSelection));
093    }
094
095    @Override
096    public void updateEnabledState() {
097        setEnabled(getLayerManager().getActiveDataSet() != null);
098    }
099
100    @Override
101    public void destroy() {
102        // Hack - this action should stay forever because it could be added to toolbar
103        // Do not call super.destroy() here
104        lastSelection = null;
105    }
106
107    /**
108     * Asynchronous task for running a collection of tests against a collection of primitives
109     */
110    static class ValidationTask extends PleaseWaitRunnable {
111        private Collection<Test> tests;
112        private final Collection<OsmPrimitive> validatedPrimitives;
113        private final Collection<OsmPrimitive> formerValidatedPrimitives;
114        private boolean canceled;
115        private List<TestError> errors;
116
117        /**
118         * Constructs a new {@code ValidationTask}
119         * @param tests  the tests to run
120         * @param validatedPrimitives the collection of primitives to validate.
121         * @param formerValidatedPrimitives the last collection of primitives being validates. May be null.
122         */
123        ValidationTask(Collection<Test> tests, Collection<OsmPrimitive> validatedPrimitives,
124                Collection<OsmPrimitive> formerValidatedPrimitives) {
125            super(tr("Validating"), false /*don't ignore exceptions */);
126            this.validatedPrimitives = validatedPrimitives;
127            this.formerValidatedPrimitives = formerValidatedPrimitives;
128            this.tests = tests;
129        }
130
131        @Override
132        protected void cancel() {
133            this.canceled = true;
134        }
135
136        @Override
137        protected void finish() {
138            if (canceled) return;
139
140            // update GUI on Swing EDT
141            //
142            GuiHelper.runInEDT(() -> {
143                MapFrame map = MainApplication.getMap();
144                map.validatorDialog.unfurlDialog();
145                map.validatorDialog.tree.setErrors(errors);
146                //FIXME: nicer way to find / invalidate the corresponding error layer
147                MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
148            });
149        }
150
151        @Override
152        protected void realRun() throws SAXException, IOException,
153        OsmTransferException {
154            if (tests == null || tests.isEmpty())
155                return;
156            errors = new ArrayList<>(200);
157            getProgressMonitor().setTicksCount(tests.size() * validatedPrimitives.size());
158            int testCounter = 0;
159            for (Test test : tests) {
160                if (canceled)
161                    return;
162                testCounter++;
163                getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, tests.size(), test.getName()));
164                test.setPartialSelection(formerValidatedPrimitives != null);
165                test.startTest(getProgressMonitor().createSubTaskMonitor(validatedPrimitives.size(), false));
166                test.visit(validatedPrimitives);
167                test.endTest();
168                errors.addAll(test.getErrors());
169                test.clear();
170            }
171            tests = null;
172            if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
173                getProgressMonitor().setCustomText("");
174                getProgressMonitor().subTask(tr("Updating ignored errors ..."));
175                for (TestError error : errors) {
176                    if (canceled) return;
177                    error.updateIgnored();
178                }
179            }
180        }
181    }
182}