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

Last change on this file since 18801 was 18801, checked in by taylor.smock, 10 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

  • Simplify and cleanup code (example: Arrays.asList(item) -> Collections.singletonList(item))
  • Fix typos in documentation (which also corrects the documentation to match what actually happens, in some cases)
  • Property svn:eol-style set to native
File size: 12.6 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 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 split 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 if (n.hasTag("highway", "crossing") && !n.hasKey("crossing"))
216 return; // see #20905 handled by mapcss test
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 Logging.trace("TODO: test context highway - https://josm.openstreetmap.de/ticket/9400");
298 }
299 }
300 }
301}
Note: See TracBrowser for help on using the repository browser.