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