001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.geom.Point2D; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.List; 012import java.util.Map; 013import java.util.Objects; 014import java.util.Set; 015 016import org.openstreetmap.josm.data.coor.EastNorth; 017import org.openstreetmap.josm.data.osm.OsmPrimitive; 018import org.openstreetmap.josm.data.osm.OsmUtils; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.Way; 021import org.openstreetmap.josm.data.osm.WaySegment; 022import org.openstreetmap.josm.data.validation.OsmValidator; 023import org.openstreetmap.josm.data.validation.Severity; 024import org.openstreetmap.josm.data.validation.Test; 025import org.openstreetmap.josm.data.validation.TestError; 026import org.openstreetmap.josm.data.validation.util.ValUtil; 027import org.openstreetmap.josm.gui.progress.ProgressMonitor; 028import org.openstreetmap.josm.tools.Logging; 029 030/** 031 * Tests if there are segments that crosses in the same layer/level 032 * 033 * @author frsantos 034 */ 035public abstract class CrossingWays extends Test { 036 037 static final String BARRIER = "barrier"; 038 static final String HIGHWAY = "highway"; 039 static final String RAILWAY = "railway"; 040 static final String WATERWAY = "waterway"; 041 static final String LANDUSE = "landuse"; 042 043 static final class MessageHelper { 044 final String message; 045 final int code; 046 047 MessageHelper(String message, int code) { 048 this.message = message; 049 this.code = code; 050 } 051 } 052 053 /** 054 * Type of way. Entries have to be declared in alphabetical order, see sort below. 055 */ 056 private enum WayType { 057 BARRIER, BUILDING, HIGHWAY, RAILWAY, RESIDENTIAL_AREA, WATERWAY, WAY; 058 059 static WayType of(Way w) { 060 if (w.hasKey(CrossingWays.BARRIER)) 061 return BARRIER; 062 if (isBuilding(w)) 063 return BUILDING; 064 else if (w.hasKey(CrossingWays.HIGHWAY)) 065 return HIGHWAY; 066 else if (isRailway(w)) 067 return RAILWAY; 068 else if (isResidentialArea(w)) 069 return RESIDENTIAL_AREA; 070 else if (w.hasKey(CrossingWays.WATERWAY)) 071 return WATERWAY; 072 else 073 return WAY; 074 } 075 } 076 077 /** All way segments, grouped by cells */ 078 private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000); 079 /** The already detected ways in error */ 080 private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50); 081 082 protected final int code; 083 084 /** 085 * General crossing ways test. 086 */ 087 public static class Ways extends CrossingWays { 088 089 protected static final int CROSSING_WAYS = 601; 090 091 /** 092 * Constructs a new crossing {@code Ways} test. 093 */ 094 public Ways() { 095 super(tr("Crossing ways"), CROSSING_WAYS); 096 } 097 098 @Override 099 public boolean isPrimitiveUsable(OsmPrimitive w) { 100 return super.isPrimitiveUsable(w) 101 && !isProposedOrAbandoned(w) 102 && (isHighway(w) 103 || w.hasKey(WATERWAY) 104 || isRailway(w) 105 || isCoastline(w) 106 || isBuilding(w) 107 || w.hasKey(BARRIER) 108 || isResidentialArea(w)); 109 } 110 111 @Override 112 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 113 if (w1 == w2) 114 return false; 115 if (areLayerOrLevelDifferent(w1, w2)) { 116 return true; 117 } 118 if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) { 119 return true; 120 } 121 if ((w1.hasKey(BARRIER, HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w2)) 122 || (w2.hasKey(BARRIER, HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w1))) 123 return true; 124 if (isSubwayOrTramOrRazed(w2)) { 125 return true; 126 } 127 if (isCoastline(w1) != isCoastline(w2)) { 128 return true; 129 } 130 if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank")) 131 || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) { 132 return true; 133 } 134 return isProposedOrAbandoned(w2); 135 } 136 137 @Override 138 MessageHelper createMessage(Way w1, Way w2) { 139 WayType[] types = {WayType.of(w1), WayType.of(w2)}; 140 Arrays.sort(types); 141 142 if (types[0] == types[1]) { 143 switch (types[0]) { 144 case BARRIER: 145 return new MessageHelper(tr("Crossing barriers"), 603); 146 case BUILDING: 147 return new MessageHelper(tr("Crossing buildings"), 610); 148 case HIGHWAY: 149 return new MessageHelper(tr("Crossing highways"), 620); 150 case RAILWAY: 151 return new MessageHelper(tr("Crossing railways"), 630); 152 case RESIDENTIAL_AREA: 153 return new MessageHelper(tr("Crossing residential areas"), 640); 154 case WATERWAY: 155 return new MessageHelper(tr("Crossing waterways"), 650); 156 case WAY: 157 default: 158 return new MessageHelper(tr("Crossing ways"), CROSSING_WAYS); 159 } 160 } else { 161 switch (types[0]) { 162 case BARRIER: 163 switch (types[1]) { 164 case BUILDING: 165 return new MessageHelper(tr("Crossing barrier/building"), 661); 166 case HIGHWAY: 167 return new MessageHelper(tr("Crossing barrier/highway"), 662); 168 case RAILWAY: 169 return new MessageHelper(tr("Crossing barrier/railway"), 663); 170 case WATERWAY: 171 return new MessageHelper(tr("Crossing barrier/waterway"), 664); 172 case WAY: 173 default: 174 return new MessageHelper(tr("Crossing barrier/way"), 665); 175 } 176 case BUILDING: 177 switch (types[1]) { 178 case HIGHWAY: 179 return new MessageHelper(tr("Crossing building/highway"), 612); 180 case RAILWAY: 181 return new MessageHelper(tr("Crossing building/railway"), 613); 182 case RESIDENTIAL_AREA: 183 return new MessageHelper(tr("Crossing building/residential area"), 614); 184 case WATERWAY: 185 return new MessageHelper(tr("Crossing building/waterway"), 615); 186 case WAY: 187 default: 188 return new MessageHelper(tr("Crossing building/way"), 611); 189 } 190 case HIGHWAY: 191 switch (types[1]) { 192 case RAILWAY: 193 return new MessageHelper(tr("Crossing highway/railway"), 622); 194 case WATERWAY: 195 return new MessageHelper(tr("Crossing highway/waterway"), 623); 196 case WAY: 197 default: 198 return new MessageHelper(tr("Crossing highway/way"), 621); 199 } 200 case RAILWAY: 201 switch (types[1]) { 202 case WATERWAY: 203 return new MessageHelper(tr("Crossing railway/waterway"), 632); 204 case WAY: 205 default: 206 return new MessageHelper(tr("Crossing railway/way"), 631); 207 } 208 case RESIDENTIAL_AREA: 209 switch (types[1]) { 210 case WAY: 211 default: 212 return new MessageHelper(tr("Crossing residential area/way"), 641); 213 } 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) || (((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 for (String type : s1) { 248 if (s2.contains(type)) 249 return false; 250 } 251 return true; 252 } 253 254 /** 255 * Collect all boundary tag values of the way and its parent relations 256 * @param w the way to check 257 * @return set with the found boundary tag values 258 */ 259 private static Set<String> getBoundaryTags(Way w) { 260 final Set<String> types = new HashSet<>(); 261 String type = w.get("boundary"); 262 if (type != null) 263 types.add(type); 264 w.referrers(Relation.class).filter(Relation::isMultipolygon).map(r -> r.get("boundary")) 265 .filter(Objects::nonNull).forEach(types::add); 266 types.remove("protected_area"); 267 return types; 268 } 269 270 @Override 271 public void visit(Relation r) { 272 for (Way w : r.getMemberPrimitives(Way.class)) { 273 if (!w.isIncomplete()) 274 visit(w); 275 } 276 } 277 } 278 279 /** 280 * Self crossing ways test (for all the rest) 281 */ 282 public static class SelfCrossing extends CrossingWays { 283 284 protected static final int CROSSING_SELF = 604; 285 286 CrossingWays.Ways normalTest = new Ways(); 287 CrossingWays.Boundaries boundariesTest = new Boundaries(); 288 289 /** 290 * Constructs a new SelfIntersection test. 291 */ 292 public SelfCrossing() { 293 super(tr("Self crossing ways"), CROSSING_SELF); 294 } 295 296 @Override 297 public boolean isPrimitiveUsable(OsmPrimitive p) { 298 return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) 299 || boundariesTest.isPrimitiveUsable(p)); 300 } 301 302 @Override 303 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 304 return w1 != w2; // should not happen 305 } 306 } 307 308 /** 309 * Constructs a new {@code CrossingWays} test. 310 * @param title The test title 311 * @param code The test code 312 * @since 12958 313 */ 314 public CrossingWays(String title, int code) { 315 super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " + 316 "but are not connected by a node.")); 317 this.code = code; 318 } 319 320 @Override 321 public void startTest(ProgressMonitor monitor) { 322 super.startTest(monitor); 323 cellSegments.clear(); 324 seenWays.clear(); 325 } 326 327 @Override 328 public void endTest() { 329 super.endTest(); 330 cellSegments.clear(); 331 seenWays.clear(); 332 } 333 334 static boolean isCoastline(OsmPrimitive w) { 335 return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir"); 336 } 337 338 static boolean isHighway(OsmPrimitive w) { 339 return w.hasTagDifferent(HIGHWAY, "rest_area", "services", "bus_stop", "platform"); 340 } 341 342 static boolean isRailway(OsmPrimitive w) { 343 return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w); 344 } 345 346 static boolean isSubwayOrTramOrRazed(OsmPrimitive w) { 347 return w.hasTag(RAILWAY, "subway", "tram", "razed") || 348 (w.hasTag(RAILWAY, "construction") && w.hasTag("construction", "tram")) || 349 (w.hasTag(RAILWAY, "disused") && w.hasTag("disused", "tram")); 350 } 351 352 static boolean isProposedOrAbandoned(OsmPrimitive w) { 353 return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned"); 354 } 355 356 abstract boolean ignoreWaySegmentCombination(Way w1, Way w2); 357 358 MessageHelper createMessage(Way w1, Way w2) { 359 return new MessageHelper(this.name, this.code); 360 } 361 362 @Override 363 public void visit(Way w) { 364 if (this instanceof SelfCrossing) { 365 // free memory, we are not interested in previous ways 366 cellSegments.clear(); 367 seenWays.clear(); 368 } 369 370 int nodesSize = w.getNodesCount(); 371 for (int i = 0; i < nodesSize - 1; i++) { 372 final WaySegment es1 = new WaySegment(w, i); 373 final EastNorth en1 = es1.getFirstNode().getEastNorth(); 374 final EastNorth en2 = es1.getSecondNode().getEastNorth(); 375 if (en1 == null || en2 == null) { 376 Logging.warn("Crossing ways test skipped " + es1); 377 continue; 378 } 379 for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) { 380 for (WaySegment es2 : segments) { 381 List<Way> prims; 382 List<WaySegment> highlight; 383 384 if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) { 385 continue; 386 } 387 388 prims = new ArrayList<>(); 389 prims.add(es1.way); 390 if (es1.way != es2.way) 391 prims.add(es2.way); 392 if ((highlight = seenWays.get(prims)) == null) { 393 highlight = new ArrayList<>(); 394 highlight.add(es1); 395 highlight.add(es2); 396 397 final MessageHelper message = createMessage(es1.way, es2.way); 398 errors.add(TestError.builder(this, Severity.WARNING, message.code) 399 .message(message.message) 400 .primitives(prims) 401 .highlightWaySegments(highlight) 402 .build()); 403 seenWays.put(prims, highlight); 404 } else { 405 highlight.add(es1); 406 highlight.add(es2); 407 } 408 } 409 segments.add(es1); 410 } 411 } 412 } 413 414 private static boolean areLayerOrLevelDifferent(Way w1, Way w2) { 415 return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2)) 416 || !Objects.equals(w1.get("level"), w2.get("level")); 417 } 418 419 /** 420 * Returns all the cells this segment crosses. Each cell contains the list 421 * of segments already processed 422 * @param cellSegments map with already collected way segments 423 * @param n1 The first EastNorth 424 * @param n2 The second EastNorth 425 * @return A list with all the cells the segment crosses 426 */ 427 public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) { 428 List<List<WaySegment>> cells = new ArrayList<>(); 429 for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) { 430 cells.add(cellSegments.computeIfAbsent(cell, k -> new ArrayList<>())); 431 } 432 return cells; 433 } 434}