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<TestError>(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     * Initializes any global data used this tester.
091     * @throws Exception When cannot initialize the test
092     */
093    public void initialize() throws Exception {
094        this.startTime = -1;
095    }
096
097    /**
098     * Start the test using a given progress monitor
099     *
100     * @param progressMonitor  the progress monitor
101     */
102    public void startTest(ProgressMonitor progressMonitor) {
103        if (progressMonitor == null) {
104            this.progressMonitor = NullProgressMonitor.INSTANCE;
105        } else {
106            this.progressMonitor = progressMonitor;
107        }
108        String startMessage = tr("Running test {0}", name);
109        this.progressMonitor.beginTask(startMessage);
110        Main.debug(startMessage);
111        this.errors = new ArrayList<TestError>(30);
112        this.startTime = System.currentTimeMillis();
113    }
114
115    /**
116     * Flag notifying that this test is run over a partial data selection
117     * @param partialSelection Whether the test is on a partial selection data
118     */
119    public void setPartialSelection(boolean partialSelection) {
120        this.partialSelection = partialSelection;
121    }
122
123    /**
124     * Gets the validation errors accumulated until this moment.
125     * @return The list of errors
126     */
127    public List<TestError> getErrors() {
128        return errors;
129    }
130
131    /**
132     * Notification of the end of the test. The tester may perform additional
133     * actions and destroy the used structures.
134     * <p>
135     * If you override this method, don't forget to cleanup {@link #progressMonitor}
136     * (most overrides call {@code super.endTest()} to do this).
137     */
138    public void endTest() {
139        progressMonitor.finishTask();
140        progressMonitor = null;
141        if (startTime > 0) {
142            long elapsedTime = System.currentTimeMillis() - startTime;
143            Main.info(tr("Test ''{0}'' completed in {1}", getName(), Utils.getDurationString(elapsedTime)));
144        }
145    }
146
147    /**
148     * Visits all primitives to be tested. These primitives are always visited
149     * in the same order: nodes first, then ways.
150     *
151     * @param selection The primitives to be tested
152     */
153    public void visit(Collection<OsmPrimitive> selection) {
154        progressMonitor.setTicksCount(selection.size());
155        for (OsmPrimitive p : selection) {
156            if (isPrimitiveUsable(p)) {
157                p.accept(this);
158            }
159            progressMonitor.worked(1);
160        }
161    }
162
163    public boolean isPrimitiveUsable(OsmPrimitive p) {
164        return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes
165    }
166
167    @Override
168    public void visit(Node n) {}
169
170    @Override
171    public void visit(Way w) {}
172
173    @Override
174    public void visit(Relation r) {}
175
176    /**
177     * Allow the tester to manage its own preferences
178     * @param testPanel The panel to add any preferences component
179     */
180    public void addGui(JPanel testPanel) {
181        checkEnabled = new JCheckBox(name, enabled);
182        checkEnabled.setToolTipText(description);
183        testPanel.add(checkEnabled, GBC.std());
184
185        GBC a = GBC.eol();
186        a.anchor = GridBagConstraints.EAST;
187        checkBeforeUpload = new JCheckBox();
188        checkBeforeUpload.setSelected(testBeforeUpload);
189        testPanel.add(checkBeforeUpload, a);
190    }
191
192    /**
193     * Called when the used submits the preferences
194     */
195    public boolean ok() {
196        enabled = checkEnabled.isSelected();
197        testBeforeUpload = checkBeforeUpload.isSelected();
198        return false;
199    }
200
201    /**
202     * Fixes the error with the appropriate command
203     *
204     * @param testError
205     * @return The command to fix the error
206     */
207    public Command fixError(TestError testError) {
208        return null;
209    }
210
211    /**
212     * Returns true if the given error can be fixed automatically
213     *
214     * @param testError The error to check if can be fixed
215     * @return true if the error can be fixed
216     */
217    public boolean isFixable(TestError testError) {
218        return false;
219    }
220
221    /**
222     * Returns true if this plugin must check the uploaded data before uploading
223     * @return true if this plugin must check the uploaded data before uploading
224     */
225    public boolean testBeforeUpload() {
226        return testBeforeUpload;
227    }
228
229    /**
230     * Sets the flag that marks an upload check
231     * @param isUpload if true, the test is before upload
232     */
233    public void setBeforeUpload(boolean isUpload) {
234        this.isBeforeUpload = isUpload;
235    }
236
237    public String getName() {
238        return name;
239    }
240
241    public boolean isCanceled() {
242        return progressMonitor.isCanceled();
243    }
244
245    /**
246     * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix.
247     * If all primitives have already been deleted, null is returned.
248     * @param primitives The primitives wanted for deletion
249     * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
250     */
251    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
252        Collection<OsmPrimitive> primitivesToDelete = new ArrayList<OsmPrimitive>();
253        for (OsmPrimitive p : primitives) {
254            if (!p.isDeleted()) {
255                primitivesToDelete.add(p);
256            }
257        }
258        if (!primitivesToDelete.isEmpty()) {
259            return DeleteCommand.delete(Main.main.getEditLayer(), primitivesToDelete);
260        } else {
261            return null;
262        }
263    }
264
265    /**
266     * Determines if the specified primitive denotes a building.
267     * @param p The primitive to be tested
268     * @return True if building key is set and different from no,entrance
269     */
270    protected static final boolean isBuilding(OsmPrimitive p) {
271        String v = p.get("building");
272        return v != null && !v.equals("no") && !v.equals("entrance");
273    }
274}