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;
011
012import javax.swing.JCheckBox;
013import javax.swing.JPanel;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.actions.search.SearchCompiler.NotOutsideDataSourceArea;
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.visitor.AbstractVisitor;
024import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
025import org.openstreetmap.josm.gui.progress.ProgressMonitor;
026import org.openstreetmap.josm.tools.GBC;
027import org.openstreetmap.josm.tools.Predicate;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * Parent class for all validation tests.
032 * <p>
033 * A test is a primitive visitor, so that it can access to all data to be
034 * validated. These primitives are always visited in the same order: nodes
035 * first, then ways.
036 *
037 * @author frsantos
038 */
039public class Test extends AbstractVisitor {
040
041    protected static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new NotOutsideDataSourceArea();
042
043    /** Name of the test */
044    protected final String name;
045
046    /** Description of the test */
047    protected final String description;
048
049    /** Whether this test is enabled. Enabled by default */
050    public boolean enabled = true;
051
052    /** The preferences check for validation */
053    protected JCheckBox checkEnabled;
054
055    /** The preferences check for validation on upload */
056    protected JCheckBox checkBeforeUpload;
057
058    /** Whether this test must check before upload. Enabled by default */
059    public boolean testBeforeUpload = true;
060
061    /** Whether this test is performing just before an upload */
062    protected boolean isBeforeUpload;
063
064    /** The list of errors */
065    protected List<TestError> errors = new ArrayList<>(30);
066
067    /** Whether the test is run on a partial selection data */
068    protected boolean partialSelection;
069
070    /** the progress monitor to use */
071    protected ProgressMonitor progressMonitor;
072
073    /** the start time to compute elapsed time when test finishes */
074    protected long startTime;
075
076    /**
077     * Constructor
078     * @param name Name of the test
079     * @param description Description of the test
080     */
081    public Test(String name, String description) {
082        this.name = name;
083        this.description = description;
084    }
085
086    /**
087     * Constructor
088     * @param name Name of the test
089     */
090    public Test(String name) {
091        this(name, null);
092    }
093
094    /**
095     * A test that forwards all primitives to {@link #check(OsmPrimitive)}.
096     */
097    public abstract static class TagTest extends Test {
098        /**
099         * Constructs a new {@code TagTest} with given name and description.
100         * @param name The test name
101         * @param description The test description
102         */
103        public TagTest(String name, String description) {
104            super(name, description);
105        }
106
107        /**
108         * Constructs a new {@code TagTest} with given name.
109         * @param name The test name
110         */
111        public TagTest(String name) {
112            super(name);
113        }
114
115        /**
116         * Checks the tags of the given primitive.
117         * @param p The primitive to test
118         */
119        public abstract void check(final OsmPrimitive p);
120
121        @Override
122        public void visit(Node n) {
123            check(n);
124        }
125
126        @Override
127        public void visit(Way w) {
128            check(w);
129        }
130
131        @Override
132        public void visit(Relation r) {
133            check(r);
134        }
135    }
136
137    /**
138     * Initializes any global data used this tester.
139     * @throws Exception When cannot initialize the test
140     */
141    public void initialize() throws Exception {
142        this.startTime = -1;
143    }
144
145    /**
146     * Start the test using a given progress monitor
147     *
148     * @param progressMonitor  the progress monitor
149     */
150    public void startTest(ProgressMonitor progressMonitor) {
151        if (progressMonitor == null) {
152            this.progressMonitor = NullProgressMonitor.INSTANCE;
153        } else {
154            this.progressMonitor = progressMonitor;
155        }
156        String startMessage = tr("Running test {0}", name);
157        this.progressMonitor.beginTask(startMessage);
158        Main.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            Main.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        for (OsmPrimitive p : selection) {
207            if (isCanceled()) {
208                break;
209            }
210            if (isPrimitiveUsable(p)) {
211                p.accept(this);
212            }
213            if (progressMonitor != null) {
214                progressMonitor.worked(1);
215            }
216        }
217    }
218
219    /**
220     * Determines if the primitive is usable for tests.
221     * @param p The primitive
222     * @return {@code true} if the primitive can be tested, {@code false} otherwise
223     */
224    public boolean isPrimitiveUsable(OsmPrimitive p) {
225        return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes
226    }
227
228    @Override
229    public void visit(Node n) {}
230
231    @Override
232    public void visit(Way w) {}
233
234    @Override
235    public void visit(Relation r) {}
236
237    /**
238     * Allow the tester to manage its own preferences
239     * @param testPanel The panel to add any preferences component
240     */
241    public void addGui(JPanel testPanel) {
242        checkEnabled = new JCheckBox(name, enabled);
243        checkEnabled.setToolTipText(description);
244        testPanel.add(checkEnabled, GBC.std());
245
246        GBC a = GBC.eol();
247        a.anchor = GridBagConstraints.EAST;
248        checkBeforeUpload = new JCheckBox();
249        checkBeforeUpload.setSelected(testBeforeUpload);
250        testPanel.add(checkBeforeUpload, a);
251    }
252
253    /**
254     * Called when the used submits the preferences
255     * @return {@code true} if restart is required, {@code false} otherwise
256     */
257    public boolean ok() {
258        enabled = checkEnabled.isSelected();
259        testBeforeUpload = checkBeforeUpload.isSelected();
260        return false;
261    }
262
263    /**
264     * Fixes the error with the appropriate command
265     *
266     * @param testError error to fix
267     * @return The command to fix the error
268     */
269    public Command fixError(TestError testError) {
270        return null;
271    }
272
273    /**
274     * Returns true if the given error can be fixed automatically
275     *
276     * @param testError The error to check if can be fixed
277     * @return true if the error can be fixed
278     */
279    public boolean isFixable(TestError testError) {
280        return false;
281    }
282
283    /**
284     * Returns true if this plugin must check the uploaded data before uploading
285     * @return true if this plugin must check the uploaded data before uploading
286     */
287    public boolean testBeforeUpload() {
288        return testBeforeUpload;
289    }
290
291    /**
292     * Sets the flag that marks an upload check
293     * @param isUpload if true, the test is before upload
294     */
295    public void setBeforeUpload(boolean isUpload) {
296        this.isBeforeUpload = isUpload;
297    }
298
299    /**
300     * Returns the test name.
301     * @return The test name
302     */
303    public String getName() {
304        return name;
305    }
306
307    /**
308     * Determines if the test has been canceled.
309     * @return {@code true} if the test has been canceled, {@code false} otherwise
310     */
311    public boolean isCanceled() {
312        return progressMonitor != null ? progressMonitor.isCanceled() : false;
313    }
314
315    /**
316     * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix.
317     * If all primitives have already been deleted, null is returned.
318     * @param primitives The primitives wanted for deletion
319     * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
320     */
321    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
322        Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>();
323        for (OsmPrimitive p : primitives) {
324            if (!p.isDeleted()) {
325                primitivesToDelete.add(p);
326            }
327        }
328        if (!primitivesToDelete.isEmpty()) {
329            return DeleteCommand.delete(Main.main.getEditLayer(), primitivesToDelete);
330        } else {
331            return null;
332        }
333    }
334
335    /**
336     * Determines if the specified primitive denotes a building.
337     * @param p The primitive to be tested
338     * @return True if building key is set and different from no,entrance
339     */
340    protected static final boolean isBuilding(OsmPrimitive p) {
341        String v = p.get("building");
342        return v != null && !"no".equals(v) && !"entrance".equals(v);
343    }
344
345    @Override
346    public int hashCode() {
347        return Objects.hash(name, description);
348    }
349
350    @Override
351    public boolean equals(Object obj) {
352        if (this == obj) return true;
353        if (obj == null || getClass() != obj.getClass()) return false;
354        Test test = (Test) obj;
355        return Objects.equals(name, test.name) &&
356                Objects.equals(description, test.description);
357    }
358}