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