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