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