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