source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/Highways.java@ 14777

Last change on this file since 14777 was 14777, checked in by GerdP, 5 years ago

fix #17313 NPE when *_link road is connected to way without a highway tag (regression from r14772)

  • Property svn:eol-style set to native
File size: 16.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.HashMap;
10import java.util.HashSet;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Locale;
14import java.util.Map;
15import java.util.Set;
16
17import org.openstreetmap.josm.command.ChangePropertyCommand;
18import org.openstreetmap.josm.data.osm.Node;
19import org.openstreetmap.josm.data.osm.OsmPrimitive;
20import org.openstreetmap.josm.data.osm.OsmUtils;
21import org.openstreetmap.josm.data.osm.Way;
22import org.openstreetmap.josm.data.validation.Severity;
23import org.openstreetmap.josm.data.validation.Test;
24import org.openstreetmap.josm.data.validation.TestError;
25import org.openstreetmap.josm.tools.Geometry;
26import org.openstreetmap.josm.tools.Logging;
27import org.openstreetmap.josm.tools.Utils;
28
29/**
30 * Test that performs semantic checks on highways.
31 * @since 5902
32 */
33public class Highways extends Test {
34
35 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701;
36 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702;
37 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703;
38 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704;
39 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705;
40 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706;
41 protected static final int SOURCE_WRONG_LINK = 2707;
42
43 protected static final String SOURCE_MAXSPEED = "source:maxspeed";
44
45 /** threshold value for angles between two highway segments. */
46 private static final int MIN_ANGLE_NOT_SHARP = 60;
47
48 // CHECKSTYLE.OFF: SingleSpaceSeparator
49 private static final Set<String> LINK_TO_HIGHWAYS = new HashSet<>(Arrays.asList(
50 "motorway", "motorway_link",
51 "trunk", "trunk_link",
52 "primary", "primary_link",
53 "secondary", "secondary_link",
54 "tertiary", "tertiary_link"
55 ));
56
57 /**
58 * Classified highways in order of importance
59 */
60 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList(
61 "motorway", "motorway_link",
62 "trunk", "trunk_link",
63 "primary", "primary_link",
64 "secondary", "secondary_link",
65 "tertiary", "tertiary_link",
66 "unclassified",
67 "residential",
68 "living_street");
69 // CHECKSTYLE.ON: SingleSpaceSeparator
70
71
72 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList(
73 "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30", "zone40",
74 "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road"));
75
76 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries()));
77
78 private boolean leftByPedestrians;
79 private boolean leftByCyclists;
80 private boolean leftByCars;
81 private int pedestrianWays;
82 private int cyclistWays;
83 private int carsWays;
84
85 /**
86 * Constructs a new {@code Highways} test.
87 */
88 public Highways() {
89 super(tr("Highways"), tr("Performs semantic checks on highways."));
90 }
91
92 @Override
93 public void visit(Node n) {
94 if (n.isUsable()) {
95 if (!n.hasTag("crossing", "no")
96 && !(n.hasKey("crossing") && (n.hasTag(HIGHWAY, "crossing")
97 || n.hasTag(HIGHWAY, "traffic_signals")))
98 && n.isReferredByWays(2)) {
99 testMissingPedestrianCrossing(n);
100 }
101 if (n.hasKey(SOURCE_MAXSPEED)) {
102 // Check maxspeed but not context against highway for nodes
103 // as maxspeed is not set on highways here but on signs, speed cameras, etc.
104 testSourceMaxspeed(n, false);
105 }
106 }
107 }
108
109 @Override
110 public void visit(Way w) {
111 if (w.isUsable()) {
112 if (w.isClosed() && w.hasTag(HIGHWAY, CLASSIFIED_HIGHWAYS) && w.hasTag("junction", "roundabout")
113 && IN_DOWNLOADED_AREA_STRICT.test(w)) {
114 // TODO: find out how to handle split roundabouts (see #12841)
115 testWrongRoundabout(w);
116 }
117 if (w.hasKey(SOURCE_MAXSPEED)) {
118 // Check maxspeed, including context against highway
119 testSourceMaxspeed(w, true);
120 }
121 testHighwayLink(w);
122 }
123 }
124
125 private void testWrongRoundabout(Way w) {
126 Map<String, List<Way>> map = new HashMap<>();
127 // Count all highways (per type) connected to this roundabout, except correct links
128 // As roundabouts are closed ways, take care of not processing the first/last node twice
129 for (Node n : new HashSet<>(w.getNodes())) {
130 for (Way h : (Iterable<Way>) n.referrers(Way.class)::iterator) {
131 String value = h.get(HIGHWAY);
132 if (h != w && value != null) {
133 boolean link = value.endsWith("_link");
134 boolean linkOk = isHighwayLinkOkay(h);
135 if (link && !linkOk) {
136 // "Autofix" bad link value to avoid false positive in roundabout check
137 value = value.replaceAll("_link$", "");
138 }
139 if (!link || !linkOk) {
140 List<Way> list = map.get(value);
141 if (list == null) {
142 list = new ArrayList<>();
143 map.put(value, list);
144 }
145 list.add(h);
146 }
147 }
148 }
149 }
150 // The roundabout should carry the highway tag of its two biggest highways
151 for (String s : CLASSIFIED_HIGHWAYS) {
152 List<Way> list = map.get(s);
153 if (list != null && list.size() >= 2) {
154 // Except when a single road is connected, but with two oneway segments
155 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
156 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
157 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
158 // Error when the highway tags do not match
159 String value = w.get(HIGHWAY);
160 if (!value.equals(s)) {
161 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY)
162 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", value, s))
163 .primitives(w)
164 .fix(() -> new ChangePropertyCommand(w, HIGHWAY, s))
165 .build());
166 }
167 break;
168 }
169 }
170 }
171 }
172
173 /**
174 * Determines if the given link road is correct, see https://wiki.openstreetmap.org/wiki/Highway_link.
175 * @param way link road
176 * @return {@code true} if the link road is correct or if the check cannot be performed due to missing data
177 */
178 public static boolean isHighwayLinkOkay(final Way way) {
179 final String highway = way.get(HIGHWAY);
180 if (highway == null || !highway.endsWith("_link")) {
181 return true;
182 }
183
184 // check if connected to a high class road where the link must match the higher class
185 String highClass = null;
186 for (int i = 0; i < way.getNodesCount(); i++) {
187 Node n = way.getNode(i);
188 if (!IN_DOWNLOADED_AREA.test(n))
189 return true;
190 Set<Way> otherWays = new HashSet<>();
191 otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class));
192 if (otherWays.size() == 1)
193 continue;
194 Iterator<Way> iter = otherWays.iterator();
195 while (iter.hasNext()) {
196 Way w = iter.next();
197 final String hw2 = w.get(HIGHWAY);
198 if (way == w || w.getNodesCount() < 2 || !w.isUsable() || hw2 == null)
199 iter.remove();
200 else {
201 if ("motorway".equals(hw2)) {
202 highClass = "motorway";
203 break;
204 } else if ("trunk".equals(hw2))
205 highClass = "trunk";
206 }
207 }
208 }
209
210 if (highClass != null && !highway.equals(highClass + "_link")) {
211 return false;
212 }
213
214 for (int i = 0; i < way.getNodesCount(); i++) {
215 Node n = way.getNode(i);
216 Set<Way> otherWays = new HashSet<>();
217 otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class));
218 if (otherWays.size() == 1)
219 continue;
220 otherWays.removeIf(w -> w == way || !w.hasTag("highway") || !highway.startsWith(w.get(HIGHWAY)) || !LINK_TO_HIGHWAYS.contains(w.get(HIGHWAY)));
221 if (otherWays.isEmpty())
222 continue;
223
224 //TODO: ignore ways which are not allowed because of turn restrictions, oneway attributes or access rules?
225 HashSet<Way> sameTag = new HashSet<>();
226 for (Way ow : otherWays) {
227 if (highway.equals(ow.get(HIGHWAY)))
228 sameTag.add(ow);
229 else
230 return true;
231 }
232 // we have way(s) with the same _link tag, ignore those with a sharp angle
233 final int pos = i;
234 sameTag.removeIf(w -> isSharpAngle(way, pos, w));
235 if (!sameTag.isEmpty())
236 return true;
237 }
238 return false;
239
240 }
241
242 /**
243 * Check if the two given connected ways form a sharp angle.
244 * @param way 1st way
245 * @param nodePos node position of connecting node in 1st way
246 * @param otherWay the 2nd way
247 * @return true if angle is sharp or way cannot be travelled because of oneway attributes
248 */
249 private static boolean isSharpAngle(Way way, int nodePos, Way otherWay) {
250 Node n = way.getNode(nodePos);
251 int oneway = way.isOneway();
252 if (oneway == 0 && "roundabout".equals(way.get("junction"))) {
253 oneway = 1;
254 }
255
256 if (oneway != 1) {
257 Node prev = getPrevNode(way, nodePos);
258 if (prev != null && !onlySharpAngle(n, prev, otherWay))
259 return false;
260 }
261 if (oneway != -1) {
262 Node next = getNextNode(way, nodePos);
263 if (next != null && !onlySharpAngle(n, next, otherWay))
264 return false;
265 }
266 return true;
267 }
268
269 private static Node getNextNode(Way way, int nodePos) {
270 if (nodePos + 1 >= way.getNodesCount()) {
271 if (way.isClosed())
272 return way.getNode(1);
273 return null;
274 } else {
275 return way.getNode(nodePos + 1);
276 }
277 }
278
279 private static Node getPrevNode(Way way, int nodePos) {
280 if (nodePos == 0) {
281 if (way.isClosed())
282 return way.getNode(way.getNodesCount() - 2);
283 return null;
284 } else {
285 return way.getNode(nodePos - 1);
286 }
287 }
288
289 private static boolean onlySharpAngle(Node common, Node from, Way toWay) {
290 int oneway = toWay.isOneway();
291 if (oneway == 0 && "roundabout".equals(toWay.get("junction"))) {
292 oneway = 1;
293 }
294
295 for (int i = 0; i < toWay.getNodesCount(); i++) {
296 if (common == toWay.getNode(i)) {
297
298 if (oneway != 1) {
299 Node to = getNextNode(toWay, i);
300 if (to != null && !isSharpAngle(from, common, to))
301 return false;
302 }
303 if (oneway != -1) {
304 Node to = getPrevNode(toWay, i);
305 if (to != null && !isSharpAngle(from, common, to))
306 return false;
307 }
308 break;
309 }
310 }
311 return true;
312 }
313
314 /**
315 * Returns true if angle of a corner defined with 3 point coordinates is &lt; MIN_ANGLE_NOT_SHARP
316 *
317 * @param n1 first node
318 * @param n2 Common node
319 * @param n3 third node
320 * @return true if angle is below value given in MIN_ANGLE_NOT_SHARP
321 */
322
323 private static boolean isSharpAngle(Node n1, Node n2, Node n3) {
324 double angle = Geometry.getNormalizedAngleInDegrees(
325 Geometry.getCornerAngle(n1.getEastNorth(), n2.getEastNorth(), n3.getEastNorth()));
326 return angle < MIN_ANGLE_NOT_SHARP;
327 }
328
329 private void testHighwayLink(final Way way) {
330 if (!isHighwayLinkOkay(way)) {
331 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
332 .message(tr("Highway link is not linked to adequate highway/link"))
333 .primitives(way)
334 .build());
335 }
336 }
337
338 private void testMissingPedestrianCrossing(Node n) {
339 leftByPedestrians = false;
340 leftByCyclists = false;
341 leftByCars = false;
342 pedestrianWays = 0;
343 cyclistWays = 0;
344 carsWays = 0;
345
346 for (Way w : n.getParentWays()) {
347 String highway = w.get(HIGHWAY);
348 if (highway != null) {
349 if ("footway".equals(highway) || "path".equals(highway)) {
350 handlePedestrianWay(n, w);
351 if (w.hasTag("bicycle", "yes", "designated")) {
352 handleCyclistWay(n, w);
353 }
354 } else if ("cycleway".equals(highway)) {
355 handleCyclistWay(n, w);
356 if (w.hasTag("foot", "yes", "designated")) {
357 handlePedestrianWay(n, w);
358 }
359 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
360 // Only look at classified highways for now:
361 // - service highways support is TBD (see #9141 comments)
362 // - roads should be determined first. Another warning is raised anyway
363 handleCarWay(n, w);
364 }
365 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
366 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING)
367 .message(tr("Incomplete pedestrian crossing tagging. Required tags are {0} and {1}.",
368 "highway=crossing|traffic_signals", "crossing=*"))
369 .primitives(n)
370 .build());
371 return;
372 }
373 }
374 }
375 }
376
377 private void handleCarWay(Node n, Way w) {
378 carsWays++;
379 if (!w.isFirstLastNode(n) || carsWays > 1) {
380 leftByCars = true;
381 }
382 }
383
384 private void handleCyclistWay(Node n, Way w) {
385 cyclistWays++;
386 if (!w.isFirstLastNode(n) || cyclistWays > 1) {
387 leftByCyclists = true;
388 }
389 }
390
391 private void handlePedestrianWay(Node n, Way w) {
392 pedestrianWays++;
393 if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
394 leftByPedestrians = true;
395 }
396 }
397
398 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
399 String value = p.get(SOURCE_MAXSPEED);
400 if (value.matches("[A-Z]{2}:.+")) {
401 int index = value.indexOf(':');
402 // Check country
403 String country = value.substring(0, index);
404 if (!ISO_COUNTRIES.contains(country)) {
405 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE)
406 .message(tr("Unknown country code: {0}", country))
407 .primitives(p);
408 if ("UK".equals(country)) {
409 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build());
410 } else {
411 errors.add(error.build());
412 }
413 }
414 // Check context
415 String context = value.substring(index+1);
416 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
417 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT)
418 .message(tr("Unknown source:maxspeed context: {0}", context))
419 .primitives(p)
420 .build());
421 }
422 if (testContextHighway) {
423 // TODO: Check coherence of context against maxspeed
424 // TODO: Check coherence of context against highway
425 Logging.trace("TODO: test context highway - https://josm.openstreetmap.de/ticket/9400");
426 }
427 }
428 }
429}
Note: See TracBrowser for help on using the repository browser.