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