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

Last change on this file since 10409 was 10409, checked in by Don-vip, 8 years ago
  • remove duplicated code
  • fix various sonar warnings
  • add some javadoc and unit tests
  • Property svn:eol-style set to native
File size: 11.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.HashMap;
9import java.util.HashSet;
10import java.util.Iterator;
11import java.util.List;
12import java.util.Locale;
13import java.util.Map;
14import java.util.Set;
15
16import org.openstreetmap.josm.command.ChangePropertyCommand;
17import org.openstreetmap.josm.command.Command;
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.FixableTestError;
23import org.openstreetmap.josm.data.validation.Severity;
24import org.openstreetmap.josm.data.validation.Test;
25import org.openstreetmap.josm.data.validation.TestError;
26import org.openstreetmap.josm.tools.Predicate;
27import org.openstreetmap.josm.tools.Utils;
28
29/**
30 * Test that performs semantic checks on highways.
31 * @since 5902
32 */
33public class Highways extends Test {
34
35 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701;
36 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702;
37 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703;
38 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704;
39 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705;
40 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706;
41 protected static final int SOURCE_WRONG_LINK = 2707;
42
43 protected static final String SOURCE_MAXSPEED = "source:maxspeed";
44
45 /**
46 * Classified highways in order of importance
47 */
48 // CHECKSTYLE.OFF: SingleSpaceSeparator
49 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList(
50 "motorway", "motorway_link",
51 "trunk", "trunk_link",
52 "primary", "primary_link",
53 "secondary", "secondary_link",
54 "tertiary", "tertiary_link",
55 "unclassified",
56 "residential",
57 "living_street");
58 // CHECKSTYLE.ON: SingleSpaceSeparator
59
60 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList(
61 "urban", "rural", "zone", "zone30", "zone:30", "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 protected static class WrongRoundaboutHighway extends TestError {
80
81 public final String correctValue;
82
83 public WrongRoundaboutHighway(Highways tester, Way w, String key) {
84 super(tester, Severity.WARNING,
85 tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key),
86 WRONG_ROUNDABOUT_HIGHWAY, w);
87 this.correctValue = key;
88 }
89 }
90
91 @Override
92 public void visit(Node n) {
93 if (n.isUsable()) {
94 if (!n.hasTag("crossing", "no")
95 && !(n.hasKey("crossing") && (n.hasTag("highway", "crossing") || n.hasTag("highway", "traffic_signals")))
96 && n.isReferredByWays(2)) {
97 testMissingPedestrianCrossing(n);
98 }
99 if (n.hasKey(SOURCE_MAXSPEED)) {
100 // Check maxspeed but not context against highway for nodes
101 // as maxspeed is not set on highways here but on signs, speed cameras, etc.
102 testSourceMaxspeed(n, false);
103 }
104 }
105 }
106
107 @Override
108 public void visit(Way w) {
109 if (w.isUsable()) {
110 if (w.isClosed() && w.hasKey("highway") && CLASSIFIED_HIGHWAYS.contains(w.get("highway"))
111 && w.hasKey("junction") && "roundabout".equals(w.get("junction"))) {
112 // TODO: find out how to handle splitted roundabouts (see #12841)
113 testWrongRoundabout(w);
114 }
115 if (w.hasKey(SOURCE_MAXSPEED)) {
116 // Check maxspeed, including context against highway
117 testSourceMaxspeed(w, true);
118 }
119 testHighwayLink(w);
120 }
121 }
122
123 private void testWrongRoundabout(Way w) {
124 Map<String, List<Way>> map = new HashMap<>();
125 // Count all highways (per type) connected to this roundabout, except links
126 // As roundabouts are closed ways, take care of not processing the first/last node twice
127 for (Node n : new HashSet<>(w.getNodes())) {
128 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) {
129 String value = h.get("highway");
130 if (h != w && value != null && !value.endsWith("_link")) {
131 List<Way> list = map.get(value);
132 if (list == null) {
133 list = new ArrayList<>();
134 map.put(value, list);
135 }
136 list.add(h);
137 }
138 }
139 }
140 // The roundabout should carry the highway tag of its two biggest highways
141 for (String s : CLASSIFIED_HIGHWAYS) {
142 List<Way> list = map.get(s);
143 if (list != null && list.size() >= 2) {
144 // Except when a single road is connected, but with two oneway segments
145 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
146 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
147 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
148 // Error when the highway tags do not match
149 if (!w.get("highway").equals(s)) {
150 errors.add(new WrongRoundaboutHighway(this, w, s));
151 }
152 break;
153 }
154 }
155 }
156 }
157
158 public static boolean isHighwayLinkOkay(final Way way) {
159 final String highway = way.get("highway");
160 if (highway == null || !highway.endsWith("_link")
161 || !IN_DOWNLOADED_AREA.evaluate(way.getNode(0)) || !IN_DOWNLOADED_AREA.evaluate(way.getNode(way.getNodesCount()-1))) {
162 return true;
163 }
164
165 final Set<OsmPrimitive> referrers = new HashSet<>();
166
167 if (way.isClosed()) {
168 // for closed way we need to check all adjacent ways
169 for (Node n: way.getNodes()) {
170 referrers.addAll(n.getReferrers());
171 }
172 } else {
173 referrers.addAll(way.firstNode().getReferrers());
174 referrers.addAll(way.lastNode().getReferrers());
175 }
176
177 return Utils.exists(Utils.filteredCollection(referrers, Way.class), new Predicate<Way>() {
178 @Override
179 public boolean evaluate(final Way otherWay) {
180 return !way.equals(otherWay) && otherWay.hasTag("highway", highway, highway.replaceAll("_link$", ""));
181 }
182 });
183 }
184
185 private void testHighwayLink(final Way way) {
186 if (!isHighwayLinkOkay(way)) {
187 errors.add(new TestError(this, Severity.WARNING,
188 tr("Highway link is not linked to adequate highway/link"), SOURCE_WRONG_LINK, way));
189 }
190 }
191
192 private void testMissingPedestrianCrossing(Node n) {
193 leftByPedestrians = false;
194 leftByCyclists = false;
195 leftByCars = false;
196 pedestrianWays = 0;
197 cyclistWays = 0;
198 carsWays = 0;
199
200 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
201 String highway = w.get("highway");
202 if (highway != null) {
203 if ("footway".equals(highway) || "path".equals(highway)) {
204 handlePedestrianWay(n, w);
205 if (w.hasTag("bicycle", "yes", "designated")) {
206 handleCyclistWay(n, w);
207 }
208 } else if ("cycleway".equals(highway)) {
209 handleCyclistWay(n, w);
210 if (w.hasTag("foot", "yes", "designated")) {
211 handlePedestrianWay(n, w);
212 }
213 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
214 // Only look at classified highways for now:
215 // - service highways support is TBD (see #9141 comments)
216 // - roads should be determined first. Another warning is raised anyway
217 handleCarWay(n, w);
218 }
219 if ((leftByPedestrians || leftByCyclists) && leftByCars) {
220 errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"),
221 MISSING_PEDESTRIAN_CROSSING, n));
222 return;
223 }
224 }
225 }
226 }
227
228 private void handleCarWay(Node n, Way w) {
229 carsWays++;
230 if (!w.isFirstLastNode(n) || carsWays > 1) {
231 leftByCars = true;
232 }
233 }
234
235 private void handleCyclistWay(Node n, Way w) {
236 cyclistWays++;
237 if (!w.isFirstLastNode(n) || cyclistWays > 1) {
238 leftByCyclists = true;
239 }
240 }
241
242 private void handlePedestrianWay(Node n, Way w) {
243 pedestrianWays++;
244 if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
245 leftByPedestrians = true;
246 }
247 }
248
249 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
250 String value = p.get(SOURCE_MAXSPEED);
251 if (value.matches("[A-Z]{2}:.+")) {
252 int index = value.indexOf(':');
253 // Check country
254 String country = value.substring(0, index);
255 if (!ISO_COUNTRIES.contains(country)) {
256 if ("UK".equals(country)) {
257 errors.add(new FixableTestError(this, Severity.WARNING,
258 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p,
259 new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))));
260 } else {
261 errors.add(new TestError(this, Severity.WARNING,
262 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p));
263 }
264 }
265 // Check context
266 String context = value.substring(index+1);
267 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
268 errors.add(new TestError(this, Severity.WARNING,
269 tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p));
270 }
271 // TODO: Check coherence of context against maxspeed
272 // TODO: Check coherence of context against highway
273 }
274 }
275
276 @Override
277 public boolean isFixable(TestError testError) {
278 return testError instanceof WrongRoundaboutHighway;
279 }
280
281 @Override
282 public Command fixError(TestError testError) {
283 if (testError instanceof WrongRoundaboutHighway) {
284 // primitives list can be empty if all primitives have been purged
285 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
286 if (it.hasNext()) {
287 return new ChangePropertyCommand(it.next(),
288 "highway", ((WrongRoundaboutHighway) testError).correctValue);
289 }
290 }
291 return null;
292 }
293}
Note: See TracBrowser for help on using the repository browser.