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

Last change on this file was 14779, checked in by GerdP, 6 weeks ago

see #16803: revert r14777,r14775, and r14772: too many side effects, patch needs more review

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