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 /** 080 * Constructor 081 * @param name Name of the test 082 * @param description Description of the test 083 */ 084 public Test(String name, String description) { 085 this.name = name; 086 this.description = description; 087 } 088 089 /** 090 * Constructor 091 * @param name Name of the test 092 */ 093 public Test(String name) { 094 this(name, null); 095 } 096 097 /** 098 * A test that forwards all primitives to {@link #check(OsmPrimitive)}. 099 */ 100 public abstract static class TagTest extends Test { 101 /** 102 * Constructs a new {@code TagTest} with given name and description. 103 * @param name The test name 104 * @param description The test description 105 */ 106 public TagTest(String name, String description) { 107 super(name, description); 108 } 109 110 /** 111 * Constructs a new {@code TagTest} with given name. 112 * @param name The test name 113 */ 114 public TagTest(String name) { 115 super(name); 116 } 117 118 /** 119 * Checks the tags of the given primitive. 120 * @param p The primitive to test 121 */ 122 public abstract void check(OsmPrimitive p); 123 124 @Override 125 public void visit(Node n) { 126 check(n); 127 } 128 129 @Override 130 public void visit(Way w) { 131 check(w); 132 } 133 134 @Override 135 public void visit(Relation r) { 136 check(r); 137 } 138 } 139 140 /** 141 * Initializes any global data used this tester. 142 * @throws Exception When cannot initialize the test 143 */ 144 public void initialize() throws Exception { 145 this.startTime = -1; 146 } 147 148 /** 149 * Start the test using a given progress monitor 150 * 151 * @param progressMonitor the progress monitor 152 */ 153 public void startTest(ProgressMonitor progressMonitor) { 154 this.progressMonitor = Optional.ofNullable(progressMonitor).orElse(NullProgressMonitor.INSTANCE); 155 String startMessage = tr("Running test {0}", name); 156 this.progressMonitor.beginTask(startMessage); 157 Logging.debug(startMessage); 158 this.errors = new ArrayList<>(30); 159 this.startTime = System.currentTimeMillis(); 160 } 161 162 /** 163 * Flag notifying that this test is run over a partial data selection 164 * @param partialSelection Whether the test is on a partial selection data 165 */ 166 public void setPartialSelection(boolean partialSelection) { 167 this.partialSelection = partialSelection; 168 } 169 170 /** 171 * Gets the validation errors accumulated until this moment. 172 * @return The list of errors 173 */ 174 public List<TestError> getErrors() { 175 return errors; 176 } 177 178 /** 179 * Notification of the end of the test. The tester may perform additional 180 * actions and destroy the used structures. 181 * <p> 182 * If you override this method, don't forget to cleanup {@code progressMonitor} 183 * (most overrides call {@code super.endTest()} to do this). 184 */ 185 public void endTest() { 186 progressMonitor.finishTask(); 187 progressMonitor = null; 188 if (startTime > 0) { 189 // fix #11567 where elapsedTime is < 0 190 long elapsedTime = Math.max(0, System.currentTimeMillis() - startTime); 191 Logging.debug(tr("Test ''{0}'' completed in {1}", getName(), Utils.getDurationString(elapsedTime))); 192 } 193 } 194 195 /** 196 * Visits all primitives to be tested. These primitives are always visited 197 * in the same order: nodes first, then ways. 198 * 199 * @param selection The primitives to be tested 200 */ 201 public void visit(Collection<OsmPrimitive> selection) { 202 if (progressMonitor != null) { 203 progressMonitor.setTicksCount(selection.size()); 204 } 205 for (OsmPrimitive p : selection) { 206 if (isCanceled()) { 207 break; 208 } 209 if (isPrimitiveUsable(p)) { 210 p.accept(this); 211 } 212 if (progressMonitor != null) { 213 progressMonitor.worked(1); 214 } 215 } 216 } 217 218 /** 219 * Determines if the primitive is usable for tests. 220 * @param p The primitive 221 * @return {@code true} if the primitive can be tested, {@code false} otherwise 222 */ 223 public boolean isPrimitiveUsable(OsmPrimitive p) { 224 return p.isUsable() && (!(p instanceof Way) || (((Way) p).getNodesCount() > 1)); // test only Ways with at least 2 nodes 225 } 226 227 @Override 228 public void visit(Node n) { 229 // To be overridden in subclasses 230 } 231 232 @Override 233 public void visit(Way w) { 234 // To be overridden in subclasses 235 } 236 237 @Override 238 public void visit(Relation r) { 239 // To be overridden in subclasses 240 } 241 242 /** 243 * Allow the tester to manage its own preferences 244 * @param testPanel The panel to add any preferences component 245 */ 246 public void addGui(JPanel testPanel) { 247 checkEnabled = new JCheckBox(name, enabled); 248 checkEnabled.setToolTipText(description); 249 testPanel.add(checkEnabled, GBC.std()); 250 251 GBC a = GBC.eol(); 252 a.anchor = GridBagConstraints.EAST; 253 checkBeforeUpload = new JCheckBox(); 254 checkBeforeUpload.setSelected(testBeforeUpload); 255 testPanel.add(checkBeforeUpload, a); 256 } 257 258 /** 259 * Called when the used submits the preferences 260 * @return {@code true} if restart is required, {@code false} otherwise 261 */ 262 public boolean ok() { 263 enabled = checkEnabled.isSelected(); 264 testBeforeUpload = checkBeforeUpload.isSelected(); 265 return false; 266 } 267 268 /** 269 * Fixes the error with the appropriate command 270 * 271 * @param testError error to fix 272 * @return The command to fix the error 273 */ 274 public Command fixError(TestError testError) { 275 return null; 276 } 277 278 /** 279 * Returns true if the given error can be fixed automatically 280 * 281 * @param testError The error to check if can be fixed 282 * @return true if the error can be fixed 283 */ 284 public boolean isFixable(TestError testError) { 285 return false; 286 } 287 288 /** 289 * Returns true if this plugin must check the uploaded data before uploading 290 * @return true if this plugin must check the uploaded data before uploading 291 */ 292 public boolean testBeforeUpload() { 293 return testBeforeUpload; 294 } 295 296 /** 297 * Sets the flag that marks an upload check 298 * @param isUpload if true, the test is before upload 299 */ 300 public void setBeforeUpload(boolean isUpload) { 301 this.isBeforeUpload = isUpload; 302 } 303 304 /** 305 * Returns the test name. 306 * @return The test name 307 */ 308 public String getName() { 309 return name; 310 } 311 312 /** 313 * Determines if the test has been canceled. 314 * @return {@code true} if the test has been canceled, {@code false} otherwise 315 */ 316 public boolean isCanceled() { 317 return progressMonitor != null ? progressMonitor.isCanceled() : false; 318 } 319 320 /** 321 * Build a Delete command on all primitives that have not yet been deleted manually by user, or by another error fix. 322 * If all primitives have already been deleted, null is returned. 323 * @param primitives The primitives wanted for deletion 324 * @return a Delete command on all primitives that have not yet been deleted, or null otherwise 325 */ 326 protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) { 327 Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>(); 328 for (OsmPrimitive p : primitives) { 329 if (!p.isDeleted()) { 330 primitivesToDelete.add(p); 331 } 332 } 333 if (!primitivesToDelete.isEmpty()) { 334 return DeleteCommand.delete(primitivesToDelete); 335 } else { 336 return null; 337 } 338 } 339 340 /** 341 * Determines if the specified primitive denotes a building. 342 * @param p The primitive to be tested 343 * @return True if building key is set and different from no,entrance 344 */ 345 protected static final boolean isBuilding(OsmPrimitive p) { 346 return p.hasTagDifferent("building", "no", "entrance"); 347 } 348 349 @Override 350 public int hashCode() { 351 return Objects.hash(name, description); 352 } 353 354 @Override 355 public boolean equals(Object obj) { 356 if (this == obj) return true; 357 if (obj == null || getClass() != obj.getClass()) return false; 358 Test test = (Test) obj; 359 return Objects.equals(name, test.name) && 360 Objects.equals(description, test.description); 361 } 362 363 @Override 364 public int compareTo(Test t) { 365 return name.compareTo(t.name); 366 } 367}