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

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

fix #16803 Validator: Wrong warning Highway link is not linked to adequate highway/link
(16803-v4.patch)

  • 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 || !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) {
253 if ("roundabout".equals(way.get("junction"))) {
254 oneway = 1;
255 }
256 }
257
258 if (oneway != 1) {
259 Node prev = getPrevNode(way, nodePos);
260 if (prev != null && !onlySharpAngle(n, prev, otherWay))
261 return false;
262 }
263 if (oneway != -1) {
264 Node next = getNextNode(way, nodePos);
265 if (next != null && !onlySharpAngle(n, next, otherWay))
266 return false;
267 }
268 return true;
269 }
270
271 private static Node getNextNode(Way way, int nodePos) {
272 if (nodePos + 1 >= way.getNodesCount()) {
273 if (way.isClosed())
274 return way.getNode(1);
275 return null;
276 } else {
277 return way.getNode(nodePos + 1);
278 }
279 }
280
281 private static Node getPrevNode(Way way, int nodePos) {
282 if (nodePos == 0) {
283 if (way.isClosed())
284 return way.getNode(way.getNodesCount() - 2);
285 return null;
286 } else {
287 return way.getNode(nodePos - 1);
288 }
289 }
290
291 private static boolean onlySharpAngle(Node common, Node from, Way toWay) {
292 int oneway = toWay.isOneway();
293 if (oneway == 0) {
294 if ("roundabout".equals(toWay.get("junction"))) {
295 oneway = 1;
296 }
297 }
298
299 for (int i = 0; i < toWay.getNodesCount(); i++) {
300 if (common == toWay.getNode(i)) {
301
302 if (oneway != 1) {
303 Node to = getNextNode(toWay, i);
304 if (to != null && !isSharpAngle(from, common, to))
305 return false;
306 }
307 if (oneway != -1) {
308 Node to = getPrevNode(toWay, i);
309 if (to != null && !isSharpAngle(from, common, to))
310 return false;
311 }
312 break;
313 }
314 }
315 return true;
316 }
317
318 /**
319 * Returns true if angle of a corner defined with 3 point coordinates is &lt; MIN_ANGLE_NOT_SHARP
320 *
321 * @param n1 first node
322 * @param n2 Common node
323 * @param n3 third node
324 * @return true if angle is below value given in MIN_ANGLE_NOT_SHARP
325 */
326
327 private static boolean isSharpAngle(Node n1, Node n2, Node n3) {
328 double angle = Geometry.getNormalizedAngleInDegrees(
329 Geometry.getCornerAngle(n1.getEastNorth(), n2.getEastNorth(), n3.getEastNorth()));
330 return angle < MIN_ANGLE_NOT_SHARP;
331 }
332
333 private void testHighwayLink(final Way way) {
334 if (!isHighwayLinkOkay(way)) {
335 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
336 .message(tr("Highway link is not linked to adequate highway/link"))
337 .primitives(way)
338 .build());
339 }
340 }
341
342 private void testMissingPedestrianCrossing(Node n) {
343 leftByPedestrians = false;
344 leftByCyclists = false;
345 leftByCars = false;
346 pedestrianWays = 0;
347 cyclistWays = 0;
348 carsWays = 0;
349
350 for (Way w : n.getParentWays()) {
351 String highway = w.get(HIGHWAY);
352 if (highway != null) {
353 if ("footway".equals(highway) || "path".equals(highway)) {
354 handlePedestrianWay(n, w);
355 if (w.hasTag("bicycle", "yes", "designated")) {
356 handleCyclistWay(n, w);
357 }
358 } else if ("cycleway".equals(highway)) {
359 handleCyclistWay(n, w);
360 if (w.hasTag("foot", "yes", "designated")) {
361 handlePedestrianWay(n, w);
362 }
363 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
364 // Only look at classified highways for now:
365 // - service highways support is TBD (see #9141 comments)
366 // - roads should be determined first. Another warning is raised anyway
367 handleCarWay(n, w);
368 }
369 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
370 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING)
371 .message(tr("Incomplete pedestrian crossing tagging. Required tags are {0} and {1}.",
372 "highway=crossing|traffic_signals", "crossing=*"))
373 .primitives(n)
374 .build());
375 return;
376 }
377 }
378 }
379 }
380
381 private void handleCarWay(Node n, Way w) {
382 carsWays++;
383 if (!w.isFirstLastNode(n) || carsWays > 1) {
384 leftByCars = true;
385 }
386 }
387
388 private void handleCyclistWay(Node n, Way w) {
389 cyclistWays++;
390 if (!w.isFirstLastNode(n) || cyclistWays > 1) {
391 leftByCyclists = true;
392 }
393 }
394
395 private void handlePedestrianWay(Node n, Way w) {
396 pedestrianWays++;
397 if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
398 leftByPedestrians = true;
399 }
400 }
401
402 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
403 String value = p.get(SOURCE_MAXSPEED);
404 if (value.matches("[A-Z]{2}:.+")) {
405 int index = value.indexOf(':');
406 // Check country
407 String country = value.substring(0, index);
408 if (!ISO_COUNTRIES.contains(country)) {
409 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE)
410 .message(tr("Unknown country code: {0}", country))
411 .primitives(p);
412 if ("UK".equals(country)) {
413 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build());
414 } else {
415 errors.add(error.build());
416 }
417 }
418 // Check context
419 String context = value.substring(index+1);
420 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
421 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT)
422 .message(tr("Unknown source:maxspeed context: {0}", context))
423 .primitives(p)
424 .build());
425 }
426 if (testContextHighway) {
427 // TODO: Check coherence of context against maxspeed
428 // TODO: Check coherence of context against highway
429 Logging.trace("TODO: test context highway - https://josm.openstreetmap.de/ticket/9400");
430 }
431 }
432 }
433}
Note: See TracBrowser for help on using the repository browser.