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

Last change on this file since 17243 was 17173, checked in by Klumbumbus, 4 years ago

fix #19909 - Don't warn about "Way end node near other way" if a power line ends with location:transition=yes

  • Property svn:eol-style set to native
File size: 22.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.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Objects;
21import java.util.Set;
22import java.util.stream.Collectors;
23
24import org.openstreetmap.josm.data.coor.EastNorth;
25import org.openstreetmap.josm.data.coor.LatLon;
26import org.openstreetmap.josm.data.osm.BBox;
27import org.openstreetmap.josm.data.osm.DataSet;
28import org.openstreetmap.josm.data.osm.Node;
29import org.openstreetmap.josm.data.osm.OsmDataManager;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.OsmUtils;
32import org.openstreetmap.josm.data.osm.QuadBuckets;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
35import org.openstreetmap.josm.data.projection.Ellipsoid;
36import org.openstreetmap.josm.data.projection.ProjectionRegistry;
37import org.openstreetmap.josm.data.validation.Severity;
38import org.openstreetmap.josm.data.validation.Test;
39import org.openstreetmap.josm.data.validation.TestError;
40import org.openstreetmap.josm.gui.progress.ProgressMonitor;
41import org.openstreetmap.josm.spi.preferences.Config;
42import org.openstreetmap.josm.tools.Geometry;
43import org.openstreetmap.josm.tools.Logging;
44
45/**
46 * Checks if a way has an endpoint very near to another way.
47 * <br>
48 * This class is abstract since highway/railway/waterway/… ways must be handled separately.
49 * An actual implementation must override {@link #isPrimitiveUsable(OsmPrimitive)}
50 * to denote which kind of primitives can be handled.
51 *
52 * @author frsantos
53 */
54public abstract class UnconnectedWays extends Test {
55 private final int code;
56 private final boolean isHighwayTest;
57
58 static final double DETOUR_FACTOR = 4;
59
60 protected abstract boolean isCandidate(OsmPrimitive p);
61
62 protected boolean isWantedWay(Way w) {
63 return w.isUsable() && isCandidate(w);
64 }
65
66 /**
67 * Check if unconnected end node should be ignored.
68 * @param n the node
69 * @return true if node should be ignored
70 */
71 protected boolean ignoreUnconnectedEndNode(Node n) {
72 return false;
73 }
74
75 @Override
76 public boolean isPrimitiveUsable(OsmPrimitive p) {
77 return super.isPrimitiveUsable(p) && ((partialSelection && p instanceof Node) || isCandidate(p));
78 }
79
80 /**
81 * Unconnected highways test.
82 */
83 public static class UnconnectedHighways extends UnconnectedWays {
84 static final int UNCONNECTED_HIGHWAYS = 1311;
85
86 /**
87 * Constructs a new {@code UnconnectedHighways} test.
88 */
89 public UnconnectedHighways() {
90 super(tr("Unconnected highways"), UNCONNECTED_HIGHWAYS, true);
91 }
92
93 @Override
94 protected boolean isCandidate(OsmPrimitive p) {
95 return p.hasKey(HIGHWAY);
96 }
97
98 @Override
99 protected boolean ignoreUnconnectedEndNode(Node n) {
100 return n.hasTag(HIGHWAY, "turning_circle", "bus_stop", "elevator")
101 || n.hasTag("amenity", "parking_entrance")
102 || n.isKeyTrue("noexit")
103 || n.hasKey("entrance", "barrier")
104 || n.getParentWays().stream().anyMatch(p -> isBuilding(p) || p.hasTag(RAILWAY, "platform", "platform_edge"));
105 }
106 }
107
108 /**
109 * Unconnected railways test.
110 */
111 public static class UnconnectedRailways extends UnconnectedWays {
112 static final int UNCONNECTED_RAILWAYS = 1321;
113 /**
114 * Constructs a new {@code UnconnectedRailways} test.
115 */
116 public UnconnectedRailways() {
117 super(tr("Unconnected railways"), UNCONNECTED_RAILWAYS, false);
118 }
119
120 @Override
121 protected boolean isCandidate(OsmPrimitive p) {
122 return p.hasTagDifferent(RAILWAY, "abandoned", "platform", "razed");
123 }
124
125 @Override
126 protected boolean ignoreUnconnectedEndNode(Node n) {
127 return n.hasTag(RAILWAY, "buffer_stop")
128 || n.isKeyTrue("noexit");
129 }
130 }
131
132 /**
133 * Unconnected waterways test.
134 */
135 public static class UnconnectedWaterways extends UnconnectedWays {
136 static final int UNCONNECTED_WATERWAYS = 1331;
137 /**
138 * Constructs a new {@code UnconnectedWaterways} test.
139 */
140 public UnconnectedWaterways() {
141 super(tr("Unconnected waterways"), UNCONNECTED_WATERWAYS, false);
142 }
143
144 @Override
145 protected boolean isCandidate(OsmPrimitive p) {
146 return p.hasKey("waterway");
147 }
148 }
149
150 /**
151 * Unconnected natural/landuse test.
152 */
153 public static class UnconnectedNaturalOrLanduse extends UnconnectedWays {
154 static final int UNCONNECTED_NATURAL_OR_LANDUSE = 1341;
155 /**
156 * Constructs a new {@code UnconnectedNaturalOrLanduse} test.
157 */
158 public UnconnectedNaturalOrLanduse() {
159 super(tr("Unconnected natural lands and landuses"), UNCONNECTED_NATURAL_OR_LANDUSE, false);
160 }
161
162 @Override
163 protected boolean isCandidate(OsmPrimitive p) {
164 return p.hasKey("landuse") || p.hasTagDifferent("natural", "tree_row", "cliff");
165 }
166 }
167
168 /**
169 * Unconnected power ways test.
170 */
171 public static class UnconnectedPower extends UnconnectedWays {
172 static final int UNCONNECTED_POWER = 1351;
173 /**
174 * Constructs a new {@code UnconnectedPower} test.
175 */
176 public UnconnectedPower() {
177 super(tr("Unconnected power ways"), UNCONNECTED_POWER, false);
178 }
179
180 @Override
181 protected boolean isCandidate(OsmPrimitive p) {
182 return p.hasTag("power", "line", "minor_line", "cable");
183 }
184
185 @Override
186 protected boolean ignoreUnconnectedEndNode(Node n) {
187 return n.hasTag("power", "terminal") || n.hasTag("location:transition", "yes");
188 }
189 }
190
191 protected static final int UNCONNECTED_WAYS = 1301;
192 protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + UnconnectedWays.class.getSimpleName();
193
194 private List<MyWaySegment> waySegments;
195 private Set<Node> endnodes; // nodes at end of way
196 private Set<Node> middlenodes; // nodes in middle of way
197 private Set<Node> othernodes; // nodes appearing at least twice
198 private QuadBuckets<Node> searchNodes;
199 private Set<Way> waysToTest;
200 private Set<Node> nodesToTest;
201 private Area dsArea;
202
203 private double mindist;
204 private double minmiddledist;
205 private double maxLen; // maximum length of allowed detour to reach the unconnected node
206 private DataSet ds;
207
208 /**
209 * Constructs a new {@code UnconnectedWays} test.
210 * @param title The test title
211 * @since 6691
212 */
213 protected UnconnectedWays(String title) {
214 this(title, UNCONNECTED_WAYS, false);
215 }
216
217 /**
218 * Constructs a new {@code UnconnectedWays} test with the given code.
219 * @param title The test title
220 * @param code The test code
221 * @param isHighwayTest use {@code true} if test concerns highways or railways
222 * @since 14468
223 */
224 protected UnconnectedWays(String title, int code, boolean isHighwayTest) {
225 super(title, tr("This test checks if a way has an endpoint very near to another way."));
226 this.code = code;
227 this.isHighwayTest = isHighwayTest;
228 }
229
230 @Override
231 public void startTest(ProgressMonitor monitor) {
232 super.startTest(monitor);
233 waySegments = new ArrayList<>();
234 searchNodes = new QuadBuckets<>();
235 waysToTest = new HashSet<>();
236 nodesToTest = new HashSet<>();
237 endnodes = new HashSet<>();
238 middlenodes = new HashSet<>();
239 othernodes = new HashSet<>();
240 mindist = Config.getPref().getDouble(PREFIX + ".node_way_distance", 10.0);
241 minmiddledist = Config.getPref().getDouble(PREFIX + ".way_way_distance", 0.0);
242 ds = OsmDataManager.getInstance().getActiveDataSet();
243 dsArea = ds == null ? null : ds.getDataSourceArea();
244 }
245
246 protected Map<Node, MyWaySegment> getHighwayEndNodesNearOtherHighway() {
247 Map<Node, MyWaySegment> map = new HashMap<>();
248 for (MyWaySegment s : waySegments) {
249 if (isCanceled()) {
250 map.clear();
251 return map;
252 }
253 if (s.w.hasTag(HIGHWAY, "platform"))
254 continue;
255 for (Node endnode : s.nearbyNodes(mindist)) {
256 Way parentWay = getWantedParentWay(endnode);
257 if (parentWay != null && !parentWay.hasTag(HIGHWAY, "platform")
258 && Objects.equals(OsmUtils.getLayer(s.w), OsmUtils.getLayer(parentWay))
259 // to handle intersections of 't' shapes and similar
260 && !s.isConnectedTo(endnode) && !s.obstacleBetween(endnode)) {
261 addIfNewOrCloser(map, endnode, s);
262 }
263 }
264 }
265 return map;
266 }
267
268 protected Map<Node, MyWaySegment> getWayEndNodesNearOtherWay() {
269 Map<Node, MyWaySegment> map = new HashMap<>();
270
271 for (MyWaySegment s : waySegments) {
272 if (isCanceled()) {
273 map.clear();
274 return map;
275 }
276 if (!s.concernsArea) {
277 for (Node endnode : s.nearbyNodes(mindist)) {
278 if (!s.isConnectedTo(endnode)) {
279 if (s.w.hasTag("power")) {
280 boolean badConnection = false;
281 Way otherWay = getWantedParentWay(endnode);
282 if (otherWay != null) {
283 for (String key : Arrays.asList("voltage", "frequency")) {
284 String v1 = s.w.get(key);
285 String v2 = otherWay.get(key);
286 if (v1 != null && v2 != null && !v1.equals(v2)) {
287 badConnection = true;
288 }
289 }
290 }
291 if (badConnection)
292 continue;
293 }
294 addIfNewOrCloser(map, endnode, s);
295 }
296 }
297 }
298 }
299 return map;
300 }
301
302 protected Map<Node, MyWaySegment> getWayNodesNearOtherWay() {
303 Map<Node, MyWaySegment> map = new HashMap<>();
304 for (MyWaySegment s : waySegments) {
305 if (isCanceled()) {
306 map.clear();
307 return map;
308 }
309 for (Node en : s.nearbyNodes(minmiddledist)) {
310 if (!s.isConnectedTo(en)) {
311 addIfNewOrCloser(map, en, s);
312 }
313 }
314 }
315 return map;
316 }
317
318 /**
319 * An unconnected node might have multiple parent ways, e.g. a highway and a landuse way.
320 * Make sure we get the one that was analysed before.
321 * @param endnode the node which is known to be an end node of the wanted way
322 * @return the wanted way
323 */
324 private Way getWantedParentWay(Node endnode) {
325 for (Way w : endnode.getParentWays()) {
326 if (isWantedWay(w))
327 return w;
328 }
329 Logging.error("end node without matching parent way");
330 return null;
331 }
332
333 private void addIfNewOrCloser(Map<Node, MyWaySegment> map, Node node, MyWaySegment ws) {
334 if (partialSelection && !nodesToTest.contains(node) && !waysToTest.contains(ws.w))
335 return;
336 MyWaySegment old = map.get(node);
337 if (old != null) {
338 double d1 = ws.getDist(node);
339 double d2 = old.getDist(node);
340 if (d1 > d2) {
341 // keep old value
342 return;
343 }
344 }
345 map.put(node, ws);
346 }
347
348 protected final void addErrors(Severity severity, Map<Node, MyWaySegment> errorMap, String message) {
349 for (Entry<Node, MyWaySegment> error : errorMap.entrySet()) {
350 Node node = error.getKey();
351 MyWaySegment ws = error.getValue();
352 errors.add(TestError.builder(this, severity, code)
353 .message(message)
354 .primitives(node, ws.w)
355 .highlight(node)
356 .build());
357 }
358 }
359
360 @Override
361 public void endTest() {
362 if (ds == null)
363 return;
364
365 for (Way w : ds.getWays()) {
366 if (isWantedWay(w) && w.getRealNodesCount() > 1) {
367 waySegments.addAll(getWaySegments(w));
368 addNode(w.firstNode(), endnodes);
369 addNode(w.lastNode(), endnodes);
370 }
371 }
372 fillSearchNodes(endnodes);
373 if (!searchNodes.isEmpty()) {
374 maxLen = DETOUR_FACTOR * mindist;
375 if (isHighwayTest) {
376 addErrors(Severity.WARNING, getHighwayEndNodesNearOtherHighway(), tr("Way end node near other highway"));
377 } else {
378 addErrors(Severity.WARNING, getWayEndNodesNearOtherWay(), tr("Way end node near other way"));
379 }
380 }
381
382 /* the following two should use a shorter distance */
383 boolean includeOther = isBeforeUpload ? ValidatorPrefHelper.PREF_OTHER_UPLOAD.get() : ValidatorPrefHelper.PREF_OTHER.get();
384 if (minmiddledist > 0.0 && includeOther) {
385 maxLen = DETOUR_FACTOR * minmiddledist;
386 fillSearchNodes(middlenodes);
387 addErrors(Severity.OTHER, getWayNodesNearOtherWay(), tr("Way node near other way"));
388 fillSearchNodes(othernodes);
389 addErrors(Severity.OTHER, getWayNodesNearOtherWay(), tr("Connected way end node near other way"));
390 }
391
392 waySegments = null;
393 endnodes = null;
394 middlenodes = null;
395 othernodes = null;
396 searchNodes = null;
397 dsArea = null;
398 ds = null;
399 super.endTest();
400 }
401
402 private void fillSearchNodes(Collection<Node> nodes) {
403 searchNodes.clear();
404 for (Node n : nodes) {
405 if (!ignoreUnconnectedEndNode(n) && n.getCoor().isIn(dsArea)) {
406 searchNodes.add(n);
407 }
408 }
409 }
410
411 private class MyWaySegment {
412 /** the way */
413 public final Way w;
414 private final Node n1;
415 private final Node n2;
416 private final boolean concernsArea;
417
418 MyWaySegment(Way w, Node n1, Node n2, boolean concersArea) {
419 this.w = w;
420 this.n1 = n1;
421 this.n2 = n2;
422 this.concernsArea = concersArea;
423 }
424
425 /**
426 * Check if the given node is connected to this segment using a reasonable short way.
427 * @param startNode the node
428 * @return true if a reasonable connection was found
429 */
430 boolean isConnectedTo(Node startNode) {
431 return isConnectedTo(startNode, new LinkedHashSet<>(), 0, w);
432 }
433
434 /**
435 * Check if the given node is connected to this segment using a reasonable short way.
436 * @param node the given node
437 * @param visited set of visited nodes
438 * @param len length of the travelled route
439 * @param parent the previous parent way
440 * @return true if a reasonable connection was found
441 */
442 private boolean isConnectedTo(Node node, LinkedHashSet<Node> visited, double len, Way parent) {
443 if (len > maxLen) {
444 return false;
445 }
446 if (n1 == node || n2 == node) {
447 Node uncon = visited.iterator().next();
448 LatLon cl = ProjectionRegistry.getProjection().eastNorth2latlon(calcClosest(uncon));
449 // calculate real detour length, closest point might be somewhere between n1 and n2
450 double detourLen = len + node.getCoor().greatCircleDistance(cl);
451 if (detourLen > maxLen)
452 return false;
453 // see #17914: flag also nodes which are very close
454 double directDist = getDist(uncon);
455 if (directDist <= 0.1)
456 return false;
457 return directDist > 0.5 || (visited.size() == 2 && directDist * 1.5 > detourLen);
458 }
459 if (visited != null) {
460 visited.add(node);
461 List<Way> wantedParents = node.getParentWays().stream().filter(pw -> isWantedWay(pw))
462 .collect(Collectors.toList());
463 if (wantedParents.size() > 1 && wantedParents.indexOf(parent) != wantedParents.size() - 1) {
464 // we want to find a different way. so move known way to the end of the list
465 wantedParents.remove(parent);
466 wantedParents.add(parent);
467 }
468
469 for (final Way way : wantedParents) {
470 List<Node> nextNodes = new ArrayList<>();
471 int pos = way.getNodes().indexOf(node);
472 if (pos > 0) {
473 nextNodes.add(way.getNode(pos - 1));
474 }
475 if (pos + 1 < way.getNodesCount()) {
476 nextNodes.add(way.getNode(pos + 1));
477 }
478 for (Node next : nextNodes) {
479 final boolean containsN = visited.contains(next);
480 visited.add(next);
481 if (!containsN && isConnectedTo(next, visited,
482 len + node.getCoor().greatCircleDistance(next.getCoor()), way)) {
483 return true;
484 }
485 }
486 }
487 }
488 return false;
489 }
490
491 private EastNorth calcClosest(Node n) {
492 return Geometry.closestPointToSegment(n1.getEastNorth(), n2.getEastNorth(), n.getEastNorth());
493 }
494
495 double getDist(Node n) {
496 EastNorth closest = calcClosest(n);
497 return n.getCoor().greatCircleDistance(ProjectionRegistry.getProjection().eastNorth2latlon(closest));
498 }
499
500 private boolean nearby(Node n, double dist) {
501 if (w.containsNode(n))
502 return false;
503 double d = getDist(n);
504 return !Double.isNaN(d) && d < dist;
505 }
506
507 private BBox getBounds(double fudge) {
508 double x1 = n1.getCoor().lon();
509 double x2 = n2.getCoor().lon();
510 if (x1 > x2) {
511 double tmpx = x1;
512 x1 = x2;
513 x2 = tmpx;
514 }
515 double y1 = n1.getCoor().lat();
516 double y2 = n2.getCoor().lat();
517 if (y1 > y2) {
518 double tmpy = y1;
519 y1 = y2;
520 y2 = tmpy;
521 }
522 LatLon topLeft = new LatLon(y2+fudge, x1-fudge);
523 LatLon botRight = new LatLon(y1-fudge, x2+fudge);
524 return new BBox(topLeft, botRight);
525 }
526
527 /**
528 * We know that any point near the line segment must be at
529 * least as close as the other end of the line, plus
530 * a little fudge for the distance away (dist)
531 * @param dist fudge to add
532 * @return collection of nearby nodes
533 */
534 Collection<Node> nearbyNodes(double dist) {
535 BBox bounds = this.getBounds(dist * (360.0d / (Ellipsoid.WGS84.a * 2 * Math.PI)));
536 List<Node> result = null;
537 List<Node> foundNodes = searchNodes.search(bounds);
538 for (Node n : foundNodes) {
539 if (!nearby(n, dist)) {
540 continue;
541 }
542 // It is actually very rare for us to find a node
543 // so defer as much of the work as possible, like
544 // allocating the hash set
545 if (result == null) {
546 result = new ArrayList<>();
547 }
548 result.add(n);
549 }
550 return result == null ? Collections.emptyList() : result;
551 }
552
553 private boolean obstacleBetween(Node endnode) {
554 EastNorth en = endnode.getEastNorth();
555 EastNorth closest = calcClosest(endnode);
556 LatLon llClosest = ProjectionRegistry.getProjection().eastNorth2latlon(closest);
557 // find obstacles between end node and way segment
558 BBox bbox = new BBox(endnode.getCoor(), llClosest);
559 for (Way nearbyWay : ds.searchWays(bbox)) {
560 if (nearbyWay != w && nearbyWay.isUsable() && isObstacle(nearbyWay)
561 && !endnode.getParentWays().contains(nearbyWay)) {
562 //make sure that the obstacle is really between endnode and the highway segment, not just close to or around them
563 Iterator<Node> iter = nearbyWay.getNodes().iterator();
564 EastNorth prev = iter.next().getEastNorth();
565 while (iter.hasNext()) {
566 EastNorth curr = iter.next().getEastNorth();
567 if (Geometry.getSegmentSegmentIntersection(closest, en, prev, curr) != null) {
568 return true;
569 }
570 prev = curr;
571 }
572 }
573 }
574 return false;
575 }
576
577 private boolean isObstacle(Way w) {
578 return w.hasKey("barrier", "waterway") || isBuilding(w) || w.hasTag("man_made", "embankment", "dyke");
579 }
580 }
581
582 List<MyWaySegment> getWaySegments(Way w) {
583 List<MyWaySegment> ret = new ArrayList<>();
584 if (!w.isUsable() || w.isKeyTrue("disused"))
585 return ret;
586
587 int size = w.getNodesCount();
588 boolean concersArea = w.concernsArea();
589 for (int i = 1; i < size; ++i) {
590 if (i < size-1) {
591 addNode(w.getNode(i), middlenodes);
592 }
593 Node a = w.getNode(i-1);
594 Node b = w.getNode(i);
595 if (a.isDrawable() && b.isDrawable()) {
596 MyWaySegment ws = new MyWaySegment(w, a, b, concersArea);
597 ret.add(ws);
598 }
599 }
600 return ret;
601 }
602
603 @Override
604 public void visit(Way w) {
605 if (partialSelection) {
606 waysToTest.add(w);
607 }
608 }
609
610 @Override
611 public void visit(Node n) {
612 if (partialSelection) {
613 nodesToTest.add(n);
614 }
615 }
616
617 private void addNode(Node n, Set<Node> s) {
618 boolean m = middlenodes.contains(n);
619 boolean e = endnodes.contains(n);
620 boolean o = othernodes.contains(n);
621 if (!m && !e && !o) {
622 s.add(n);
623 } else if (!o) {
624 othernodes.add(n);
625 if (e) {
626 endnodes.remove(n);
627 } else {
628 middlenodes.remove(n);
629 }
630 }
631 }
632}
Note: See TracBrowser for help on using the repository browser.