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