001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Set; 015import java.util.stream.Collectors; 016 017import org.openstreetmap.josm.command.ChangePropertyCommand; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.OsmUtils; 021import org.openstreetmap.josm.data.osm.Way; 022import org.openstreetmap.josm.data.validation.Severity; 023import org.openstreetmap.josm.data.validation.Test; 024import org.openstreetmap.josm.data.validation.TestError; 025import org.openstreetmap.josm.tools.Logging; 026import org.openstreetmap.josm.tools.Utils; 027 028/** 029 * Test that performs semantic checks on highways. 030 * @since 5902 031 */ 032public class Highways extends Test { 033 034 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701; 035 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702; 036 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703; 037 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704; 038 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705; 039 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706; 040 protected static final int SOURCE_WRONG_LINK = 2707; 041 042 protected static final String SOURCE_MAXSPEED = "source:maxspeed"; 043 044 /** 045 * Classified highways in order of importance 046 */ 047 // CHECKSTYLE.OFF: SingleSpaceSeparator 048 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 049 "motorway", "motorway_link", 050 "trunk", "trunk_link", 051 "primary", "primary_link", 052 "secondary", "secondary_link", 053 "tertiary", "tertiary_link", 054 "unclassified", 055 "residential", 056 "living_street"); 057 // CHECKSTYLE.ON: SingleSpaceSeparator 058 059 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( 060 "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30", "zone40", 061 "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road")); 062 063 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries())); 064 065 private boolean leftByPedestrians; 066 private boolean leftByCyclists; 067 private boolean leftByCars; 068 private int pedestrianWays; 069 private int cyclistWays; 070 private int carsWays; 071 072 /** 073 * Constructs a new {@code Highways} test. 074 */ 075 public Highways() { 076 super(tr("Highways"), tr("Performs semantic checks on highways.")); 077 } 078 079 @Override 080 public void visit(Node n) { 081 if (n.isUsable()) { 082 if (!n.hasTag("crossing", "no") 083 && !(n.hasKey("crossing") && (n.hasTag(HIGHWAY, "crossing") 084 || n.hasTag(HIGHWAY, "traffic_signals"))) 085 && n.isReferredByWays(2)) { 086 testMissingPedestrianCrossing(n); 087 } 088 if (n.hasKey(SOURCE_MAXSPEED)) { 089 // Check maxspeed but not context against highway for nodes 090 // as maxspeed is not set on highways here but on signs, speed cameras, etc. 091 testSourceMaxspeed(n, false); 092 } 093 } 094 } 095 096 @Override 097 public void visit(Way w) { 098 if (w.isUsable()) { 099 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}