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