| 45 | // CHECKSTYLE.OFF: SingleSpaceSeparator |
| 46 | private static final Set<String> LINK_TO_HIGHWAYS = new HashSet<>(Arrays.asList( |
| 47 | "motorway", "motorway_link", |
| 48 | "trunk", "trunk_link", |
| 49 | "primary", "primary_link", |
| 50 | "secondary", "secondary_link", |
| 51 | "tertiary", "tertiary_link" |
| 52 | )); |
| 53 | |
59 | 69 | private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( |
60 | 70 | "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30", "zone40", |
61 | 71 | "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road")); |
172 | | final Set<OsmPrimitive> referrers = new HashSet<>(); |
| 181 | // check if connected to a high class road where the link must match the higher class |
| 182 | String highClass = null; |
| 183 | for (int i = 0; i < way.getNodesCount(); i++) { |
| 184 | Node n = way.getNode(i); |
| 185 | if (!IN_DOWNLOADED_AREA.test(n)) |
| 186 | return true; |
| 187 | Set<Way> otherWays = new HashSet<>(); |
| 188 | otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class)); |
| 189 | if (otherWays.size() == 1) |
| 190 | continue; |
| 191 | Iterator<Way> iter = otherWays.iterator(); |
| 192 | while (iter.hasNext()) { |
| 193 | Way w = iter.next(); |
| 194 | final String hw2 = w.get(HIGHWAY); |
| 195 | if (way == w || w.getNodesCount() < 2 || !w.isUsable() || hw2 == null) |
| 196 | iter.remove(); |
| 197 | else { |
| 198 | if ("motorway".equals(hw2)) { |
| 199 | highClass = "motorway"; |
| 200 | break; |
| 201 | } else if ("trunk".equals(hw2)) |
| 202 | highClass = "trunk"; |
| 203 | } |
| 204 | } |
| 205 | } |
174 | | if (way.isClosed()) { |
175 | | // for closed way we need to check all adjacent ways |
176 | | for (Node n: way.getNodes()) { |
177 | | referrers.addAll(n.getReferrers()); |
| 207 | if (highClass != null && !highway.equals(highClass + "_link")) { |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | for (int i = 0; i < way.getNodesCount(); i++) { |
| 212 | Node n = way.getNode(i); |
| 213 | Set<Way> otherWays = new HashSet<>(); |
| 214 | otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class)); |
| 215 | if (otherWays.size() == 1) |
| 216 | continue; |
| 217 | otherWays.removeIf(w -> w == way || !highway.startsWith(w.get(HIGHWAY)) || !LINK_TO_HIGHWAYS.contains(w.get(HIGHWAY))); |
| 218 | if (otherWays.isEmpty()) |
| 219 | continue; |
| 220 | |
| 221 | //TODO: ignore ways which are not allowed because of turn restrictions, oneway attributes or access rules? |
| 222 | HashSet<Way> sameTag = new HashSet<>(); |
| 223 | for (Way ow : otherWays) { |
| 224 | if (highway.equals(ow.get(HIGHWAY))) |
| 225 | sameTag.add(ow); |
| 226 | else |
| 227 | return true; |
| 229 | // we have way(s) with the same _link tag, ignore those with a sharp angle |
| 230 | final int pos = i; |
| 231 | sameTag.removeIf(w -> isSharpAngle(way, pos, w)); |
| 232 | if (!sameTag.isEmpty()) |
| 233 | return true; |
| 234 | } |
| 235 | return false; |
| 236 | |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Check if the two given connected ways form a sharp angle. |
| 241 | * @param way 1st way |
| 242 | * @param nodePos node position of connecting node in 1st way |
| 243 | * @param otherWay the 2nd way |
| 244 | * @return true if angle is sharp or way cannot be travelled because of oneway attributes |
| 245 | */ |
| 246 | private static boolean isSharpAngle(Way way, int nodePos, Way otherWay) { |
| 247 | Node n = way.getNode(nodePos); |
| 248 | int oneway = way.isOneway(); |
| 249 | if (oneway == 0) { |
| 250 | if ("roundabout".equals(way.get("junction"))) { |
| 251 | oneway = 1; |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | if (oneway != 1) { |
| 256 | Node prev = getPrevNode(way, nodePos); |
| 257 | if (prev != null && !onlySharpAngle(n, prev, otherWay)) |
| 258 | return false; |
| 259 | } |
| 260 | if (oneway != -1) { |
| 261 | Node next = getNextNode(way, nodePos); |
| 262 | if (next != null && !onlySharpAngle(n, next, otherWay)) |
| 263 | return false; |
| 264 | } |
| 265 | return true; |
| 266 | } |
| 267 | |
| 268 | private static Node getNextNode(Way way, int nodePos) { |
| 269 | if (nodePos + 1 > way.getNodesCount()) { |
| 270 | if (way.isClosed()) |
| 271 | return way.getNode(1); |
| 272 | return null; |
184 | | // Find ways of same class (exact class of class_link) |
185 | | List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter( |
186 | | otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", ""))) |
187 | | .collect(Collectors.toList()); |
188 | | if (sameClass.size() > 1) { |
189 | | // It is possible to have a class_link between 2 segments of same class |
190 | | // in roundabout designs that physically separate a specific turn from the main roundabout |
191 | | // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error |
192 | | for (Way w : sameClass) { |
193 | | if (w.hasTag("junction", "roundabout")) { |
194 | | return false; |
| 278 | private static Node getPrevNode(Way way, int nodePos) { |
| 279 | if (nodePos == 0) { |
| 280 | if (way.isClosed()) |
| 281 | return way.getNode(way.getNodesCount()-2); |
| 282 | return null; |
| 283 | } else { |
| 284 | return way.getNode(nodePos - 1); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | private static boolean onlySharpAngle(Node common, Node from, Way toWay) { |
| 289 | int oneway = toWay.isOneway(); |
| 290 | if (oneway == 0) { |
| 291 | if ("roundabout".equals(toWay.get("junction"))) { |
| 292 | oneway = 1; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | for (int i = 0; i < toWay.getNodesCount(); i++) { |
| 297 | if (common == toWay.getNode(i)) { |
| 298 | |
| 299 | if (oneway != 1) { |
| 300 | Node to = getNextNode(toWay, i); |
| 301 | if (to != null && !isSharpAngle(from, common, to)) |
| 302 | return false; |