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

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