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

Last change on this file since 13687 was 13687, checked in by Klumbumbus, 6 years ago

fix #16186, see #15497 - fix "Incomplete pedestrian crossing tagging" validator message.

  • Property svn:eol-style set to native
File size: 12.4 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.List;
12import java.util.Locale;
13import java.util.Map;
14import java.util.Set;
15import java.util.stream.Collectors;
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.Utils;
26
27/**
28 * Test that performs semantic checks on highways.
29 * @since 5902
30 */
31public class Highways extends Test {
32
33 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701;
34 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702;
35 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703;
36 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704;
37 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705;
38 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706;
39 protected static final int SOURCE_WRONG_LINK = 2707;
40
41 protected static final String SOURCE_MAXSPEED = "source:maxspeed";
42
43 /**
44 * Classified highways in order of importance
45 */
46 // CHECKSTYLE.OFF: SingleSpaceSeparator
47 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList(
48 "motorway", "motorway_link",
49 "trunk", "trunk_link",
50 "primary", "primary_link",
51 "secondary", "secondary_link",
52 "tertiary", "tertiary_link",
53 "unclassified",
54 "residential",
55 "living_street");
56 // CHECKSTYLE.ON: SingleSpaceSeparator
57
58 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList(
59 "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30",
60 "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road"));
61
62 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries()));
63
64 private boolean leftByPedestrians;
65 private boolean leftByCyclists;
66 private boolean leftByCars;
67 private int pedestrianWays;
68 private int cyclistWays;
69 private int carsWays;
70
71 /**
72 * Constructs a new {@code Highways} test.
73 */
74 public Highways() {
75 super(tr("Highways"), tr("Performs semantic checks on highways."));
76 }
77
78 @Override
79 public void visit(Node n) {
80 if (n.isUsable()) {
81 if (!n.hasTag("crossing", "no")
82 && !(n.hasKey("crossing") && (n.hasTag(HIGHWAY, "crossing")
83 || n.hasTag(HIGHWAY, "traffic_signals")))
84 && n.isReferredByWays(2)) {
85 testMissingPedestrianCrossing(n);
86 }
87 if (n.hasKey(SOURCE_MAXSPEED)) {
88 // Check maxspeed but not context against highway for nodes
89 // as maxspeed is not set on highways here but on signs, speed cameras, etc.
90 testSourceMaxspeed(n, false);
91 }
92 }
93 }
94
95 @Override
96 public void visit(Way w) {
97 if (w.isUsable()) {
98 if (w.isClosed() && w.hasTag(HIGHWAY, CLASSIFIED_HIGHWAYS) && w.hasTag("junction", "roundabout")
99 && IN_DOWNLOADED_AREA_STRICT.test(w)) {
100 // TODO: find out how to handle splitted roundabouts (see #12841)
101 testWrongRoundabout(w);
102 }
103 if (w.hasKey(SOURCE_MAXSPEED)) {
104 // Check maxspeed, including context against highway
105 testSourceMaxspeed(w, true);
106 }
107 testHighwayLink(w);
108 }
109 }
110
111 private void testWrongRoundabout(Way w) {
112 Map<String, List<Way>> map = new HashMap<>();
113 // Count all highways (per type) connected to this roundabout, except correct links
114 // As roundabouts are closed ways, take care of not processing the first/last node twice
115 for (Node n : new HashSet<>(w.getNodes())) {
116 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) {
117 String value = h.get(HIGHWAY);
118 if (h != w && value != null) {
119 boolean link = value.endsWith("_link");
120 boolean linkOk = isHighwayLinkOkay(h);
121 if (link && !linkOk) {
122 // "Autofix" bad link value to avoid false positive in roundabout check
123 value = value.replaceAll("_link$", "");
124 }
125 if (!link || !linkOk) {
126 List<Way> list = map.get(value);
127 if (list == null) {
128 list = new ArrayList<>();
129 map.put(value, list);
130 }
131 list.add(h);
132 }
133 }
134 }
135 }
136 // The roundabout should carry the highway tag of its two biggest highways
137 for (String s : CLASSIFIED_HIGHWAYS) {
138 List<Way> list = map.get(s);
139 if (list != null && list.size() >= 2) {
140 // Except when a single road is connected, but with two oneway segments
141 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
142 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
143 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
144 // Error when the highway tags do not match
145 String value = w.get(HIGHWAY);
146 if (!value.equals(s)) {
147 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY)
148 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", value, s))
149 .primitives(w)
150 .fix(() -> new ChangePropertyCommand(w, HIGHWAY, s))
151 .build());
152 }
153 break;
154 }
155 }
156 }
157 }
158
159 /**
160 * Determines if the given link road is correct, see https://wiki.openstreetmap.org/wiki/Highway_link.
161 * @param way link road
162 * @return {@code true} if the link road is correct or if the check cannot be performed due to missing data
163 */
164 public static boolean isHighwayLinkOkay(final Way way) {
165 final String highway = way.get(HIGHWAY);
166 if (highway == null || !highway.endsWith("_link")
167 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) {
168 return true;
169 }
170
171 final Set<OsmPrimitive> referrers = new HashSet<>();
172
173 if (way.isClosed()) {
174 // for closed way we need to check all adjacent ways
175 for (Node n: way.getNodes()) {
176 referrers.addAll(n.getReferrers());
177 }
178 } else {
179 referrers.addAll(way.firstNode().getReferrers());
180 referrers.addAll(way.lastNode().getReferrers());
181 }
182
183 // Find ways of same class (exact class of class_link)
184 List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter(
185 otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", "")))
186 .collect(Collectors.toList());
187 if (sameClass.size() > 1) {
188 // It is possible to have a class_link between 2 segments of same class
189 // in roundabout designs that physically separate a specific turn from the main roundabout
190 // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error
191 for (Way w : sameClass) {
192 if (w.hasTag("junction", "roundabout")) {
193 return false;
194 }
195 }
196 }
197 // Link roads should always at least one adjacent segment of same class
198 return !sameClass.isEmpty();
199 }
200
201 private void testHighwayLink(final Way way) {
202 if (!isHighwayLinkOkay(way)) {
203 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
204 .message(tr("Highway link is not linked to adequate highway/link"))
205 .primitives(way)
206 .build());
207 }
208 }
209
210 private void testMissingPedestrianCrossing(Node n) {
211 leftByPedestrians = false;
212 leftByCyclists = false;
213 leftByCars = false;
214 pedestrianWays = 0;
215 cyclistWays = 0;
216 carsWays = 0;
217
218 for (Way w : n.getParentWays()) {
219 String highway = w.get(HIGHWAY);
220 if (highway != null) {
221 if ("footway".equals(highway) || "path".equals(highway)) {
222 handlePedestrianWay(n, w);
223 if (w.hasTag("bicycle", "yes", "designated")) {
224 handleCyclistWay(n, w);
225 }
226 } else if ("cycleway".equals(highway)) {
227 handleCyclistWay(n, w);
228 if (w.hasTag("foot", "yes", "designated")) {
229 handlePedestrianWay(n, w);
230 }
231 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
232 // Only look at classified highways for now:
233 // - service highways support is TBD (see #9141 comments)
234 // - roads should be determined first. Another warning is raised anyway
235 handleCarWay(n, w);
236 }
237 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
238 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING)
239 .message(tr("Incomplete pedestrian crossing tagging. Required tags are {0} and {1}.",
240 "highway=crossing|traffic_signals", "crossing=*"))
241 .primitives(n)
242 .build());
243 return;
244 }
245 }
246 }
247 }
248
249 private void handleCarWay(Node n, Way w) {
250 carsWays++;
251 if (!w.isFirstLastNode(n) || carsWays > 1) {
252 leftByCars = true;
253 }
254 }
255
256 private void handleCyclistWay(Node n, Way w) {
257 cyclistWays++;
258 if (!w.isFirstLastNode(n) || cyclistWays > 1) {
259 leftByCyclists = true;
260 }
261 }
262
263 private void handlePedestrianWay(Node n, Way w) {
264 pedestrianWays++;
265 if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
266 leftByPedestrians = true;
267 }
268 }
269
270 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
271 String value = p.get(SOURCE_MAXSPEED);
272 if (value.matches("[A-Z]{2}:.+")) {
273 int index = value.indexOf(':');
274 // Check country
275 String country = value.substring(0, index);
276 if (!ISO_COUNTRIES.contains(country)) {
277 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE)
278 .message(tr("Unknown country code: {0}", country))
279 .primitives(p);
280 if ("UK".equals(country)) {
281 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build());
282 } else {
283 errors.add(error.build());
284 }
285 }
286 // Check context
287 String context = value.substring(index+1);
288 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
289 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT)
290 .message(tr("Unknown source:maxspeed context: {0}", context))
291 .primitives(p)
292 .build());
293 }
294 if (testContextHighway) {
295 // TODO: Check coherence of context against maxspeed
296 // TODO: Check coherence of context against highway
297 }
298 }
299 }
300}
Note: See TracBrowser for help on using the repository browser.