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 // To be overridden in subclasses 231 } 232 233 @Override 234 public void visit(Way w) { 235 // To be overridden in subclasses 236 } 237 238 @Override 239 public void visit(Relation r) { 240 // To be overridden in subclasses 241 } 242 243 /** 244 * Allow the tester to manage its own preferences 245 * @param testPanel The panel to add any preferences component 246 */ 247 public void addGui(JPanel testPanel) { 248 checkEnabled = new JCheckBox(name, enabled); 249 checkEnabled.setToolTipText(description); 250 testPanel.add(checkEnabled, GBC.std()); 251 252 GBC a = GBC.eol(); 253 a.anchor = GridBagConstraints.EAST; 254 checkBeforeUpload = new JCheckBox(); 255 checkBeforeUpload.setSelected(testBeforeUpload); 256 testPanel.add(checkBeforeUpload, a); 257 } 258 259 /** 260 * Called when the used submits the preferences 261 * @return {@code true} if restart is required, {@code false} otherwise 262 */ 263 public boolean ok() { 264 enabled = checkEnabled.isSelected(); 265 testBeforeUpload = checkBeforeUpload.isSelected(); 266 return false; 267 } 268 269 /** 270 * Fixes the error with the appropriate command 271 * 272 * @param testError error to fix 273 * @return The command to fix the error 274 */ 275 public Command fixError(TestError testError) { 276 return null; 277 } 278 279 /** 280 * Returns true if the given error can be fixed automatically 281 * 282 * @param testError The error to check if can be fixed 283 * @return true if the error can be fixed 284 */ 285 public boolean isFixable(TestError testError) { 286 return false; 287 } 288 289 /** 290 * Returns true if this plugin must check the uploaded data before uploading 291 * @return true if this plugin must check the uploaded data before uploading 292 */ 293 public boolean testBeforeUpload() { 294 return testBeforeUpload; 295 } 296 297 /** 298 * Sets the flag that marks an upload check 299 * @param isUpload if true, the test is before upload 300 */ 301 public void setBeforeUpload(boolean isUpload) { 302 this.isBeforeUpload = isUpload; 303 } 304 305 /** 306 * Returns the test name. 307 * @return The test name 308 */ 309 public String getName() { 310 return name; 311 } 312 313 /** 314 * Determines if the test has been canceled. 315 * @return {@code true} if the test has been canceled, {@code false} otherwise 316 */ 317 public boolean isCanceled() { 318 return progressMonitor != null ? progressMonitor.isCanceled() : false; 319 } 320 321 /** 322 * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix. 323 * If all primitives have already been deleted, null is returned. 324 * @param primitives The primitives wanted for deletion 325 * @return a Delete command on all primitives that have not yet been deleted, or null otherwise 326 */ 327 protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) { 328 Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>(); 329 for (OsmPrimitive p : primitives) { 330 if (!p.isDeleted()) { 331 primitivesToDelete.add(p); 332 } 333 } 334 if (!primitivesToDelete.isEmpty()) { 335 return DeleteCommand.delete(Main.main.getEditLayer(), primitivesToDelete); 336 } else { 337 return null; 338 } 339 } 340 341 /** 342 * Determines if the specified primitive denotes a building. 343 * @param p The primitive to be tested 344 * @return True if building key is set and different from no,entrance 345 */ 346 protected static final boolean isBuilding(OsmPrimitive p) { 347 String v = p.get("building"); 348 return v != null && !"no".equals(v) && !"entrance".equals(v); 349 } 350 351 @Override 352 public int hashCode() { 353 return Objects.hash(name, description); 354 } 355 356 @Override 357 public boolean equals(Object obj) { 358 if (this == obj) return true; 359 if (obj == null || getClass() != obj.getClass()) return false; 360 Test test = (Test) obj; 361 return Objects.equals(name, test.name) && 362 Objects.equals(description, test.description); 363 } 364}