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

Last change on this file since 12312 was 12312, checked in by Don-vip, 7 years ago

fix #14891 - improve roundabout/links validator test

  • Property svn:eol-style set to native
File size: 12.2 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", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road"));
60
61 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries()));
62
63 private boolean leftByPedestrians;
64 private boolean leftByCyclists;
65 private boolean leftByCars;
66 private int pedestrianWays;
67 private int cyclistWays;
68 private int carsWays;
69
70 /**
71 * Constructs a new {@code Highways} test.
72 */
73 public Highways() {
74 super(tr("Highways"), tr("Performs semantic checks on highways."));
75 }
76
77 @Override
78 public void visit(Node n) {
79 if (n.isUsable()) {
80 if (!n.hasTag("crossing", "no")
81 && !(n.hasKey("crossing") && (n.hasTag(HIGHWAY, "crossing")
82 || n.hasTag(HIGHWAY, "traffic_signals")))
83 && n.isReferredByWays(2)) {
84 testMissingPedestrianCrossing(n);
85 }
86 if (n.hasKey(SOURCE_MAXSPEED)) {
87 // Check maxspeed but not context against highway for nodes
88 // as maxspeed is not set on highways here but on signs, speed cameras, etc.
89 testSourceMaxspeed(n, false);
90 }
91 }
92 }
93
94 @Override
95 public void visit(Way w) {
96 if (w.isUsable()) {
97 if (w.isClosed() && w.hasTag(HIGHWAY, CLASSIFIED_HIGHWAYS) && w.hasTag("junction", "roundabout")) {
98 // TODO: find out how to handle splitted roundabouts (see #12841)
99 testWrongRoundabout(w);
100 }
101 if (w.hasKey(SOURCE_MAXSPEED)) {
102 // Check maxspeed, including context against highway
103 testSourceMaxspeed(w, true);
104 }
105 testHighwayLink(w);
106 }
107 }
108
109 private void testWrongRoundabout(Way w) {
110 Map<String, List<Way>> map = new HashMap<>();
111 // Count all highways (per type) connected to this roundabout, except correct links
112 // As roundabouts are closed ways, take care of not processing the first/last node twice
113 for (Node n : new HashSet<>(w.getNodes())) {
114 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) {
115 String value = h.get(HIGHWAY);
116 if (h != w && value != null) {
117 boolean link = value.endsWith("_link");
118 boolean linkOk = isHighwayLinkOkay(h);
119 if (link && !linkOk) {
120 // "Autofix" bad link value to avoid false positive in roundabout check
121 value = value.replaceAll("_link$", "");
122 }
123 if (!link || !linkOk) {
124 List<Way> list = map.get(value);
125 if (list == null) {
126 list = new ArrayList<>();
127 map.put(value, list);
128 }
129 list.add(h);
130 }
131 }
132 }
133 }
134 // The roundabout should carry the highway tag of its two biggest highways
135 for (String s : CLASSIFIED_HIGHWAYS) {
136 List<Way> list = map.get(s);
137 if (list != null && list.size() >= 2) {
138 // Except when a single road is connected, but with two oneway segments
139 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
140 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
141 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
142 // Error when the highway tags do not match
143 String value = w.get(HIGHWAY);
144 if (!value.equals(s)) {
145 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY)
146 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", value, s))
147 .primitives(w)
148 .fix(() -> new ChangePropertyCommand(w, HIGHWAY, s))
149 .build());
150 }
151 break;
152 }
153 }
154 }
155 }
156
157 /**
158 * Determines if the given link road is correct, see https://wiki.openstreetmap.org/wiki/Highway_link.
159 * @param way link road
160 * @return {@code true} if the link road is correct or if the check cannot be performed due to missing data
161 */
162 public static boolean isHighwayLinkOkay(final Way way) {
163 final String highway = way.get(HIGHWAY);
164 if (highway == null || !highway.endsWith("_link")
165 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) {
166 return true;
167 }
168
169 final Set<OsmPrimitive> referrers = new HashSet<>();
170
171 if (way.isClosed()) {
172 // for closed way we need to check all adjacent ways
173 for (Node n: way.getNodes()) {
174 referrers.addAll(n.getReferrers());
175 }
176 } else {
177 referrers.addAll(way.firstNode().getReferrers());
178 referrers.addAll(way.lastNode().getReferrers());
179 }
180
181 // Find ways of same class (exact class of class_link)
182 List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter(
183 otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", "")))
184 .collect(Collectors.toList());
185 if (sameClass.size() > 1) {
186 // It is possible to have a class_link between 2 segments of same class
187 // in roundabout designs that physically separate a specific turn from the main roundabout
188 // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error
189 for (Way w : sameClass) {
190 if (w.hasTag("junction", "roundabout")) {
191 return false;
192 }
193 }
194 }
195 // Link roads should always at least one adjacent segment of same class
196 return !sameClass.isEmpty();
197 }
198
199 private void testHighwayLink(final Way way) {
200 if (!isHighwayLinkOkay(way)) {
201 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
202 .message(tr("Highway link is not linked to adequate highway/link"))
203 .primitives(way)
204 .build());
205 }
206 }
207
208 private void testMissingPedestrianCrossing(Node n) {
209 leftByPedestrians = false;
210 leftByCyclists = false;
211 leftByCars = false;
212 pedestrianWays = 0;
213 cyclistWays = 0;
214 carsWays = 0;
215
216 for (Way w : n.getParentWays()) {
217 String highway = w.get(HIGHWAY);
218 if (highway != null) {
219 if ("footway".equals(highway) || "path".equals(highway)) {
220 handlePedestrianWay(n, w);
221 if (w.hasTag("bicycle", "yes", "designated")) {
222 handleCyclistWay(n, w);
223 }
224 } else if ("cycleway".equals(highway)) {
225 handleCyclistWay(n, w);
226 if (w.hasTag("foot", "yes", "designated")) {
227 handlePedestrianWay(n, w);
228 }
229 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
230 // Only look at classified highways for now:
231 // - service highways support is TBD (see #9141 comments)
232 // - roads should be determined first. Another warning is raised anyway
233 handleCarWay(n, w);
234 }
235 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
236 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING)
237 .message(tr("Missing pedestrian crossing information"))
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 }
295 }
296 }
297}
Note: See TracBrowser for help on using the repository browser.