001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagConstraints;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.List;
010import java.util.Optional;
011import java.util.function.Predicate;
012import java.util.stream.Collectors;
013
014import javax.swing.JCheckBox;
015import javax.swing.JPanel;
016
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.command.DeleteCommand;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.Way;
023import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
024import org.openstreetmap.josm.data.osm.search.SearchCompiler.NotOutsideDataSourceArea;
025import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
026import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
027import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
028import org.openstreetmap.josm.gui.progress.ProgressMonitor;
029import org.openstreetmap.josm.tools.GBC;
030import org.openstreetmap.josm.tools.Logging;
031import org.openstreetmap.josm.tools.Stopwatch;
032
033/**
034 * Parent class for all validation tests.
035 * <p>
036 * A test is a primitive visitor, so that it can access to all data to be
037 * validated. These primitives are always visited in the same order: nodes
038 * first, then ways.
039 *
040 * @author frsantos
041 */
042public class Test implements OsmPrimitiveVisitor {
043
044    protected static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new NotOutsideDataSourceArea();
045    protected static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA_STRICT = new InDataSourceArea(true);
046
047    /** Name of the test */
048    protected final String name;
049
050    /** Description of the test */
051    protected final String description;
052
053    /** Whether this test is enabled. Enabled by default */
054    public boolean enabled = true;
055
056    /** The preferences check for validation */
057    protected JCheckBox checkEnabled;
058
059    /** The preferences check for validation on upload */
060    protected JCheckBox checkBeforeUpload;
061
062    /** Whether this test must check before upload. Enabled by default */
063    public boolean testBeforeUpload = true;
064
065    /** Whether this test is performing just before an upload */
066    protected boolean isBeforeUpload;
067
068    /** The list of errors */
069    protected List<TestError> errors = new ArrayList<>();
070
071    /** Whether the test is run on a partial selection data */
072    protected boolean partialSelection;
073
074    /** the progress monitor to use */
075    protected ProgressMonitor progressMonitor;
076
077    /** the start time to compute elapsed time when test finishes */
078    protected Stopwatch stopwatch;
079
080    private boolean showElementCount;
081
082    /**
083     * Constructor
084     * @param name Name of the test
085     * @param description Description of the test
086     */
087    public Test(String name, String description) {
088        this.name = name;
089        this.description = description;
090    }
091
092    /**
093     * Constructor
094     * @param name Name of the test
095     */
096    public Test(String name) {
097        this(name, null);
098    }
099
100    /**
101     * A test that forwards all primitives to {@link #check(OsmPrimitive)}.
102     */
103    public abstract static class TagTest extends Test {
104        /**
105         * Constructs a new {@code TagTest} with given name and description.
106         * @param name The test name
107         * @param description The test description
108         */
109        protected TagTest(String name, String description) {
110            super(name, description);
111        }
112
113        /**
114         * Constructs a new {@code TagTest} with given name.
115         * @param name The test name
116         */
117        protected TagTest(String name) {
118            super(name);
119        }
120
121        /**
122         * Checks the tags of the given primitive.
123         * @param p The primitive to test
124         */
125        public abstract void check(OsmPrimitive p);
126
127        @Override
128        public void visit(Node n) {
129            check(n);
130        }
131
132        @Override
133        public void visit(Way w) {
134            check(w);
135        }
136
137        @Override
138        public void visit(Relation r) {
139            check(r);
140        }
141
142        protected boolean includeOtherSeverityChecks() {
143            return isBeforeUpload ? ValidatorPrefHelper.PREF_OTHER_UPLOAD.get() : ValidatorPrefHelper.PREF_OTHER.get();
144        }
145    }
146
147    /**
148     * Initializes any global data used this tester.
149     * @throws Exception When cannot initialize the test
150     */
151    public void initialize() throws Exception {
152        this.stopwatch = Stopwatch.createStarted();
153    }
154
155    /**
156     * Start the test using a given progress monitor
157     *
158     * @param progressMonitor  the progress monitor
159     */
160    public void startTest(ProgressMonitor progressMonitor) {
161        this.progressMonitor = Optional.ofNullable(progressMonitor).orElse(NullProgressMonitor.INSTANCE);
162        String startMessage = tr("Running test {0}", name);
163        this.progressMonitor.beginTask(startMessage);
164        Logging.debug(startMessage);
165        this.errors = new ArrayList<>(30);
166        this.stopwatch = Stopwatch.createStarted();
167    }
168
169    /**
170     * Flag notifying that this test is run over a partial data selection
171     * @param partialSelection Whether the test is on a partial selection data
172     */
173    public void setPartialSelection(boolean partialSelection) {
174        this.partialSelection = partialSelection;
175    }
176
177    /**
178     * Gets the validation errors accumulated until this moment.
179     * @return The list of errors
180     */
181    public List<TestError> getErrors() {
182        return errors;
183    }
184
185    /**
186     * Notification of the end of the test. The tester may perform additional
187     * actions and destroy the used structures.
188     * <p>
189     * If you override this method, don't forget to cleanup {@code progressMonitor}
190     * (most overrides call {@code super.endTest()} to do this).
191     */
192    public void endTest() {
193        progressMonitor.finishTask();
194        progressMonitor = null;
195        if (stopwatch.elapsed() > 0) {
196            Logging.debug(stopwatch.toString(getName()));
197        }
198    }
199
200    /**
201     * Visits all primitives to be tested. These primitives are always visited
202     * in the same order: nodes first, then ways.
203     *
204     * @param selection The primitives to be tested
205     */
206    public void visit(Collection<OsmPrimitive> selection) {
207        if (progressMonitor != null) {
208            progressMonitor.setTicksCount(selection.size());
209        }
210        long cnt = 0;
211        for (OsmPrimitive p : selection) {
212            if (isCanceled()) {
213                break;
214            }
215            if (isPrimitiveUsable(p)) {
216                p.accept(this);
217            }
218            if (progressMonitor != null) {
219                progressMonitor.worked(1);
220                cnt++;
221                // add frequently changing info to progress monitor so that it
222                // doesn't seem to hang when test takes long
223                if (showElementCount && cnt % 1000 == 0) {
224                    progressMonitor.setExtraText(tr("{0} of {1} elements done", cnt, selection.size()));
225                }
226            }
227        }
228    }
229
230    /**
231     * Determines if the primitive is usable for tests.
232     * @param p The primitive
233     * @return {@code true} if the primitive can be tested, {@code false} otherwise
234     */
235    public boolean isPrimitiveUsable(OsmPrimitive p) {
236        return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes
237    }
238
239    @Override
240    public void visit(Node n) {
241        // To be overridden in subclasses
242    }
243
244    @Override
245    public void visit(Way w) {
246        // To be overridden in subclasses
247    }
248
249    @Override
250    public void visit(Relation r) {
251        // To be overridden in subclasses
252    }
253
254    /**
255     * Allow the tester to manage its own preferences
256     * @param testPanel The panel to add any preferences component
257     */
258    public void addGui(JPanel testPanel) {
259        checkEnabled = new JCheckBox(name, enabled);
260        checkEnabled.setToolTipText(description);
261        testPanel.add(checkEnabled, GBC.std());
262
263        GBC a = GBC.eol();
264        a.anchor = GridBagConstraints.LINE_END;
265        checkBeforeUpload = new JCheckBox();
266        checkBeforeUpload.setSelected(testBeforeUpload);
267        testPanel.add(checkBeforeUpload, a);
268    }
269
270    /**
271     * Called when the used submits the preferences
272     * @return {@code true} if restart is required, {@code false} otherwise
273     */
274    public boolean ok() {
275        enabled = checkEnabled.isSelected();
276        testBeforeUpload = checkBeforeUpload.isSelected();
277        return false;
278    }
279
280    /**
281     * Fixes the error with the appropriate command
282     *
283     * @param testError error to fix
284     * @return The command to fix the error
285     */
286    public Command fixError(TestError testError) {
287        return null;
288    }
289
290    /**
291     * Returns true if the given error can be fixed automatically
292     *
293     * @param testError The error to check if can be fixed
294     * @return true if the error can be fixed
295     */
296    public boolean isFixable(TestError testError) {
297        return false;
298    }
299
300    /**
301     * Returns true if this plugin must check the uploaded data before uploading
302     * @return true if this plugin must check the uploaded data before uploading
303     */
304    public boolean testBeforeUpload() {
305        return testBeforeUpload;
306    }
307
308    /**
309     * Sets the flag that marks an upload check
310     * @param isUpload if true, the test is before upload
311     */
312    public void setBeforeUpload(boolean isUpload) {
313        this.isBeforeUpload = isUpload;
314    }
315
316    /**
317     * Returns the test name.
318     * @return The test name
319     */
320    public String getName() {
321        return name;
322    }
323
324    /**
325     * Determines if the test has been canceled.
326     * @return {@code true} if the test has been canceled, {@code false} otherwise
327     */
328    public boolean isCanceled() {
329        return progressMonitor != null && progressMonitor.isCanceled();
330    }
331
332    /**
333     * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix.
334     * If all primitives have already been deleted, null is returned.
335     * @param primitives The primitives wanted for deletion
336     * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
337     */
338    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
339        Collection<OsmPrimitive> primitivesToDelete = primitives.stream()
340                .filter(p -> !p.isDeleted())
341                .collect(Collectors.toList());
342        if (!primitivesToDelete.isEmpty()) {
343            return DeleteCommand.delete(primitivesToDelete);
344        } else {
345            return null;
346        }
347    }
348
349    /**
350     * Determines if the specified primitive denotes a building.
351     * @param p The primitive to be tested
352     * @return True if building key is set and different from no,entrance
353     */
354    protected static final boolean isBuilding(OsmPrimitive p) {
355        return p.hasTagDifferent("building", "no", "entrance");
356    }
357
358    /**
359     * Determines if the specified primitive denotes a residential area.
360     * @param p The primitive to be tested
361     * @return True if landuse key is equal to residential
362     */
363    protected static final boolean isResidentialArea(OsmPrimitive p) {
364        return p.hasTag("landuse", "residential");
365    }
366
367    /**
368     * Free resources.
369     */
370    public void clear() {
371        errors.clear();
372    }
373
374    protected void setShowElements(boolean b) {
375        showElementCount = b;
376    }
377
378    /**
379     * Returns the name of this class.
380     * @return the name of this class (for ToolTip)
381     * @since 15972
382     */
383    public Object getSource() {
384        return "Java: " + this.getClass().getName();
385    }
386}