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    }
105
106    /**
107     * Asynchronous task for running a collection of tests against a collection of primitives
108     */
109    static class ValidationTask extends PleaseWaitRunnable {
110        private Collection<Test> tests;
111        private final Collection<OsmPrimitive> validatedPrimitives;
112        private final Collection<OsmPrimitive> formerValidatedPrimitives;
113        private boolean canceled;
114        private List<TestError> errors;
115
116        /**
117         * Constructs a new {@code ValidationTask}
118         * @param tests  the tests to run
119         * @param validatedPrimitives the collection of primitives to validate.
120         * @param formerValidatedPrimitives the last collection of primitives being validates. May be null.
121         */
122        ValidationTask(Collection<Test> tests, Collection<OsmPrimitive> validatedPrimitives,
123                Collection<OsmPrimitive> formerValidatedPrimitives) {
124            super(tr("Validating"), false /*don't ignore exceptions */);
125            this.validatedPrimitives = validatedPrimitives;
126            this.formerValidatedPrimitives = formerValidatedPrimitives;
127            this.tests = tests;
128        }
129
130        @Override
131        protected void cancel() {
132            this.canceled = true;
133        }
134
135        @Override
136        protected void finish() {
137            if (canceled) return;
138
139            // update GUI on Swing EDT
140            //
141            GuiHelper.runInEDT(() -> {
142                MapFrame map = MainApplication.getMap();
143                map.validatorDialog.tree.setErrors(errors);
144                map.validatorDialog.unfurlDialog();
145                //FIXME: nicer way to find / invalidate the corresponding error layer
146                MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
147            });
148        }
149
150        @Override
151        protected void realRun() throws SAXException, IOException,
152        OsmTransferException {
153            if (tests == null || tests.isEmpty())
154                return;
155            errors = new ArrayList<>(200);
156            getProgressMonitor().setTicksCount(tests.size() * validatedPrimitives.size());
157            int testCounter = 0;
158            for (Test test : tests) {
159                if (canceled)
160                    return;
161                testCounter++;
162                getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, tests.size(), test.getName()));
163                test.setPartialSelection(formerValidatedPrimitives != null);
164                test.startTest(getProgressMonitor().createSubTaskMonitor(validatedPrimitives.size(), false));
165                test.visit(validatedPrimitives);
166                test.endTest();
167                errors.addAll(test.getErrors());
168            }
169            tests = null;
170            if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
171                getProgressMonitor().subTask(tr("Updating ignored errors ..."));
172                for (TestError error : errors) {
173                    if (canceled) return;
174                    List<String> s = new ArrayList<>();
175                    s.add(error.getIgnoreState());
176                    s.add(error.getIgnoreGroup());
177                    s.add(error.getIgnoreSubGroup());
178                    for (String state : s) {
179                        if (state != null && OsmValidator.hasIgnoredError(state)) {
180                            error.setIgnored(true);
181                        }
182                    }
183                }
184            }
185        }
186    }
187}