source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java@ 17393

Last change on this file since 17393 was 17393, checked in by GerdP, 3 years ago

see #10205: Strange align nodes in circle behavior

  • allow to define center node also when a single unclosed way is selected
  • add robustness and more unit tests for evaluation of valid selections
  • if only nodes and no way are selected, order the nodes using the angle to the agv. east-north position. This should produce a predictable result
  • if way(s) are selected the order the nodes is determined by the occurence in the way(s). Self-Intersecting polygons are rejected if no center node is given
  • don't throw InvalidSelection when selection is valid but no point was moved, let buildCommand() return null instead

With a selection that gives a center point and way(s) which are not even close to a circular shape the result might still be surprising. Sometimes the way nodes are arranged around the center node, sometimes they are moved so that a circle arc with nearly the same length is produced. The result changes significantly when the way nodes are also selected. Subject to further improvements.

  • Property svn:eol-style set to native
File size: 18.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.geom.Point2D;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.HashMap;
10import java.util.HashSet;
11import java.util.List;
12import java.util.Map;
13import java.util.Objects;
14import java.util.Set;
15import java.util.stream.Collectors;
16
17import org.openstreetmap.josm.data.coor.EastNorth;
18import org.openstreetmap.josm.data.osm.OsmPrimitive;
19import org.openstreetmap.josm.data.osm.OsmUtils;
20import org.openstreetmap.josm.data.osm.Relation;
21import org.openstreetmap.josm.data.osm.Way;
22import org.openstreetmap.josm.data.osm.WaySegment;
23import org.openstreetmap.josm.data.validation.OsmValidator;
24import org.openstreetmap.josm.data.validation.Severity;
25import org.openstreetmap.josm.data.validation.Test;
26import org.openstreetmap.josm.data.validation.TestError;
27import org.openstreetmap.josm.data.validation.util.ValUtil;
28import org.openstreetmap.josm.gui.progress.ProgressMonitor;
29import org.openstreetmap.josm.tools.CheckParameterUtil;
30import org.openstreetmap.josm.tools.Logging;
31
32/**
33 * Tests if there are segments that crosses in the same layer/level
34 *
35 * @author frsantos
36 */
37public abstract class CrossingWays extends Test {
38
39 static final String BARRIER = "barrier";
40 static final String HIGHWAY = "highway";
41 static final String RAILWAY = "railway";
42 static final String WATERWAY = "waterway";
43 static final String LANDUSE = "landuse";
44
45 static final class MessageHelper {
46 final String message;
47 final int code;
48
49 MessageHelper(String message, int code) {
50 this.message = message;
51 this.code = code;
52 }
53 }
54
55 /**
56 * Type of way. Entries have to be declared in alphabetical order, see sort below.
57 */
58 private enum WayType {
59 BARRIER, BUILDING, HIGHWAY, RAILWAY, RESIDENTIAL_AREA, WATERWAY, WAY;
60
61 static WayType of(Way w) {
62 if (w.hasKey(CrossingWays.BARRIER))
63 return BARRIER;
64 if (isBuilding(w))
65 return BUILDING;
66 else if (w.hasKey(CrossingWays.HIGHWAY))
67 return HIGHWAY;
68 else if (isRailway(w))
69 return RAILWAY;
70 else if (isResidentialArea(w))
71 return RESIDENTIAL_AREA;
72 else if (w.hasKey(CrossingWays.WATERWAY))
73 return WATERWAY;
74 else
75 return WAY;
76 }
77 }
78
79 /** All way segments, grouped by cells */
80 private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
81 /** The already detected ways in error */
82 private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);
83
84 protected final int code;
85
86 /**
87 * General crossing ways test.
88 */
89 public static class Ways extends CrossingWays {
90
91 protected static final int CROSSING_WAYS = 601;
92
93 /**
94 * Constructs a new crossing {@code Ways} test.
95 */
96 public Ways() {
97 super(tr("Crossing ways"), CROSSING_WAYS);
98 }
99
100 @Override
101 public boolean isPrimitiveUsable(OsmPrimitive w) {
102 return super.isPrimitiveUsable(w)
103 && !isProposedOrAbandoned(w)
104 && (isHighway(w)
105 || w.hasKey(WATERWAY)
106 || isRailway(w)
107 || isCoastline(w)
108 || isBuilding(w)
109 || w.hasKey(BARRIER)
110 || isResidentialArea(w));
111 }
112
113 @Override
114 boolean ignoreWaySegmentCombination(Way w1, Way w2) {
115 if (w1 == w2)
116 return false;
117 if (areLayerOrLevelDifferent(w1, w2)) {
118 return true;
119 }
120 if (isBuilding(w1) && isBuilding(w2))
121 return true;
122 if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
123 return true;
124 }
125 if (((isResidentialArea(w1) || w1.hasKey(BARRIER, HIGHWAY, RAILWAY, WATERWAY)) && isResidentialArea(w2))
126 || ((isResidentialArea(w2) || w2.hasKey(BARRIER, HIGHWAY, RAILWAY, WATERWAY)) && isResidentialArea(w1)))
127 return true;
128 if (isSubwayOrTramOrRazed(w2)) {
129 return true;
130 }
131 if (w1.hasKey(RAILWAY) && w2.hasKey(RAILWAY) && w1.hasTag(RAILWAY, "yard") != w2.hasTag(RAILWAY, "yard")) {
132 return true; // see #20089
133 }
134 if (isCoastline(w1) != isCoastline(w2)) {
135 return true;
136 }
137 if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank"))
138 || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) {
139 return true;
140 }
141 return isProposedOrAbandoned(w2);
142 }
143
144 @Override
145 MessageHelper createMessage(Way w1, Way w2) {
146 WayType[] types = {WayType.of(w1), WayType.of(w2)};
147 Arrays.sort(types);
148
149 if (types[0] == types[1]) {
150 switch (types[0]) {
151 // 610 and 640 where removed for #16707
152 case BARRIER:
153 return new MessageHelper(tr("Crossing barriers"), 603);
154 case HIGHWAY:
155 return new MessageHelper(tr("Crossing highways"), 620);
156 case RAILWAY:
157 return new MessageHelper(tr("Crossing railways"), 630);
158 case WATERWAY:
159 return new MessageHelper(tr("Crossing waterways"), 650);
160 case WAY:
161 default:
162 return new MessageHelper(tr("Crossing ways"), CROSSING_WAYS);
163 }
164 } else {
165 switch (types[0]) {
166 case BARRIER:
167 switch (types[1]) {
168 case BUILDING:
169 return new MessageHelper(tr("Crossing barrier/building"), 661);
170 case HIGHWAY:
171 return new MessageHelper(tr("Crossing barrier/highway"), 662);
172 case RAILWAY:
173 return new MessageHelper(tr("Crossing barrier/railway"), 663);
174 case WATERWAY:
175 return new MessageHelper(tr("Crossing barrier/waterway"), 664);
176 case WAY:
177 default:
178 return new MessageHelper(tr("Crossing barrier/way"), 665);
179 }
180 case BUILDING:
181 switch (types[1]) {
182 case HIGHWAY:
183 return new MessageHelper(tr("Crossing building/highway"), 612);
184 case RAILWAY:
185 return new MessageHelper(tr("Crossing building/railway"), 613);
186 case RESIDENTIAL_AREA:
187 return new MessageHelper(tr("Crossing building/residential area"), 614);
188 case WATERWAY:
189 return new MessageHelper(tr("Crossing building/waterway"), 615);
190 case WAY:
191 default:
192 return new MessageHelper(tr("Crossing building/way"), 611);
193 }
194 case HIGHWAY:
195 switch (types[1]) {
196 case RAILWAY:
197 return new MessageHelper(tr("Crossing highway/railway"), 622);
198 case WATERWAY:
199 return new MessageHelper(tr("Crossing highway/waterway"), 623);
200 case WAY:
201 default:
202 return new MessageHelper(tr("Crossing highway/way"), 621);
203 }
204 case RAILWAY:
205 switch (types[1]) {
206 case WATERWAY:
207 return new MessageHelper(tr("Crossing railway/waterway"), 632);
208 case WAY:
209 default:
210 return new MessageHelper(tr("Crossing railway/way"), 631);
211 }
212 case RESIDENTIAL_AREA:
213 return new MessageHelper(tr("Crossing residential area/way"), 641);
214 case WATERWAY:
215 default:
216 return new MessageHelper(tr("Crossing waterway/way"), 651);
217 }
218 }
219 }
220 }
221
222 /**
223 * Crossing boundaries ways test.
224 */
225 public static class Boundaries extends CrossingWays {
226
227 protected static final int CROSSING_BOUNDARIES = 602;
228
229 /**
230 * Constructs a new crossing {@code Boundaries} test.
231 */
232 public Boundaries() {
233 super(tr("Crossing boundaries"), CROSSING_BOUNDARIES);
234 }
235
236 @Override
237 public boolean isPrimitiveUsable(OsmPrimitive p) {
238 return super.isPrimitiveUsable(p) && p.hasKey("boundary") && !p.hasTag("boundary", "protected_area")
239 && (!(p instanceof Relation) || p.isMultipolygon());
240 }
241
242 @Override
243 boolean ignoreWaySegmentCombination(Way w1, Way w2) {
244 // ignore ways which have no common boundary tag value
245 Set<String> s1 = getBoundaryTags(w1);
246 Set<String> s2 = getBoundaryTags(w2);
247 return s1.stream().noneMatch(s2::contains);
248 }
249
250 /**
251 * Collect all boundary tag values of the way and its parent relations
252 * @param w the way to check
253 * @return set with the found boundary tag values
254 */
255 private static Set<String> getBoundaryTags(Way w) {
256 final Set<String> types = new HashSet<>();
257 String type = w.get("boundary");
258 if (type != null)
259 types.add(type);
260 w.referrers(Relation.class).filter(Relation::isMultipolygon).map(r -> r.get("boundary"))
261 .filter(Objects::nonNull).forEach(types::add);
262 types.remove("protected_area");
263 return types;
264 }
265
266 @Override
267 public void visit(Relation r) {
268 for (Way w : r.getMemberPrimitives(Way.class)) {
269 if (!w.isIncomplete())
270 visit(w);
271 }
272 }
273 }
274
275 /**
276 * Self crossing ways test (for all the rest)
277 */
278 public static class SelfCrossing extends CrossingWays {
279
280 protected static final int CROSSING_SELF = 604;
281
282 CrossingWays.Ways normalTest = new Ways();
283 CrossingWays.Boundaries boundariesTest = new Boundaries();
284
285 /**
286 * Constructs a new SelfIntersection test.
287 */
288 public SelfCrossing() {
289 super(tr("Self crossing ways"), CROSSING_SELF);
290 }
291
292 @Override
293 public boolean isPrimitiveUsable(OsmPrimitive p) {
294 return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p)
295 || boundariesTest.isPrimitiveUsable(p));
296 }
297
298 @Override
299 boolean ignoreWaySegmentCombination(Way w1, Way w2) {
300 return w1 != w2; // should not happen
301 }
302 }
303
304 /**
305 * Constructs a new {@code CrossingWays} test.
306 * @param title The test title
307 * @param code The test code
308 * @since 12958
309 */
310 protected CrossingWays(String title, int code) {
311 super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
312 "but are not connected by a node."));
313 this.code = code;
314 }
315
316 @Override
317 public void startTest(ProgressMonitor monitor) {
318 super.startTest(monitor);
319 cellSegments.clear();
320 seenWays.clear();
321 }
322
323 @Override
324 public void endTest() {
325 super.endTest();
326 cellSegments.clear();
327 seenWays.clear();
328 }
329
330 static boolean isCoastline(OsmPrimitive w) {
331 return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir");
332 }
333
334 static boolean isHighway(OsmPrimitive w) {
335 return w.hasTagDifferent(HIGHWAY, "rest_area", "services", "bus_stop", "platform");
336 }
337
338 static boolean isRailway(OsmPrimitive w) {
339 return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
340 }
341
342 static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
343 return w.hasTag(RAILWAY, "subway", "tram", "razed") ||
344 (w.hasTag(RAILWAY, "construction") && w.hasTag("construction", "tram")) ||
345 (w.hasTag(RAILWAY, "disused") && w.hasTag("disused", "tram"));
346 }
347
348 static boolean isProposedOrAbandoned(OsmPrimitive w) {
349 return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
350 }
351
352 abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
353
354 MessageHelper createMessage(Way w1, Way w2) {
355 return new MessageHelper(this.name, this.code);
356 }
357
358 @Override
359 public void visit(Way w) {
360 if (this instanceof SelfCrossing) {
361 // free memory, we are not interested in previous ways
362 cellSegments.clear();
363 seenWays.clear();
364 }
365
366 int nodesSize = w.getNodesCount();
367 for (int i = 0; i < nodesSize - 1; i++) {
368 final WaySegment es1 = new WaySegment(w, i);
369 final EastNorth en1 = es1.getFirstNode().getEastNorth();
370 final EastNorth en2 = es1.getSecondNode().getEastNorth();
371 if (en1 == null || en2 == null) {
372 Logging.warn("Crossing ways test skipped " + es1);
373 continue;
374 }
375 for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
376 for (WaySegment es2 : segments) {
377 List<Way> prims;
378 List<WaySegment> highlight;
379
380 if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
381 continue;
382 }
383
384 prims = new ArrayList<>();
385 prims.add(es1.way);
386 if (es1.way != es2.way)
387 prims.add(es2.way);
388 if ((highlight = seenWays.get(prims)) == null) {
389 highlight = new ArrayList<>();
390 highlight.add(es1);
391 highlight.add(es2);
392
393 final MessageHelper message = createMessage(es1.way, es2.way);
394 errors.add(TestError.builder(this, Severity.WARNING, message.code)
395 .message(message.message)
396 .primitives(prims)
397 .highlightWaySegments(highlight)
398 .build());
399 seenWays.put(prims, highlight);
400 } else {
401 highlight.add(es1);
402 highlight.add(es2);
403 }
404 }
405 segments.add(es1);
406 }
407 }
408 }
409
410 private static boolean areLayerOrLevelDifferent(Way w1, Way w2) {
411 return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))
412 || !Objects.equals(w1.get("level"), w2.get("level"));
413 }
414
415 /**
416 * Returns all the cells this segment crosses. Each cell contains the list
417 * of segments already processed
418 * @param cellSegments map with already collected way segments
419 * @param n1 The first EastNorth
420 * @param n2 The second EastNorth
421 * @return A list with all the cells the segment crosses
422 */
423 public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {
424 return ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail()).stream()
425 .map(cell -> cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()))
426 .collect(Collectors.toList());
427 }
428
429 /**
430 * Find ways which are crossing without sharing a node.
431 * @param w way that is to be checked
432 * @param cellSegments map with already collected way segments
433 * @param crossingWays map to collect crossing ways and related segments
434 * @param findSharedWaySegments true: find shared way segments instead of crossings
435 */
436 public static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment>> cellSegments,
437 Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) {
438 int nodesSize = w.getNodesCount();
439 for (int i = 0; i < nodesSize - 1; i++) {
440 final WaySegment es1 = new WaySegment(w, i);
441 final EastNorth en1 = es1.getFirstNode().getEastNorth();
442 final EastNorth en2 = es1.getSecondNode().getEastNorth();
443 if (en1 == null || en2 == null) {
444 Logging.warn("Crossing ways test skipped " + es1);
445 continue;
446 }
447 for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
448 for (WaySegment es2 : segments) {
449
450 List<WaySegment> highlight;
451 if (es2.way == w // reported by CrossingWays.SelfIntersection
452 || (findSharedWaySegments && !es1.isSimilar(es2))
453 || (!findSharedWaySegments && !es1.intersects(es2)))
454 continue;
455
456 List<Way> prims = Arrays.asList(es1.way, es2.way);
457 if ((highlight = crossingWays.get(prims)) == null) {
458 highlight = new ArrayList<>();
459 highlight.add(es1);
460 highlight.add(es2);
461 crossingWays.put(prims, highlight);
462 } else {
463 highlight.add(es1);
464 highlight.add(es2);
465 }
466 }
467 segments.add(es1);
468 }
469 }
470 }
471
472 /**
473 * Check if the given way is self crossing
474 * @param way the way to check
475 * @return {@code true} if one or more segments of the way are crossing
476 * @see SelfIntersectingWay
477 * @since xxx
478 */
479 public static boolean isSelfCrossing(Way way) {
480 CheckParameterUtil.ensureParameterNotNull(way, "way");
481 SelfCrossing test = new SelfCrossing();
482 test.visit(way);
483 return !test.getErrors().isEmpty();
484 }
485}
Note: See TracBrowser for help on using the repository browser.