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.Objects;
011import java.util.Optional;
012import java.util.function.Predicate;
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.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.Utils;
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, Comparable<Test> {
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 long startTime;
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        /**
121         * Checks the tags of the given primitive.
122         * @param p The primitive to test
123         */
124        public abstract void check(OsmPrimitive p);
125
126        @Override
127        public void visit(Node n) {
128            check(n);
129        }
130
131        @Override
132        public void visit(Way w) {
133            check(w);
134        }
135
136        @Override
137        public void visit(Relation r) {
138            check(r);
139        }
140    }
141
142    /**
143     * Initializes any global data used this tester.
144     * @throws Exception When cannot initialize the test
145     */
146    public void initialize() throws Exception {
147        this.startTime = -1;
148    }
149
150    /**
151     * Start the test using a given progress monitor
152     *
153     * @param progressMonitor  the progress monitor
154     */
155    public void startTest(ProgressMonitor progressMonitor) {
156        this.progressMonitor = Optional.ofNullable(progressMonitor).orElse(NullProgressMonitor.INSTANCE);
157        String startMessage = tr("Running test {0}", name);
158        this.progressMonitor.beginTask(startMessage);
159        Logging.debug(startMessage);
160        this.errors = new ArrayList<>(30);
161        this.startTime = System.currentTimeMillis();
162    }
163
164    /**
165     * Flag notifying that this test is run over a partial data selection
166     * @param partialSelection Whether the test is on a partial selection data
167     */
168    public void setPartialSelection(boolean partialSelection) {
169        this.partialSelection = partialSelection;
170    }
171
172    /**
173     * Gets the validation errors accumulated until this moment.
174     * @return The list of errors
175     */
176    public List<TestError> getErrors() {
177        return errors;
178    }
179
180    /**
181     * Notification of the end of the test. The tester may perform additional
182     * actions and destroy the used structures.
183     * <p>
184     * If you override this method, don't forget to cleanup {@code progressMonitor}
185     * (most overrides call {@code super.endTest()} to do this).
186     */
187    public void endTest() {
188        progressMonitor.finishTask();
189        progressMonitor = null;
190        if (startTime > 0) {
191            // fix #11567 where elapsedTime is < 0
192            long elapsedTime = Math.max(0, System.currentTimeMillis() - startTime);
193            Logging.debug(tr("Test ''{0}'' completed in {1}", getName(), Utils.getDurationString(elapsedTime)));
194        }
195    }
196
197    /**
198     * Visits all primitives to be tested. These primitives are always visited
199     * in the same order: nodes first, then ways.
200     *
201     * @param selection The primitives to be tested
202     */
203    public void visit(Collection<OsmPrimitive> selection) {
204        if (progressMonitor != null) {
205            progressMonitor.setTicksCount(selection.size());
206        }
207        long cnt = 0;
208        for (OsmPrimitive p : selection) {
209            if (isCanceled()) {
210                break;
211            }
212            if (isPrimitiveUsable(p)) {
213                p.accept(this);
214            }
215            if (progressMonitor != null) {
216                progressMonitor.worked(1);
217                cnt++;
218                // add frequently changing info to progress monitor so that it
219                // doesn't seem to hang when test takes long
220                if (showElementCount && cnt % 1000 == 0) {
221                    progressMonitor.setExtraText(tr("{0} of {1} elements done", cnt, selection.size()));
222                }
223            }
224        }
225    }
226
227    /**
228     * Determines if the primitive is usable for tests.
229     * @param p The primitive
230     * @return {@code true} if the primitive can be tested, {@code false} otherwise
231     */
232    public boolean isPrimitiveUsable(OsmPrimitive p) {
233        return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes
234    }
235
236    @Override
237    public void visit(Node n) {
238        // To be overridden in subclasses
239    }
240
241    @Override
242    public void visit(Way w) {
243        // To be overridden in subclasses
244    }
245
246    @Override
247    public void visit(Relation r) {
248        // To be overridden in subclasses
249    }
250
251    /**
252     * Allow the tester to manage its own preferences
253     * @param testPanel The panel to add any preferences component
254     */
255    public void addGui(JPanel testPanel) {
256        checkEnabled = new JCheckBox(name, enabled);
257        checkEnabled.setToolTipText(description);
258        testPanel.add(checkEnabled, GBC.std());
259
260        GBC a = GBC.eol();
261        a.anchor = GridBagConstraints.EAST;
262        checkBeforeUpload = new JCheckBox();
263        checkBeforeUpload.setSelected(testBeforeUpload);
264        testPanel.add(checkBeforeUpload, a);
265    }
266
267    /**
268     * Called when the used submits the preferences
269     * @return {@code true} if restart is required, {@code false} otherwise
270     */
271    public boolean ok() {
272        enabled = checkEnabled.isSelected();
273        testBeforeUpload = checkBeforeUpload.isSelected();
274        return false;
275    }
276
277    /**
278     * Fixes the error with the appropriate command
279     *
280     * @param testError error to fix
281     * @return The command to fix the error
282     */
283    public Command fixError(TestError testError) {
284        return null;
285    }
286
287    /**
288     * Returns true if the given error can be fixed automatically
289     *
290     * @param testError The error to check if can be fixed
291     * @return true if the error can be fixed
292     */
293    public boolean isFixable(TestError testError) {
294        return false;
295    }
296
297    /**
298     * Returns true if this plugin must check the uploaded data before uploading
299     * @return true if this plugin must check the uploaded data before uploading
300     */
301    public boolean testBeforeUpload() {
302        return testBeforeUpload;
303    }
304
305    /**
306     * Sets the flag that marks an upload check
307     * @param isUpload if true, the test is before upload
308     */
309    public void setBeforeUpload(boolean isUpload) {
310        this.isBeforeUpload = isUpload;
311    }
312
313    /**
314     * Returns the test name.
315     * @return The test name
316     */
317    public String getName() {
318        return name;
319    }
320
321    /**
322     * Determines if the test has been canceled.
323     * @return {@code true} if the test has been canceled, {@code false} otherwise
324     */
325    public boolean isCanceled() {
326        return progressMonitor != null && progressMonitor.isCanceled();
327    }
328
329    /**
330     * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix.
331     * If all primitives have already been deleted, null is returned.
332     * @param primitives The primitives wanted for deletion
333     * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
334     */
335    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
336        Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>();
337        for (OsmPrimitive p : primitives) {
338            if (!p.isDeleted()) {
339                primitivesToDelete.add(p);
340            }
341        }
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    @Override
368    public int hashCode() {
369        return Objects.hash(name, description);
370    }
371
372    @Override
373    public boolean equals(Object obj) {
374        if (this == obj) return true;
375        if (obj == null || getClass() != obj.getClass()) return false;
376        Test test = (Test) obj;
377        return Objects.equals(name, test.name) &&
378               Objects.equals(description, test.description);
379    }
380
381    @Override
382    public int compareTo(Test t) {
383        return name.compareTo(t.name);
384    }
385
386    /**
387     * Free resources.
388     */
389    public void clear() {
390        errors.clear();
391    }
392
393    protected void setShowElements(boolean b) {
394        showElementCount = b;
395    }
396}