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;
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            // fix #11567 where elapsedTime is < 0
186            long elapsedTime = Math.max(0, System.currentTimeMillis() - startTime);
187            Main.debug(tr("Test ''{0}'' completed in {1}", getName(), Utils.getDurationString(elapsedTime)));
188        }
189    }
190
191    /**
192     * Visits all primitives to be tested. These primitives are always visited
193     * in the same order: nodes first, then ways.
194     *
195     * @param selection The primitives to be tested
196     */
197    public void visit(Collection<OsmPrimitive> selection) {
198        if (progressMonitor != null) {
199            progressMonitor.setTicksCount(selection.size());
200        }
201        for (OsmPrimitive p : selection) {
202            if (isCanceled()) {
203                break;
204            }
205            if (isPrimitiveUsable(p)) {
206                p.accept(this);
207            }
208            if (progressMonitor != null) {
209                progressMonitor.worked(1);
210            }
211        }
212    }
213
214    /**
215     * Determines if the primitive is usable for tests.
216     * @param p The primitive
217     * @return {@code true} if the primitive can be tested, {@code false} otherwise
218     */
219    public boolean isPrimitiveUsable(OsmPrimitive p) {
220        return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes
221    }
222
223    @Override
224    public void visit(Node n) {}
225
226    @Override
227    public void visit(Way w) {}
228
229    @Override
230    public void visit(Relation r) {}
231
232    /**
233     * Allow the tester to manage its own preferences
234     * @param testPanel The panel to add any preferences component
235     */
236    public void addGui(JPanel testPanel) {
237        checkEnabled = new JCheckBox(name, enabled);
238        checkEnabled.setToolTipText(description);
239        testPanel.add(checkEnabled, GBC.std());
240
241        GBC a = GBC.eol();
242        a.anchor = GridBagConstraints.EAST;
243        checkBeforeUpload = new JCheckBox();
244        checkBeforeUpload.setSelected(testBeforeUpload);
245        testPanel.add(checkBeforeUpload, a);
246    }
247
248    /**
249     * Called when the used submits the preferences
250     * @return {@code true} if restart is required, {@code false} otherwise
251     */
252    public boolean ok() {
253        enabled = checkEnabled.isSelected();
254        testBeforeUpload = checkBeforeUpload.isSelected();
255        return false;
256    }
257
258    /**
259     * Fixes the error with the appropriate command
260     *
261     * @param testError error to fix
262     * @return The command to fix the error
263     */
264    public Command fixError(TestError testError) {
265        return null;
266    }
267
268    /**
269     * Returns true if the given error can be fixed automatically
270     *
271     * @param testError The error to check if can be fixed
272     * @return true if the error can be fixed
273     */
274    public boolean isFixable(TestError testError) {
275        return false;
276    }
277
278    /**
279     * Returns true if this plugin must check the uploaded data before uploading
280     * @return true if this plugin must check the uploaded data before uploading
281     */
282    public boolean testBeforeUpload() {
283        return testBeforeUpload;
284    }
285
286    /**
287     * Sets the flag that marks an upload check
288     * @param isUpload if true, the test is before upload
289     */
290    public void setBeforeUpload(boolean isUpload) {
291        this.isBeforeUpload = isUpload;
292    }
293
294    /**
295     * Returns the test name.
296     * @return The test name
297     */
298    public String getName() {
299        return name;
300    }
301
302    /**
303     * Determines if the test has been canceled.
304     * @return {@code true} if the test has been canceled, {@code false} otherwise
305     */
306    public boolean isCanceled() {
307        return progressMonitor.isCanceled();
308    }
309
310    /**
311     * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix.
312     * If all primitives have already been deleted, null is returned.
313     * @param primitives The primitives wanted for deletion
314     * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
315     */
316    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
317        Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>();
318        for (OsmPrimitive p : primitives) {
319            if (!p.isDeleted()) {
320                primitivesToDelete.add(p);
321            }
322        }
323        if (!primitivesToDelete.isEmpty()) {
324            return DeleteCommand.delete(Main.main.getEditLayer(), primitivesToDelete);
325        } else {
326            return null;
327        }
328    }
329
330    /**
331     * Determines if the specified primitive denotes a building.
332     * @param p The primitive to be tested
333     * @return True if building key is set and different from no,entrance
334     */
335    protected static final boolean isBuilding(OsmPrimitive p) {
336        String v = p.get("building");
337        return v != null && !"no".equals(v) && !"entrance".equals(v);
338    }
339
340    @Override
341    public int hashCode() {
342        final int prime = 31;
343        int result = 1;
344        result = prime * result + ((description == null) ? 0 : description.hashCode());
345        result = prime * result + ((name == null) ? 0 : name.hashCode());
346        return result;
347    }
348
349    @Override
350    public boolean equals(Object obj) {
351        if (this == obj)
352            return true;
353        if (obj == null)
354            return false;
355        if (!(obj instanceof Test))
356            return false;
357        Test other = (Test) obj;
358        if (description == null) {
359            if (other.description != null)
360                return false;
361        } else if (!description.equals(other.description))
362            return false;
363        if (name == null) {
364            if (other.name != null)
365                return false;
366        } else if (!name.equals(other.name))
367            return false;
368        return true;
369    }
370}