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

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

fix #15256 - limit roundabout validation test to waynodes all in downloaded area

  • Property svn:eol-style set to native
File size: 12.3 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 && IN_DOWNLOADED_AREA_STRICT.test(w)) {
99 // TODO: find out how to handle splitted roundabouts (see #12841)
100 testWrongRoundabout(w);
101 }
102 if (w.hasKey(SOURCE_MAXSPEED)) {
103 // Check maxspeed, including context against highway
104 testSourceMaxspeed(w, true);
105 }
106 testHighwayLink(w);
107 }
108 }
109
110 private void testWrongRoundabout(Way w) {
111 Map<String, List<Way>> map = new HashMap<>();
112 // Count all highways (per type) connected to this roundabout, except correct links
113 // As roundabouts are closed ways, take care of not processing the first/last node twice
114 for (Node n : new HashSet<>(w.getNodes())) {
115 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) {
116 String value = h.get(HIGHWAY);
117 if (h != w && value != null) {
118 boolean link = value.endsWith("_link");
119 boolean linkOk = isHighwayLinkOkay(h);
120 if (link && !linkOk) {
121 // "Autofix" bad link value to avoid false positive in roundabout check
122 value = value.replaceAll("_link$", "");
123 }
124 if (!link || !linkOk) {
125 List<Way> list = map.get(value);
126 if (list == null) {
127 list = new ArrayList<>();
128 map.put(value, list);
129 }
130 list.add(h);
131 }
132 }
133 }
134 }
135 // The roundabout should carry the highway tag of its two biggest highways
136 for (String s : CLASSIFIED_HIGHWAYS) {
137 List<Way> list = map.get(s);
138 if (list != null && list.size() >= 2) {
139 // Except when a single road is connected, but with two oneway segments
140 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
141 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
142 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
143 // Error when the highway tags do not match
144 String value = w.get(HIGHWAY);
145 if (!value.equals(s)) {
146 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY)
147 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", value, s))
148 .primitives(w)
149 .fix(() -> new ChangePropertyCommand(w, HIGHWAY, s))
150 .build());
151 }
152 break;
153 }
154 }
155 }
156 }
157
158 /**
159 * Determines if the given link road is correct, see https://wiki.openstreetmap.org/wiki/Highway_link.
160 * @param way link road
161 * @return {@code true} if the link road is correct or if the check cannot be performed due to missing data
162 */
163 public static boolean isHighwayLinkOkay(final Way way) {
164 final String highway = way.get(HIGHWAY);
165 if (highway == null || !highway.endsWith("_link")
166 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) {
167 return true;
168 }
169
170 final Set<OsmPrimitive> referrers = new HashSet<>();
171
172 if (way.isClosed()) {
173 // for closed way we need to check all adjacent ways
174 for (Node n: way.getNodes()) {
175 referrers.addAll(n.getReferrers());
176 }
177 } else {
178 referrers.addAll(way.firstNode().getReferrers());
179 referrers.addAll(way.lastNode().getReferrers());
180 }
181
182 // Find ways of same class (exact class of class_link)
183 List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter(
184 otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", "")))
185 .collect(Collectors.toList());
186 if (sameClass.size() > 1) {
187 // It is possible to have a class_link between 2 segments of same class
188 // in roundabout designs that physically separate a specific turn from the main roundabout
189 // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error
190 for (Way w : sameClass) {
191 if (w.hasTag("junction", "roundabout")) {
192 return false;
193 }
194 }
195 }
196 // Link roads should always at least one adjacent segment of same class
197 return !sameClass.isEmpty();
198 }
199
200 private void testHighwayLink(final Way way) {
201 if (!isHighwayLinkOkay(way)) {
202 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
203 .message(tr("Highway link is not linked to adequate highway/link"))
204 .primitives(way)
205 .build());
206 }
207 }
208
209 private void testMissingPedestrianCrossing(Node n) {
210 leftByPedestrians = false;
211 leftByCyclists = false;
212 leftByCars = false;
213 pedestrianWays = 0;
214 cyclistWays = 0;
215 carsWays = 0;
216
217 for (Way w : n.getParentWays()) {
218 String highway = w.get(HIGHWAY);
219 if (highway != null) {
220 if ("footway".equals(highway) || "path".equals(highway)) {
221 handlePedestrianWay(n, w);
222 if (w.hasTag("bicycle", "yes", "designated")) {
223 handleCyclistWay(n, w);
224 }
225 } else if ("cycleway".equals(highway)) {
226 handleCyclistWay(n, w);
227 if (w.hasTag("foot", "yes", "designated")) {
228 handlePedestrianWay(n, w);
229 }
230 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
231 // Only look at classified highways for now:
232 // - service highways support is TBD (see #9141 comments)
233 // - roads should be determined first. Another warning is raised anyway
234 handleCarWay(n, w);
235 }
236 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
237 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING)
238 .message(tr("Missing pedestrian crossing information"))
239 .primitives(n)
240 .build());
241 return;
242 }
243 }
244 }
245 }
246
247 private void handleCarWay(Node n, Way w) {
248 carsWays++;
249 if (!w.isFirstLastNode(n) || carsWays > 1) {
250 leftByCars = true;
251 }
252 }
253
254 private void handleCyclistWay(Node n, Way w) {
255 cyclistWays++;
256 if (!w.isFirstLastNode(n) || cyclistWays > 1) {
257 leftByCyclists = true;
258 }
259 }
260
261 private void handlePedestrianWay(Node n, Way w) {
262 pedestrianWays++;
263 if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
264 leftByPedestrians = true;
265 }
266 }
267
268 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
269 String value = p.get(SOURCE_MAXSPEED);
270 if (value.matches("[A-Z]{2}:.+")) {
271 int index = value.indexOf(':');
272 // Check country
273 String country = value.substring(0, index);
274 if (!ISO_COUNTRIES.contains(country)) {
275 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE)
276 .message(tr("Unknown country code: {0}", country))
277 .primitives(p);
278 if ("UK".equals(country)) {
279 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build());
280 } else {
281 errors.add(error.build());
282 }
283 }
284 // Check context
285 String context = value.substring(index+1);
286 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
287 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT)
288 .message(tr("Unknown source:maxspeed context: {0}", context))
289 .primitives(p)
290 .build());
291 }
292 if (testContextHighway) {
293 // TODO: Check coherence of context against maxspeed
294 // TODO: Check coherence of context against highway
295 }
296 }
297 }
298}
Note: See TracBrowser for help on using the repository browser.