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