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