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