| 1 | // License: GPL. See LICENSE file for details. |
|---|
| 2 | package org.openstreetmap.josm.data.validation.tests; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.marktr; |
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 6 | |
|---|
| 7 | import java.util.ArrayList; |
|---|
| 8 | import java.util.Collection; |
|---|
| 9 | import java.util.Collections; |
|---|
| 10 | import java.util.HashMap; |
|---|
| 11 | import java.util.Iterator; |
|---|
| 12 | import java.util.LinkedHashSet; |
|---|
| 13 | import java.util.LinkedList; |
|---|
| 14 | import java.util.List; |
|---|
| 15 | import java.util.Map; |
|---|
| 16 | import java.util.Set; |
|---|
| 17 | |
|---|
| 18 | import org.openstreetmap.josm.Main; |
|---|
| 19 | import org.openstreetmap.josm.actions.MergeNodesAction; |
|---|
| 20 | import org.openstreetmap.josm.command.Command; |
|---|
| 21 | import org.openstreetmap.josm.command.DeleteCommand; |
|---|
| 22 | import org.openstreetmap.josm.data.coor.LatLon; |
|---|
| 23 | import org.openstreetmap.josm.data.osm.Hash; |
|---|
| 24 | import org.openstreetmap.josm.data.osm.Node; |
|---|
| 25 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 26 | import org.openstreetmap.josm.data.osm.OsmPrimitiveType; |
|---|
| 27 | import org.openstreetmap.josm.data.osm.Storage; |
|---|
| 28 | import org.openstreetmap.josm.data.osm.Way; |
|---|
| 29 | import org.openstreetmap.josm.data.validation.Severity; |
|---|
| 30 | import org.openstreetmap.josm.data.validation.Test; |
|---|
| 31 | import org.openstreetmap.josm.data.validation.TestError; |
|---|
| 32 | import org.openstreetmap.josm.gui.progress.ProgressMonitor; |
|---|
| 33 | import org.openstreetmap.josm.tools.MultiMap; |
|---|
| 34 | |
|---|
| 35 | /** |
|---|
| 36 | * Tests if there are duplicate nodes |
|---|
| 37 | * |
|---|
| 38 | * @author frsantos |
|---|
| 39 | */ |
|---|
| 40 | public class DuplicateNode extends Test { |
|---|
| 41 | |
|---|
| 42 | private static class NodeHash implements Hash<Object, Object> { |
|---|
| 43 | |
|---|
| 44 | double precision = Main.pref.getDouble("validator.duplicatenodes.precision", 0.); |
|---|
| 45 | |
|---|
| 46 | private LatLon RoundCoord(Node o) { |
|---|
| 47 | return new LatLon( |
|---|
| 48 | Math.round(o.getCoor().lat() / precision) * precision, |
|---|
| 49 | Math.round(o.getCoor().lon() / precision) * precision |
|---|
| 50 | ); |
|---|
| 51 | } |
|---|
| 52 | |
|---|
| 53 | @SuppressWarnings("unchecked") |
|---|
| 54 | private LatLon getLatLon(Object o) { |
|---|
| 55 | if (o instanceof Node) { |
|---|
| 56 | if (precision==0) |
|---|
| 57 | return ((Node) o).getCoor().getRoundedToOsmPrecision(); |
|---|
| 58 | return RoundCoord((Node) o); |
|---|
| 59 | } else if (o instanceof List<?>) { |
|---|
| 60 | if (precision==0) |
|---|
| 61 | return ((List<Node>) o).get(0).getCoor().getRoundedToOsmPrecision(); |
|---|
| 62 | return RoundCoord(((List<Node>) o).get(0)); |
|---|
| 63 | } else |
|---|
| 64 | throw new AssertionError(); |
|---|
| 65 | } |
|---|
| 66 | |
|---|
| 67 | @Override |
|---|
| 68 | public boolean equals(Object k, Object t) { |
|---|
| 69 | return getLatLon(k).equals(getLatLon(t)); |
|---|
| 70 | } |
|---|
| 71 | |
|---|
| 72 | @Override |
|---|
| 73 | public int getHashCode(Object k) { |
|---|
| 74 | return getLatLon(k).hashCode(); |
|---|
| 75 | } |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | protected final static int DUPLICATE_NODE = 1; |
|---|
| 79 | protected final static int DUPLICATE_NODE_MIXED = 2; |
|---|
| 80 | protected final static int DUPLICATE_NODE_OTHER = 3; |
|---|
| 81 | protected final static int DUPLICATE_NODE_UNCLOSED = 4; |
|---|
| 82 | protected final static int DUPLICATE_NODE_BUILDING = 10; |
|---|
| 83 | protected final static int DUPLICATE_NODE_BOUNDARY = 11; |
|---|
| 84 | protected final static int DUPLICATE_NODE_HIGHWAY = 12; |
|---|
| 85 | protected final static int DUPLICATE_NODE_LANDUSE = 13; |
|---|
| 86 | protected final static int DUPLICATE_NODE_NATURAL = 14; |
|---|
| 87 | protected final static int DUPLICATE_NODE_POWER = 15; |
|---|
| 88 | protected final static int DUPLICATE_NODE_RAILWAY = 16; |
|---|
| 89 | protected final static int DUPLICATE_NODE_WATERWAY = 17; |
|---|
| 90 | |
|---|
| 91 | /** The map of potential duplicates. |
|---|
| 92 | * |
|---|
| 93 | * If there is exactly one node for a given pos, the map includes a pair <pos, Node>. |
|---|
| 94 | * If there are multiple nodes for a given pos, the map includes a pair |
|---|
| 95 | * <pos, NodesByEqualTagsMap> |
|---|
| 96 | */ |
|---|
| 97 | Storage<Object> potentialDuplicates; |
|---|
| 98 | |
|---|
| 99 | /** |
|---|
| 100 | * Constructor |
|---|
| 101 | */ |
|---|
| 102 | public DuplicateNode() { |
|---|
| 103 | super(tr("Duplicated nodes"), |
|---|
| 104 | tr("This test checks that there are no nodes at the very same location.")); |
|---|
| 105 | } |
|---|
| 106 | |
|---|
| 107 | @Override |
|---|
| 108 | public void startTest(ProgressMonitor monitor) { |
|---|
| 109 | super.startTest(monitor); |
|---|
| 110 | potentialDuplicates = new Storage<Object>(new NodeHash()); |
|---|
| 111 | } |
|---|
| 112 | |
|---|
| 113 | |
|---|
| 114 | @SuppressWarnings("unchecked") |
|---|
| 115 | @Override |
|---|
| 116 | public void endTest() { |
|---|
| 117 | for (Object v: potentialDuplicates) { |
|---|
| 118 | if (v instanceof Node) { |
|---|
| 119 | // just one node at this position. Nothing to report as |
|---|
| 120 | // error |
|---|
| 121 | continue; |
|---|
| 122 | } |
|---|
| 123 | |
|---|
| 124 | // multiple nodes at the same position -> report errors |
|---|
| 125 | // |
|---|
| 126 | List<Node> nodes = (List<Node>) v; |
|---|
| 127 | errors.addAll(buildTestErrors(this, nodes)); |
|---|
| 128 | } |
|---|
| 129 | super.endTest(); |
|---|
| 130 | potentialDuplicates = null; |
|---|
| 131 | } |
|---|
| 132 | |
|---|
| 133 | public List<TestError> buildTestErrors(Test parentTest, List<Node> nodes) { |
|---|
| 134 | List<TestError> errors = new ArrayList<TestError>(); |
|---|
| 135 | |
|---|
| 136 | MultiMap<Map<String,String>, OsmPrimitive> mm = new MultiMap<Map<String,String>, OsmPrimitive>(); |
|---|
| 137 | for (Node n: nodes) { |
|---|
| 138 | mm.put(n.getKeys(), n); |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | Map<String,Boolean> typeMap=new HashMap<String,Boolean>(); |
|---|
| 142 | String[] types = {"none", "highway", "railway", "waterway", "boundary", "power", "natural", "landuse", "building"}; |
|---|
| 143 | |
|---|
| 144 | |
|---|
| 145 | // check whether we have multiple nodes at the same position with |
|---|
| 146 | // the same tag set |
|---|
| 147 | // |
|---|
| 148 | for (Iterator<Map<String,String>> it = mm.keySet().iterator(); it.hasNext();) { |
|---|
| 149 | Map<String,String> tagSet = it.next(); |
|---|
| 150 | if (mm.get(tagSet).size() > 1) { |
|---|
| 151 | |
|---|
| 152 | boolean oneWayClosed = false; |
|---|
| 153 | |
|---|
| 154 | for (String type: types) { |
|---|
| 155 | typeMap.put(type, false); |
|---|
| 156 | } |
|---|
| 157 | |
|---|
| 158 | for (OsmPrimitive p : mm.get(tagSet)) { |
|---|
| 159 | if (p.getType()==OsmPrimitiveType.NODE) { |
|---|
| 160 | Node n = (Node) p; |
|---|
| 161 | List<OsmPrimitive> lp=n.getReferrers(); |
|---|
| 162 | for (OsmPrimitive sp: lp) { |
|---|
| 163 | if (sp.getType()==OsmPrimitiveType.WAY) { |
|---|
| 164 | boolean typed = false; |
|---|
| 165 | Way w=(Way) sp; |
|---|
| 166 | oneWayClosed = oneWayClosed || w.isClosed(); |
|---|
| 167 | Map<String, String> keys = w.getKeys(); |
|---|
| 168 | for (String type: typeMap.keySet()) { |
|---|
| 169 | if (keys.containsKey(type)) { |
|---|
| 170 | typeMap.put(type, true); |
|---|
| 171 | typed=true; |
|---|
| 172 | } |
|---|
| 173 | } |
|---|
| 174 | if (!typed) { |
|---|
| 175 | typeMap.put("none", true); |
|---|
| 176 | } |
|---|
| 177 | } |
|---|
| 178 | } |
|---|
| 179 | |
|---|
| 180 | } |
|---|
| 181 | } |
|---|
| 182 | |
|---|
| 183 | int nbType=0; |
|---|
| 184 | for (String type: typeMap.keySet()) { |
|---|
| 185 | if (typeMap.get(type)) { |
|---|
| 186 | nbType++; |
|---|
| 187 | } |
|---|
| 188 | } |
|---|
| 189 | |
|---|
| 190 | if (!oneWayClosed) { |
|---|
| 191 | String msg = marktr("Duplicate nodes in two un-closed ways"); |
|---|
| 192 | errors.add(new TestError( |
|---|
| 193 | parentTest, |
|---|
| 194 | Severity.WARNING, |
|---|
| 195 | tr("Duplicated nodes"), |
|---|
| 196 | tr(msg), |
|---|
| 197 | msg, |
|---|
| 198 | DUPLICATE_NODE_UNCLOSED, |
|---|
| 199 | mm.get(tagSet) |
|---|
| 200 | )); |
|---|
| 201 | } else if (nbType>1) { |
|---|
| 202 | String msg = marktr("Mixed type duplicated nodes"); |
|---|
| 203 | errors.add(new TestError( |
|---|
| 204 | parentTest, |
|---|
| 205 | Severity.WARNING, |
|---|
| 206 | tr("Duplicated nodes"), |
|---|
| 207 | tr(msg), |
|---|
| 208 | msg, |
|---|
| 209 | DUPLICATE_NODE_MIXED, |
|---|
| 210 | mm.get(tagSet) |
|---|
| 211 | )); |
|---|
| 212 | } else if (typeMap.get("highway")) { |
|---|
| 213 | String msg = marktr("Highway duplicated nodes"); |
|---|
| 214 | errors.add(new TestError( |
|---|
| 215 | parentTest, |
|---|
| 216 | Severity.ERROR, |
|---|
| 217 | tr("Duplicated nodes"), |
|---|
| 218 | tr(msg), |
|---|
| 219 | msg, |
|---|
| 220 | DUPLICATE_NODE_HIGHWAY, |
|---|
| 221 | mm.get(tagSet) |
|---|
| 222 | )); |
|---|
| 223 | } else if (typeMap.get("railway")) { |
|---|
| 224 | String msg = marktr("Railway duplicated nodes"); |
|---|
| 225 | errors.add(new TestError( |
|---|
| 226 | parentTest, |
|---|
| 227 | Severity.ERROR, |
|---|
| 228 | tr("Duplicated nodes"), |
|---|
| 229 | tr(msg), |
|---|
| 230 | msg, |
|---|
| 231 | DUPLICATE_NODE_RAILWAY, |
|---|
| 232 | mm.get(tagSet) |
|---|
| 233 | )); |
|---|
| 234 | } else if (typeMap.get("waterway")) { |
|---|
| 235 | String msg = marktr("Waterway duplicated nodes"); |
|---|
| 236 | errors.add(new TestError( |
|---|
| 237 | parentTest, |
|---|
| 238 | Severity.ERROR, |
|---|
| 239 | tr("Duplicated nodes"), |
|---|
| 240 | tr(msg), |
|---|
| 241 | msg, |
|---|
| 242 | DUPLICATE_NODE_WATERWAY, |
|---|
| 243 | mm.get(tagSet) |
|---|
| 244 | )); |
|---|
| 245 | } else if (typeMap.get("boundary")) { |
|---|
| 246 | String msg = marktr("Boundary duplicated nodes"); |
|---|
| 247 | errors.add(new TestError( |
|---|
| 248 | parentTest, |
|---|
| 249 | Severity.ERROR, |
|---|
| 250 | tr("Duplicated nodes"), |
|---|
| 251 | tr(msg), |
|---|
| 252 | msg, |
|---|
| 253 | DUPLICATE_NODE_BOUNDARY, |
|---|
| 254 | mm.get(tagSet) |
|---|
| 255 | )); |
|---|
| 256 | } else if (typeMap.get("power")) { |
|---|
| 257 | String msg = marktr("Power duplicated nodes"); |
|---|
| 258 | errors.add(new TestError( |
|---|
| 259 | parentTest, |
|---|
| 260 | Severity.ERROR, |
|---|
| 261 | tr("Duplicated nodes"), |
|---|
| 262 | tr(msg), |
|---|
| 263 | msg, |
|---|
| 264 | DUPLICATE_NODE_POWER, |
|---|
| 265 | mm.get(tagSet) |
|---|
| 266 | )); |
|---|
| 267 | } else if (typeMap.get("natural")) { |
|---|
| 268 | String msg = marktr("Natural duplicated nodes"); |
|---|
| 269 | errors.add(new TestError( |
|---|
| 270 | parentTest, |
|---|
| 271 | Severity.ERROR, |
|---|
| 272 | tr("Duplicated nodes"), |
|---|
| 273 | tr(msg), |
|---|
| 274 | msg, |
|---|
| 275 | DUPLICATE_NODE_NATURAL, |
|---|
| 276 | mm.get(tagSet) |
|---|
| 277 | )); |
|---|
| 278 | } else if (typeMap.get("building")) { |
|---|
| 279 | String msg = marktr("Building duplicated nodes"); |
|---|
| 280 | errors.add(new TestError( |
|---|
| 281 | parentTest, |
|---|
| 282 | Severity.ERROR, |
|---|
| 283 | tr("Duplicated nodes"), |
|---|
| 284 | tr(msg), |
|---|
| 285 | msg, |
|---|
| 286 | DUPLICATE_NODE_BUILDING, |
|---|
| 287 | mm.get(tagSet) |
|---|
| 288 | )); |
|---|
| 289 | } else if (typeMap.get("landuse")) { |
|---|
| 290 | String msg = marktr("Landuse duplicated nodes"); |
|---|
| 291 | errors.add(new TestError( |
|---|
| 292 | parentTest, |
|---|
| 293 | Severity.ERROR, |
|---|
| 294 | tr("Duplicated nodes"), |
|---|
| 295 | tr(msg), |
|---|
| 296 | msg, |
|---|
| 297 | DUPLICATE_NODE_LANDUSE, |
|---|
| 298 | mm.get(tagSet) |
|---|
| 299 | )); |
|---|
| 300 | } else { |
|---|
| 301 | String msg = marktr("Other duplicated nodes"); |
|---|
| 302 | errors.add(new TestError( |
|---|
| 303 | parentTest, |
|---|
| 304 | Severity.WARNING, |
|---|
| 305 | tr("Duplicated nodes"), |
|---|
| 306 | tr(msg), |
|---|
| 307 | msg, |
|---|
| 308 | DUPLICATE_NODE_OTHER, |
|---|
| 309 | mm.get(tagSet) |
|---|
| 310 | )); |
|---|
| 311 | |
|---|
| 312 | } |
|---|
| 313 | it.remove(); |
|---|
| 314 | } |
|---|
| 315 | } |
|---|
| 316 | |
|---|
| 317 | // check whether we have multiple nodes at the same position with |
|---|
| 318 | // differing tag sets |
|---|
| 319 | // |
|---|
| 320 | if (!mm.isEmpty()) { |
|---|
| 321 | List<OsmPrimitive> duplicates = new ArrayList<OsmPrimitive>(); |
|---|
| 322 | for (Set<OsmPrimitive> l: mm.values()) { |
|---|
| 323 | duplicates.addAll(l); |
|---|
| 324 | } |
|---|
| 325 | if (duplicates.size() > 1) { |
|---|
| 326 | errors.add(new TestError( |
|---|
| 327 | parentTest, |
|---|
| 328 | Severity.WARNING, |
|---|
| 329 | tr("Nodes at same position"), |
|---|
| 330 | DUPLICATE_NODE, |
|---|
| 331 | duplicates |
|---|
| 332 | )); |
|---|
| 333 | } |
|---|
| 334 | } |
|---|
| 335 | return errors; |
|---|
| 336 | } |
|---|
| 337 | |
|---|
| 338 | @SuppressWarnings("unchecked") |
|---|
| 339 | @Override |
|---|
| 340 | public void visit(Node n) { |
|---|
| 341 | if (n.isUsable()) { |
|---|
| 342 | if (potentialDuplicates.get(n) == null) { |
|---|
| 343 | // in most cases there is just one node at a given position. We |
|---|
| 344 | // avoid to create an extra object and add remember the node |
|---|
| 345 | // itself at this position |
|---|
| 346 | potentialDuplicates.put(n); |
|---|
| 347 | } else if (potentialDuplicates.get(n) instanceof Node) { |
|---|
| 348 | // we have an additional node at the same position. Create an extra |
|---|
| 349 | // object to keep track of the nodes at this position. |
|---|
| 350 | // |
|---|
| 351 | Node n1 = (Node)potentialDuplicates.get(n); |
|---|
| 352 | List<Node> nodes = new ArrayList<Node>(2); |
|---|
| 353 | nodes.add(n1); |
|---|
| 354 | nodes.add(n); |
|---|
| 355 | potentialDuplicates.put(nodes); |
|---|
| 356 | } else if (potentialDuplicates.get(n) instanceof List<?>) { |
|---|
| 357 | // we have multiple nodes at the same position. |
|---|
| 358 | // |
|---|
| 359 | List<Node> nodes = (List<Node>)potentialDuplicates.get(n); |
|---|
| 360 | nodes.add(n); |
|---|
| 361 | } |
|---|
| 362 | } |
|---|
| 363 | } |
|---|
| 364 | |
|---|
| 365 | /** |
|---|
| 366 | * Merge the nodes into one. |
|---|
| 367 | * Copied from UtilsPlugin.MergePointsAction |
|---|
| 368 | */ |
|---|
| 369 | @Override |
|---|
| 370 | public Command fixError(TestError testError) { |
|---|
| 371 | if (!isFixable(testError)) return null; |
|---|
| 372 | Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(testError.getPrimitives()); |
|---|
| 373 | LinkedHashSet<Node> nodes = new LinkedHashSet<Node>(OsmPrimitive.getFilteredList(sel, Node.class)); |
|---|
| 374 | |
|---|
| 375 | // Use first existing node or first node if all nodes are new |
|---|
| 376 | Node target = null; |
|---|
| 377 | for (Node n: nodes) { |
|---|
| 378 | if (!n.isNew()) { |
|---|
| 379 | target = n; |
|---|
| 380 | break; |
|---|
| 381 | } |
|---|
| 382 | } |
|---|
| 383 | if (target == null) { |
|---|
| 384 | target = nodes.iterator().next(); |
|---|
| 385 | } |
|---|
| 386 | |
|---|
| 387 | if (DeleteCommand.checkAndConfirmOutlyingDelete(Main.main.getCurrentDataSet().getDataSourceArea(), nodes, Collections.singleton(target))) |
|---|
| 388 | return MergeNodesAction.mergeNodes(Main.main.getEditLayer(), nodes, target); |
|---|
| 389 | |
|---|
| 390 | return null;// undoRedo handling done in mergeNodes |
|---|
| 391 | } |
|---|
| 392 | |
|---|
| 393 | @Override |
|---|
| 394 | public boolean isFixable(TestError testError) { |
|---|
| 395 | if (!(testError.getTester() instanceof DuplicateNode)) return false; |
|---|
| 396 | // never merge nodes with different tags. |
|---|
| 397 | if (testError.getCode() == DUPLICATE_NODE) return false; |
|---|
| 398 | // never merge nodes from two different non-closed geometries |
|---|
| 399 | if (testError.getCode() == DUPLICATE_NODE_UNCLOSED) return false; |
|---|
| 400 | // everything else is ok to merge |
|---|
| 401 | return true; |
|---|
| 402 | } |
|---|
| 403 | } |
|---|