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.Optional; 011import java.util.function.Predicate; 012 013import javax.swing.JCheckBox; 014import javax.swing.JPanel; 015 016import org.openstreetmap.josm.command.Command; 017import org.openstreetmap.josm.command.DeleteCommand; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.Relation; 021import org.openstreetmap.josm.data.osm.Way; 022import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea; 023import org.openstreetmap.josm.data.osm.search.SearchCompiler.NotOutsideDataSourceArea; 024import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 025import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 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.Stopwatch; 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 { 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 Stopwatch stopwatch; 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 @Override 121 public boolean isPrimitiveUsable(OsmPrimitive p) { 122 return super.isPrimitiveUsable(p) && p.isTagged(); 123 } 124 125 /** 126 * Checks the tags of the given primitive. 127 * @param p The primitive to test 128 */ 129 public abstract void check(OsmPrimitive p); 130 131 @Override 132 public void visit(Node n) { 133 check(n); 134 } 135 136 @Override 137 public void visit(Way w) { 138 check(w); 139 } 140 141 @Override 142 public void visit(Relation r) { 143 check(r); 144 } 145 146 protected boolean includeOtherSeverityChecks() { 147 return isBeforeUpload ? ValidatorPrefHelper.PREF_OTHER_UPLOAD.get() : ValidatorPrefHelper.PREF_OTHER.get(); 148 } 149 } 150 151 /** 152 * Initializes any global data used this tester. 153 * @throws Exception When cannot initialize the test 154 */ 155 public void initialize() throws Exception { 156 this.stopwatch = Stopwatch.createStarted(); 157 } 158 159 /** 160 * Start the test using a given progress monitor 161 * 162 * @param progressMonitor the progress monitor 163 */ 164 public void startTest(ProgressMonitor progressMonitor) { 165 this.progressMonitor = Optional.ofNullable(progressMonitor).orElse(NullProgressMonitor.INSTANCE); 166 String startMessage = tr("Running test {0}", name); 167 this.progressMonitor.beginTask(startMessage); 168 Logging.debug(startMessage); 169 this.errors = new ArrayList<>(30); 170 this.stopwatch = Stopwatch.createStarted(); 171 } 172 173 /** 174 * Flag notifying that this test is run over a partial data selection 175 * @param partialSelection Whether the test is on a partial selection data 176 */ 177 public void setPartialSelection(boolean partialSelection) { 178 this.partialSelection = partialSelection; 179 } 180 181 /** 182 * Gets the validation errors accumulated until this moment. 183 * @return The list of errors 184 */ 185 public List<TestError> getErrors() { 186 return errors; 187 } 188 189 /** 190 * Notification of the end of the test. The tester may perform additional 191 * actions and destroy the used structures. 192 * <p> 193 * If you override this method, don't forget to cleanup {@code progressMonitor} 194 * (most overrides call {@code super.endTest()} to do this). 195 */ 196 public void endTest() { 197 progressMonitor.finishTask(); 198 progressMonitor = null; 199 if (stopwatch.elapsed() > 0) { 200 Logging.debug(tr("Test ''{0}'' completed in {1}", getName(), stopwatch)); 201 } 202 } 203 204 /** 205 * Visits all primitives to be tested. These primitives are always visited 206 * in the same order: nodes first, then ways. 207 * 208 * @param selection The primitives to be tested 209 */ 210 public void visit(Collection<OsmPrimitive> selection) { 211 if (progressMonitor != null) { 212 progressMonitor.setTicksCount(selection.size()); 213 } 214 long cnt = 0; 215 for (OsmPrimitive p : selection) { 216 if (isCanceled()) { 217 break; 218 } 219 if (isPrimitiveUsable(p)) { 220 p.accept(this); 221 } 222 if (progressMonitor != null) { 223 progressMonitor.worked(1); 224 cnt++; 225 // add frequently changing info to progress monitor so that it 226 // doesn't seem to hang when test takes long 227 if (showElementCount && cnt % 1000 == 0) { 228 progressMonitor.setExtraText(tr("{0} of {1} elements done", cnt, selection.size())); 229 } 230 } 231 } 232 } 233 234 /** 235 * Determines if the primitive is usable for tests. 236 * @param p The primitive 237 * @return {@code true} if the primitive can be tested, {@code false} otherwise 238 */ 239 public boolean isPrimitiveUsable(OsmPrimitive p) { 240 return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes 241 } 242 243 @Override 244 public void visit(Node n) { 245 // To be overridden in subclasses 246 } 247 248 @Override 249 public void visit(Way w) { 250 // To be overridden in subclasses 251 } 252 253 @Override 254 public void visit(Relation r) { 255 // To be overridden in subclasses 256 } 257 258 /** 259 * Allow the tester to manage its own preferences 260 * @param testPanel The panel to add any preferences component 261 */ 262 public void addGui(JPanel testPanel) { 263 checkEnabled = new JCheckBox(name, enabled); 264 checkEnabled.setToolTipText(description); 265 testPanel.add(checkEnabled, GBC.std()); 266 267 GBC a = GBC.eol(); 268 a.anchor = GridBagConstraints.EAST; 269 checkBeforeUpload = new JCheckBox(); 270 checkBeforeUpload.setSelected(testBeforeUpload); 271 testPanel.add(checkBeforeUpload, a); 272 } 273 274 /** 275 * Called when the used submits the preferences 276 * @return {@code true} if restart is required, {@code false} otherwise 277 */ 278 public boolean ok() { 279 enabled = checkEnabled.isSelected(); 280 testBeforeUpload = checkBeforeUpload.isSelected(); 281 return false; 282 } 283 284 /** 285 * Fixes the error with the appropriate command 286 * 287 * @param testError error to fix 288 * @return The command to fix the error 289 */ 290 public Command fixError(TestError testError) { 291 return null; 292 } 293 294 /** 295 * Returns true if the given error can be fixed automatically 296 * 297 * @param testError The error to check if can be fixed 298 * @return true if the error can be fixed 299 */ 300 public boolean isFixable(TestError testError) { 301 return false; 302 } 303 304 /** 305 * Returns true if this plugin must check the uploaded data before uploading 306 * @return true if this plugin must check the uploaded data before uploading 307 */ 308 public boolean testBeforeUpload() { 309 return testBeforeUpload; 310 } 311 312 /** 313 * Sets the flag that marks an upload check 314 * @param isUpload if true, the test is before upload 315 */ 316 public void setBeforeUpload(boolean isUpload) { 317 this.isBeforeUpload = isUpload; 318 } 319 320 /** 321 * Returns the test name. 322 * @return The test name 323 */ 324 public String getName() { 325 return name; 326 } 327 328 /** 329 * Determines if the test has been canceled. 330 * @return {@code true} if the test has been canceled, {@code false} otherwise 331 */ 332 public boolean isCanceled() { 333 return progressMonitor != null && progressMonitor.isCanceled(); 334 } 335 336 /** 337 * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix. 338 * If all primitives have already been deleted, null is returned. 339 * @param primitives The primitives wanted for deletion 340 * @return a Delete command on all primitives that have not yet been deleted, or null otherwise 341 */ 342 protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) { 343 Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>(); 344 for (OsmPrimitive p : primitives) { 345 if (!p.isDeleted()) { 346 primitivesToDelete.add(p); 347 } 348 } 349 if (!primitivesToDelete.isEmpty()) { 350 return DeleteCommand.delete(primitivesToDelete); 351 } else { 352 return null; 353 } 354 } 355 356 /** 357 * Determines if the specified primitive denotes a building. 358 * @param p The primitive to be tested 359 * @return True if building key is set and different from no,entrance 360 */ 361 protected static final boolean isBuilding(OsmPrimitive p) { 362 return p.hasTagDifferent("building", "no", "entrance"); 363 } 364 365 /** 366 * Determines if the specified primitive denotes a residential area. 367 * @param p The primitive to be tested 368 * @return True if landuse key is equal to residential 369 */ 370 protected static final boolean isResidentialArea(OsmPrimitive p) { 371 return p.hasTag("landuse", "residential"); 372 } 373 374 /** 375 * Free resources. 376 */ 377 public void clear() { 378 errors.clear(); 379 } 380 381 protected void setShowElements(boolean b) { 382 showElementCount = b; 383 } 384}