source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/UnconnectedWays.java@ 14468

Last change on this file since 14468 was 14468, checked in by GerdP, 5 years ago

fix #16978 Use distinct code number for each test group in CrossingWays and UnconnectedWays

  • Property svn:eol-style set to native
File size: 17.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
5import static org.openstreetmap.josm.data.validation.tests.CrossingWays.RAILWAY;
6import static org.openstreetmap.josm.tools.I18n.tr;
7
8import java.awt.geom.Area;
9import java.awt.geom.Line2D;
10import java.awt.geom.Point2D;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.Set;
19
20import org.openstreetmap.josm.data.coor.EastNorth;
21import org.openstreetmap.josm.data.coor.LatLon;
22import org.openstreetmap.josm.data.osm.BBox;
23import org.openstreetmap.josm.data.osm.DataSet;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmDataManager;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.data.osm.QuadBuckets;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
30import org.openstreetmap.josm.data.projection.Ellipsoid;
31import org.openstreetmap.josm.data.validation.Severity;
32import org.openstreetmap.josm.data.validation.Test;
33import org.openstreetmap.josm.data.validation.TestError;
34import org.openstreetmap.josm.gui.progress.ProgressMonitor;
35import org.openstreetmap.josm.spi.preferences.Config;
36import org.openstreetmap.josm.tools.Logging;
37
38/**
39 * Checks if a way has an endpoint very near to another way.
40 * <br>
41 * This class is abstract since highway/railway/waterway/… ways must be handled separately.
42 * An actual implementation must override {@link #isPrimitiveUsable(OsmPrimitive)}
43 * to denote which kind of primitives can be handled.
44 *
45 * @author frsantos
46 */
47public abstract class UnconnectedWays extends Test {
48 private final int code;
49
50 /**
51 * Unconnected highways test.
52 */
53 public static class UnconnectedHighways extends UnconnectedWays {
54 static final int UNCONNECTED_HIGHWAYS = 1311;
55
56 /**
57 * Constructs a new {@code UnconnectedHighways} test.
58 */
59 public UnconnectedHighways() {
60 super(tr("Unconnected highways"), UNCONNECTED_HIGHWAYS);
61 }
62
63 @Override
64 public boolean isPrimitiveUsable(OsmPrimitive p) {
65 return super.isPrimitiveUsable(p) && p.hasKey(HIGHWAY);
66 }
67 }
68
69 /**
70 * Unconnected railways test.
71 */
72 public static class UnconnectedRailways extends UnconnectedWays {
73 static final int UNCONNECTED_RAILWAYS = 1321;
74 /**
75 * Constructs a new {@code UnconnectedRailways} test.
76 */
77 public UnconnectedRailways() {
78 super(tr("Unconnected railways"), UNCONNECTED_RAILWAYS);
79 }
80
81 @Override
82 public boolean isPrimitiveUsable(OsmPrimitive p) {
83 return super.isPrimitiveUsable(p) && p.hasKey("railway");
84 }
85 }
86
87 /**
88 * Unconnected waterways test.
89 */
90 public static class UnconnectedWaterways extends UnconnectedWays {
91 static final int UNCONNECTED_WATERWAYS = 1331;
92 /**
93 * Constructs a new {@code UnconnectedWaterways} test.
94 */
95 public UnconnectedWaterways() {
96 super(tr("Unconnected waterways"), UNCONNECTED_WATERWAYS);
97 }
98
99 @Override
100 public boolean isPrimitiveUsable(OsmPrimitive p) {
101 return super.isPrimitiveUsable(p) && p.hasKey("waterway");
102 }
103 }
104
105 /**
106 * Unconnected natural/landuse test.
107 */
108 public static class UnconnectedNaturalOrLanduse extends UnconnectedWays {
109 static final int UNCONNECTED_NATURAL_OR_LANDUSE = 1341;
110 /**
111 * Constructs a new {@code UnconnectedNaturalOrLanduse} test.
112 */
113 public UnconnectedNaturalOrLanduse() {
114 super(tr("Unconnected natural lands and landuses"), UNCONNECTED_NATURAL_OR_LANDUSE);
115 }
116
117 @Override
118 public boolean isPrimitiveUsable(OsmPrimitive p) {
119 return super.isPrimitiveUsable(p) && p.hasKey("natural", "landuse");
120 }
121 }
122
123 /**
124 * Unconnected power ways test.
125 */
126 public static class UnconnectedPower extends UnconnectedWays {
127 static final int UNCONNECTED_POWER = 1351;
128 /**
129 * Constructs a new {@code UnconnectedPower} test.
130 */
131 public UnconnectedPower() {
132 super(tr("Unconnected power ways"), UNCONNECTED_POWER);
133 }
134
135 @Override
136 public boolean isPrimitiveUsable(OsmPrimitive p) {
137 return super.isPrimitiveUsable(p) && p.hasTag("power", "line", "minor_line", "cable");
138 }
139 }
140
141 protected static final int UNCONNECTED_WAYS = 1301;
142 protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + UnconnectedWays.class.getSimpleName();
143
144 private Set<MyWaySegment> ways;
145 private QuadBuckets<Node> endnodes; // nodes at end of way
146 private QuadBuckets<Node> endnodesHighway; // nodes at end of way
147 private QuadBuckets<Node> middlenodes; // nodes in middle of way
148 private Set<Node> othernodes; // nodes appearing at least twice
149 private Area dsArea;
150
151 private double mindist;
152 private double minmiddledist;
153
154 /**
155 * Constructs a new {@code UnconnectedWays} test.
156 * @param title The test title
157 * @since 6691
158 */
159 public UnconnectedWays(String title) {
160 this(title, UNCONNECTED_WAYS);
161
162 }
163
164 /**
165 * Constructs a new {@code UnconnectedWays} test with the given code.
166 * @param title The test title
167 * @param code The test code
168 * @since 14468
169 */
170 public UnconnectedWays(String title, int code) {
171 super(title, tr("This test checks if a way has an endpoint very near to another way."));
172 this.code = code;
173 }
174
175 @Override
176 public void startTest(ProgressMonitor monitor) {
177 super.startTest(monitor);
178 ways = new HashSet<>();
179 endnodes = new QuadBuckets<>();
180 endnodesHighway = new QuadBuckets<>();
181 middlenodes = new QuadBuckets<>();
182 othernodes = new HashSet<>();
183 mindist = Config.getPref().getDouble(PREFIX + ".node_way_distance", 10.0);
184 minmiddledist = Config.getPref().getDouble(PREFIX + ".way_way_distance", 0.0);
185 DataSet dataSet = OsmDataManager.getInstance().getEditDataSet();
186 dsArea = dataSet == null ? null : dataSet.getDataSourceArea();
187 }
188
189 protected Map<Node, Way> getWayEndNodesNearOtherHighway() {
190 Map<Node, Way> map = new HashMap<>();
191 for (int iter = 0; iter < 1; iter++) {
192 for (MyWaySegment s : ways) {
193 if (isCanceled()) {
194 map.clear();
195 return map;
196 }
197 if (s.highway) {
198 for (Node en : s.nearbyNodes(mindist)) {
199 if (en == null || !endnodesHighway.contains(en)) {
200 continue;
201 }
202 if (en.hasTag(HIGHWAY, "turning_circle", "bus_stop")
203 || en.hasTag("amenity", "parking_entrance")
204 || en.hasTag(RAILWAY, "buffer_stop")
205 || en.isKeyTrue("noexit")
206 || en.hasKey("entrance", "barrier")) {
207 continue;
208 }
209 // to handle intersections of 't' shapes and similar
210 if (en.isConnectedTo(s.w.getNodes(), 3 /* hops */, null)) {
211 continue;
212 }
213 map.put(en, s.w);
214 }
215 }
216 }
217 }
218 return map;
219 }
220
221 protected Map<Node, Way> getWayEndNodesNearOtherWay() {
222 Map<Node, Way> map = new HashMap<>();
223 for (MyWaySegment s : ways) {
224 if (isCanceled()) {
225 map.clear();
226 return map;
227 }
228 for (Node en : s.nearbyNodes(mindist)) {
229 if (en.isConnectedTo(s.w.getNodes(), 3 /* hops */, null)) {
230 continue;
231 }
232 if (!s.highway && endnodesHighway.contains(en) && !s.w.concernsArea()) {
233 map.put(en, s.w);
234 } else if (endnodes.contains(en) && !s.w.concernsArea()) {
235 map.put(en, s.w);
236 }
237 }
238 }
239 return map;
240 }
241
242 protected Map<Node, Way> getWayNodesNearOtherWay() {
243 Map<Node, Way> map = new HashMap<>();
244 for (MyWaySegment s : ways) {
245 if (isCanceled()) {
246 map.clear();
247 return map;
248 }
249 for (Node en : s.nearbyNodes(minmiddledist)) {
250 if (en.isConnectedTo(s.w.getNodes(), 3 /* hops */, null)) {
251 continue;
252 }
253 if (!middlenodes.contains(en)) {
254 continue;
255 }
256 map.put(en, s.w);
257 }
258 }
259 return map;
260 }
261
262 protected Map<Node, Way> getConnectedWayEndNodesNearOtherWay() {
263 Map<Node, Way> map = new HashMap<>();
264 for (MyWaySegment s : ways) {
265 if (isCanceled()) {
266 map.clear();
267 return map;
268 }
269 for (Node en : s.nearbyNodes(minmiddledist)) {
270 if (en.isConnectedTo(s.w.getNodes(), 3 /* hops */, null)) {
271 continue;
272 }
273 if (!othernodes.contains(en)) {
274 continue;
275 }
276 map.put(en, s.w);
277 }
278 }
279 return map;
280 }
281
282 protected final void addErrors(Severity severity, Map<Node, Way> errorMap, String message) {
283 for (Map.Entry<Node, Way> error : errorMap.entrySet()) {
284 errors.add(TestError.builder(this, severity, code)
285 .message(message)
286 .primitives(error.getKey(), error.getValue())
287 .highlight(error.getKey())
288 .build());
289 }
290 }
291
292 @Override
293 public void endTest() {
294 addErrors(Severity.WARNING, getWayEndNodesNearOtherHighway(), tr("Way end node near other highway"));
295 addErrors(Severity.WARNING, getWayEndNodesNearOtherWay(), tr("Way end node near other way"));
296 /* the following two use a shorter distance */
297 if (minmiddledist > 0.0) {
298 addErrors(Severity.OTHER, getWayNodesNearOtherWay(), tr("Way node near other way"));
299 addErrors(Severity.OTHER, getConnectedWayEndNodesNearOtherWay(), tr("Connected way end node near other way"));
300 }
301 ways = null;
302 endnodes = null;
303 endnodesHighway = null;
304 middlenodes = null;
305 othernodes = null;
306 dsArea = null;
307 super.endTest();
308 }
309
310 private class MyWaySegment {
311 private final Line2D line;
312 public final Way w;
313 public final boolean isAbandoned;
314 public final boolean isBoundary;
315 public final boolean highway;
316 private final double len;
317 private Set<Node> nearbyNodeCache;
318 private double nearbyNodeCacheDist = -1.0;
319 private final Node n1;
320 private final Node n2;
321
322 MyWaySegment(Way w, Node n1, Node n2) {
323 this.w = w;
324 String railway = w.get(RAILWAY);
325 String highway = w.get(HIGHWAY);
326 this.isAbandoned = "abandoned".equals(railway) || w.isKeyTrue("disused");
327 this.highway = (highway != null || railway != null) && !isAbandoned;
328 this.isBoundary = !this.highway && w.hasTag("boundary", "administrative");
329 line = new Line2D.Double(n1.getEastNorth().east(), n1.getEastNorth().north(),
330 n2.getEastNorth().east(), n2.getEastNorth().north());
331 len = line.getP1().distance(line.getP2());
332 this.n1 = n1;
333 this.n2 = n2;
334 }
335
336 public boolean nearby(Node n, double dist) {
337 if (w == null) {
338 Logging.debug("way null");
339 return false;
340 }
341 if (w.containsNode(n))
342 return false;
343 if (n.isKeyTrue("noexit"))
344 return false;
345 EastNorth coord = n.getEastNorth();
346 if (coord == null)
347 return false;
348 Point2D p = new Point2D.Double(coord.east(), coord.north());
349 if (line.getP1().distance(p) > len+dist)
350 return false;
351 if (line.getP2().distance(p) > len+dist)
352 return false;
353 return line.ptSegDist(p) < dist;
354 }
355
356 public List<LatLon> getBounds(double fudge) {
357 double x1 = n1.getCoor().lon();
358 double x2 = n2.getCoor().lon();
359 if (x1 > x2) {
360 double tmpx = x1;
361 x1 = x2;
362 x2 = tmpx;
363 }
364 double y1 = n1.getCoor().lat();
365 double y2 = n2.getCoor().lat();
366 if (y1 > y2) {
367 double tmpy = y1;
368 y1 = y2;
369 y2 = tmpy;
370 }
371 LatLon topLeft = new LatLon(y2+fudge, x1-fudge);
372 LatLon botRight = new LatLon(y1-fudge, x2+fudge);
373 List<LatLon> ret = new ArrayList<>(2);
374 ret.add(topLeft);
375 ret.add(botRight);
376 return ret;
377 }
378
379 public Collection<Node> nearbyNodes(double dist) {
380 // If you're looking for nodes that are farther away that we looked for last time,
381 // the cached result is no good
382 if (dist > nearbyNodeCacheDist) {
383 nearbyNodeCache = null;
384 }
385 if (nearbyNodeCache != null) {
386 // If we've cached an area greater than the
387 // one now being asked for...
388 if (nearbyNodeCacheDist > dist) {
389 // Used the cached result and trim out
390 // the nodes that are not in the smaller
391 // area, but keep the old larger cache.
392 Set<Node> trimmed = new HashSet<>(nearbyNodeCache);
393 Set<Node> initial = new HashSet<>(nearbyNodeCache);
394 for (Node n : initial) {
395 if (!nearby(n, dist)) {
396 trimmed.remove(n);
397 }
398 }
399 return trimmed;
400 }
401 return nearbyNodeCache;
402 }
403 /*
404 * We know that any point near the line must be at
405 * least as close as the other end of the line, plus
406 * a little fudge for the distance away ('dist').
407 */
408
409 // This needs to be a hash set because the searches
410 // overlap a bit and can return duplicate nodes.
411 nearbyNodeCache = null;
412 List<LatLon> bounds = this.getBounds(dist * (360.0d / (Ellipsoid.WGS84.a * 2 * Math.PI)));
413 List<Node> foundNodes = endnodesHighway.search(new BBox(bounds.get(0), bounds.get(1)));
414 foundNodes.addAll(endnodes.search(new BBox(bounds.get(0), bounds.get(1))));
415
416 for (Node n : foundNodes) {
417 if (!nearby(n, dist) || !n.getCoor().isIn(dsArea)) {
418 continue;
419 }
420 // It is actually very rare for us to find a node
421 // so defer as much of the work as possible, like
422 // allocating the hash set
423 if (nearbyNodeCache == null) {
424 nearbyNodeCache = new HashSet<>();
425 }
426 nearbyNodeCache.add(n);
427 }
428 nearbyNodeCacheDist = dist;
429 if (nearbyNodeCache == null) {
430 nearbyNodeCache = Collections.emptySet();
431 }
432 return nearbyNodeCache;
433 }
434 }
435
436 List<MyWaySegment> getWaySegments(Way w) {
437 List<MyWaySegment> ret = new ArrayList<>();
438 if (!w.isUsable()
439 || w.hasKey("barrier")
440 || w.hasTag("natural", "cliff"))
441 return ret;
442
443 int size = w.getNodesCount();
444 if (size < 2)
445 return ret;
446 for (int i = 1; i < size; ++i) {
447 if (i < size-1) {
448 addNode(w.getNode(i), middlenodes);
449 }
450 Node a = w.getNode(i-1);
451 Node b = w.getNode(i);
452 if (a.isDrawable() && b.isDrawable()) {
453 MyWaySegment ws = new MyWaySegment(w, a, b);
454 if (ws.isBoundary || ws.isAbandoned) {
455 continue;
456 }
457 ret.add(ws);
458 }
459 }
460 return ret;
461 }
462
463 @Override
464 public void visit(Way w) {
465 // do not consider empty ways
466 if (w.getNodesCount() > 0
467 // ignore addr:interpolation ways as they are not physical features and most of
468 // the time very near the associated highway, which is perfectly normal, see #9332
469 && !w.hasKey("addr:interpolation")
470 // similarly for public transport platforms, tree rows
471 && !w.hasTag(HIGHWAY, "platform") && !w.hasTag(RAILWAY, "platform", "platform_edge") && !w.hasTag("natural", "tree_row")
472 ) {
473 ways.addAll(getWaySegments(w));
474 QuadBuckets<Node> set = endnodes;
475 if (w.hasKey(HIGHWAY, RAILWAY)) {
476 set = endnodesHighway;
477 }
478 addNode(w.firstNode(), set);
479 addNode(w.lastNode(), set);
480 }
481 }
482
483 private void addNode(Node n, QuadBuckets<Node> s) {
484 boolean m = middlenodes.contains(n);
485 boolean e = endnodes.contains(n);
486 boolean eh = endnodesHighway.contains(n);
487 boolean o = othernodes.contains(n);
488 if (!m && !e && !o && !eh) {
489 s.add(n);
490 } else if (!o) {
491 othernodes.add(n);
492 if (e) {
493 endnodes.remove(n);
494 } else if (eh) {
495 endnodesHighway.remove(n);
496 } else {
497 middlenodes.remove(n);
498 }
499 }
500 }
501}
Note: See TracBrowser for help on using the repository browser.