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.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Objects; 012 013import org.openstreetmap.josm.Main; 014import org.openstreetmap.josm.data.coor.EastNorth; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.Relation; 017import org.openstreetmap.josm.data.osm.Way; 018import org.openstreetmap.josm.data.osm.WaySegment; 019import org.openstreetmap.josm.data.validation.OsmValidator; 020import org.openstreetmap.josm.data.validation.Severity; 021import org.openstreetmap.josm.data.validation.Test; 022import org.openstreetmap.josm.data.validation.TestError; 023import org.openstreetmap.josm.data.validation.util.ValUtil; 024import org.openstreetmap.josm.gui.progress.ProgressMonitor; 025 026/** 027 * Tests if there are segments that crosses in the same layer 028 * 029 * @author frsantos 030 */ 031public abstract class CrossingWays extends Test { 032 protected static final int CROSSING_WAYS = 601; 033 034 private static final String HIGHWAY = "highway"; 035 private static final String RAILWAY = "railway"; 036 private static final String WATERWAY = "waterway"; 037 038 /** All way segments, grouped by cells */ 039 private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000); 040 /** The already detected ways in error */ 041 private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50); 042 043 /** 044 * General crossing ways test. 045 */ 046 public static class Ways extends CrossingWays { 047 048 /** 049 * Constructs a new crossing {@code Ways} test. 050 */ 051 public Ways() { 052 super(tr("Crossing ways")); 053 } 054 055 @Override 056 public boolean isPrimitiveUsable(OsmPrimitive w) { 057 return super.isPrimitiveUsable(w) 058 && !isProposedOrAbandoned(w) 059 && ((w.hasKey(HIGHWAY) && !w.hasTag(HIGHWAY, "rest_area", "services")) 060 || w.hasKey(WATERWAY) 061 || (w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w)) 062 || isCoastline(w) 063 || isBuilding(w)); 064 } 065 066 @Override 067 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 068 if (w1 == w2) 069 return false; 070 if (!Objects.equals(getLayer(w1), getLayer(w2))) { 071 return true; 072 } 073 if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) { 074 return true; 075 } 076 if (isSubwayOrTramOrRazed(w2)) { 077 return true; 078 } 079 if (isCoastline(w1) != isCoastline(w2)) { 080 return true; 081 } 082 if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank")) 083 || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) { 084 return true; 085 } 086 if (isProposedOrAbandoned(w2)) { 087 return true; 088 } 089 return false; 090 } 091 092 @Override 093 String createMessage(Way w1, Way w2) { 094 if (isBuilding(w1)) { 095 return tr("Crossing buildings"); 096 } else if (w1.hasKey(WATERWAY) && w2.hasKey(WATERWAY)) { 097 return tr("Crossing waterways"); 098 } else if ((w1.hasKey(HIGHWAY) && w2.hasKey(WATERWAY)) 099 || (w2.hasKey(HIGHWAY) && w1.hasKey(WATERWAY))) { 100 return tr("Crossing waterway/highway"); 101 } else { 102 return tr("Crossing ways"); 103 } 104 } 105 } 106 107 /** 108 * Crossing boundaries ways test. 109 */ 110 public static class Boundaries extends CrossingWays { 111 112 /** 113 * Constructs a new crossing {@code Boundaries} test. 114 */ 115 public Boundaries() { 116 super(tr("Crossing boundaries")); 117 } 118 119 @Override 120 public boolean isPrimitiveUsable(OsmPrimitive p) { 121 return super.isPrimitiveUsable(p) && p.hasKey("boundary") 122 && (!(p instanceof Relation) || (((Relation) p).isMultipolygon() && !((Relation) p).hasIncompleteMembers())); 123 } 124 125 @Override 126 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 127 return !Objects.equals(w1.get("boundary"), w2.get("boundary")); 128 } 129 130 @Override 131 String createMessage(Way w1, Way w2) { 132 return tr("Crossing boundaries"); 133 } 134 135 @Override 136 public void visit(Relation r) { 137 for (Way w : r.getMemberPrimitives(Way.class)) { 138 visit(w); 139 } 140 } 141 } 142 143 /** 144 * Crossing barriers ways test. 145 */ 146 public static class Barrier extends CrossingWays { 147 148 /** 149 * Constructs a new crossing {@code Barrier} test. 150 */ 151 public Barrier() { 152 super(tr("Crossing barriers")); 153 } 154 155 @Override 156 public boolean isPrimitiveUsable(OsmPrimitive p) { 157 return super.isPrimitiveUsable(p) && p.hasKey("barrier"); 158 } 159 160 @Override 161 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 162 if (!Objects.equals(getLayer(w1), getLayer(w2))) { 163 return true; 164 } 165 return false; 166 } 167 168 @Override 169 String createMessage(Way w1, Way w2) { 170 return tr("Crossing barriers"); 171 } 172 } 173 174 /** 175 * Self crossing ways test (for all the rest) 176 */ 177 public static class SelfCrossing extends CrossingWays { 178 CrossingWays.Ways normalTest = new Ways(); 179 CrossingWays.Barrier barrierTest = new Barrier(); 180 CrossingWays.Boundaries boundariesTest = new Boundaries(); 181 182 /** 183 * Constructs a new SelfIntersection test. 184 */ 185 public SelfCrossing() { 186 super(tr("Self crossing")); 187 } 188 189 @Override 190 public boolean isPrimitiveUsable(OsmPrimitive p) { 191 return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) || barrierTest.isPrimitiveUsable(p) 192 || boundariesTest.isPrimitiveUsable(p)); 193 } 194 195 @Override 196 boolean ignoreWaySegmentCombination(Way w1, Way w2) { 197 return w1 != w2; // should not happen 198 } 199 200 @Override 201 String createMessage(Way w1, Way w2) { 202 return tr("Self-crossing ways"); 203 } 204 } 205 206 /** 207 * Constructs a new {@code CrossingWays} test. 208 * @param title The test title 209 * @since 6691 210 */ 211 public CrossingWays(String title) { 212 super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " + 213 "but are not connected by a node.")); 214 } 215 216 @Override 217 public void startTest(ProgressMonitor monitor) { 218 super.startTest(monitor); 219 cellSegments.clear(); 220 seenWays.clear(); 221 } 222 223 @Override 224 public void endTest() { 225 super.endTest(); 226 cellSegments.clear(); 227 seenWays.clear(); 228 } 229 230 static String getLayer(OsmPrimitive w) { 231 String layer1 = w.get("layer"); 232 if ("0".equals(layer1)) { 233 layer1 = null; // 0 is default value for layer. 234 } 235 return layer1; 236 } 237 238 static boolean isCoastline(OsmPrimitive w) { 239 return w.hasTag("natural", "water", "coastline") || w.hasTag("landuse", "reservoir"); 240 } 241 242 static boolean isSubwayOrTramOrRazed(OsmPrimitive w) { 243 return w.hasTag(RAILWAY, "subway", "tram", "razed"); 244 } 245 246 static boolean isProposedOrAbandoned(OsmPrimitive w) { 247 return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned"); 248 } 249 250 abstract boolean ignoreWaySegmentCombination(Way w1, Way w2); 251 252 abstract String createMessage(Way w1, Way w2); 253 254 @Override 255 public void visit(Way w) { 256 if (this instanceof SelfCrossing) { 257 // free memory, we are not interested in previous ways 258 cellSegments.clear(); 259 seenWays.clear(); 260 } 261 262 int nodesSize = w.getNodesCount(); 263 for (int i = 0; i < nodesSize - 1; i++) { 264 final WaySegment es1 = new WaySegment(w, i); 265 final EastNorth en1 = es1.getFirstNode().getEastNorth(); 266 final EastNorth en2 = es1.getSecondNode().getEastNorth(); 267 if (en1 == null || en2 == null) { 268 Main.warn("Crossing ways test skipped "+es1); 269 continue; 270 } 271 for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) { 272 for (WaySegment es2 : segments) { 273 List<Way> prims; 274 List<WaySegment> highlight; 275 276 if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) { 277 continue; 278 } 279 280 prims = new ArrayList<>(); 281 prims.add(es1.way); 282 if (es1.way != es2.way) 283 prims.add(es2.way); 284 if ((highlight = seenWays.get(prims)) == null) { 285 highlight = new ArrayList<>(); 286 highlight.add(es1); 287 highlight.add(es2); 288 289 final String message = createMessage(es1.way, es2.way); 290 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 291 .message(message) 292 .primitives(prims) 293 .highlightWaySegments(highlight) 294 .build()); 295 seenWays.put(prims, highlight); 296 } else { 297 highlight.add(es1); 298 highlight.add(es2); 299 } 300 } 301 segments.add(es1); 302 } 303 } 304 } 305 306 /** 307 * Returns all the cells this segment crosses. Each cell contains the list 308 * of segments already processed 309 * @param cellSegments map with already collected way segments 310 * @param n1 The first EastNorth 311 * @param n2 The second EastNorth 312 * @return A list with all the cells the segment crosses 313 */ 314 public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) { 315 316 List<List<WaySegment>> cells = new ArrayList<>(); 317 for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail)) { 318 List<WaySegment> segments = cellSegments.get(cell); 319 if (segments == null) { 320 segments = new ArrayList<>(); 321 cellSegments.put(cell, segments); 322 } 323 cells.add(segments); 324 } 325 return cells; 326 } 327}