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