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.Utils; 026 027/** 028 * Test that performs semantic checks on highways. 029 * @since 5902 030 */ 031public class Highways extends Test { 032 033 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701; 034 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702; 035 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703; 036 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704; 037 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705; 038 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706; 039 protected static final int SOURCE_WRONG_LINK = 2707; 040 041 protected static final String SOURCE_MAXSPEED = "source:maxspeed"; 042 043 /** 044 * Classified highways in order of importance 045 */ 046 // CHECKSTYLE.OFF: SingleSpaceSeparator 047 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 048 "motorway", "motorway_link", 049 "trunk", "trunk_link", 050 "primary", "primary_link", 051 "secondary", "secondary_link", 052 "tertiary", "tertiary_link", 053 "unclassified", 054 "residential", 055 "living_street"); 056 // CHECKSTYLE.ON: SingleSpaceSeparator 057 058 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( 059 "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30", 060 "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road")); 061 062 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries())); 063 064 private boolean leftByPedestrians; 065 private boolean leftByCyclists; 066 private boolean leftByCars; 067 private int pedestrianWays; 068 private int cyclistWays; 069 private int carsWays; 070 071 /** 072 * Constructs a new {@code Highways} test. 073 */ 074 public Highways() { 075 super(tr("Highways"), tr("Performs semantic checks on highways.")); 076 } 077 078 @Override 079 public void visit(Node n) { 080 if (n.isUsable()) { 081 if (!n.hasTag("crossing", "no") 082 && !(n.hasKey("crossing") && (n.hasTag(HIGHWAY, "crossing") 083 || n.hasTag(HIGHWAY, "traffic_signals"))) 084 && n.isReferredByWays(2)) { 085 testMissingPedestrianCrossing(n); 086 } 087 if (n.hasKey(SOURCE_MAXSPEED)) { 088 // Check maxspeed but not context against highway for nodes 089 // as maxspeed is not set on highways here but on signs, speed cameras, etc. 090 testSourceMaxspeed(n, false); 091 } 092 } 093 } 094 095 @Override 096 public void visit(Way w) { 097 if (w.isUsable()) { 098 if (w.isClosed() && w.hasTag(HIGHWAY, CLASSIFIED_HIGHWAYS) && w.hasTag("junction", "roundabout") 099 && IN_DOWNLOADED_AREA_STRICT.test(w)) { 100 // TODO: find out how to handle splitted roundabouts (see #12841) 101 testWrongRoundabout(w); 102 } 103 if (w.hasKey(SOURCE_MAXSPEED)) { 104 // Check maxspeed, including context against highway 105 testSourceMaxspeed(w, true); 106 } 107 testHighwayLink(w); 108 } 109 } 110 111 private void testWrongRoundabout(Way w) { 112 Map<String, List<Way>> map = new HashMap<>(); 113 // Count all highways (per type) connected to this roundabout, except correct links 114 // As roundabouts are closed ways, take care of not processing the first/last node twice 115 for (Node n : new HashSet<>(w.getNodes())) { 116 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) { 117 String value = h.get(HIGHWAY); 118 if (h != w && value != null) { 119 boolean link = value.endsWith("_link"); 120 boolean linkOk = isHighwayLinkOkay(h); 121 if (link && !linkOk) { 122 // "Autofix" bad link value to avoid false positive in roundabout check 123 value = value.replaceAll("_link$", ""); 124 } 125 if (!link || !linkOk) { 126 List<Way> list = map.get(value); 127 if (list == null) { 128 list = new ArrayList<>(); 129 map.put(value, list); 130 } 131 list.add(h); 132 } 133 } 134 } 135 } 136 // The roundabout should carry the highway tag of its two biggest highways 137 for (String s : CLASSIFIED_HIGHWAYS) { 138 List<Way> list = map.get(s); 139 if (list != null && list.size() >= 2) { 140 // Except when a single road is connected, but with two oneway segments 141 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway")); 142 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway")); 143 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) { 144 // Error when the highway tags do not match 145 String value = w.get(HIGHWAY); 146 if (!value.equals(s)) { 147 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY) 148 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", value, s)) 149 .primitives(w) 150 .fix(() -> new ChangePropertyCommand(w, HIGHWAY, s)) 151 .build()); 152 } 153 break; 154 } 155 } 156 } 157 } 158 159 /** 160 * Determines if the given link road is correct, see https://wiki.openstreetmap.org/wiki/Highway_link. 161 * @param way link road 162 * @return {@code true} if the link road is correct or if the check cannot be performed due to missing data 163 */ 164 public static boolean isHighwayLinkOkay(final Way way) { 165 final String highway = way.get(HIGHWAY); 166 if (highway == null || !highway.endsWith("_link") 167 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) { 168 return true; 169 } 170 171 final Set<OsmPrimitive> referrers = new HashSet<>(); 172 173 if (way.isClosed()) { 174 // for closed way we need to check all adjacent ways 175 for (Node n: way.getNodes()) { 176 referrers.addAll(n.getReferrers()); 177 } 178 } else { 179 referrers.addAll(way.firstNode().getReferrers()); 180 referrers.addAll(way.lastNode().getReferrers()); 181 } 182 183 // Find ways of same class (exact class of class_link) 184 List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter( 185 otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", ""))) 186 .collect(Collectors.toList()); 187 if (sameClass.size() > 1) { 188 // It is possible to have a class_link between 2 segments of same class 189 // in roundabout designs that physically separate a specific turn from the main roundabout 190 // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error 191 for (Way w : sameClass) { 192 if (w.hasTag("junction", "roundabout")) { 193 return false; 194 } 195 } 196 } 197 // Link roads should always at least one adjacent segment of same class 198 return !sameClass.isEmpty(); 199 } 200 201 private void testHighwayLink(final Way way) { 202 if (!isHighwayLinkOkay(way)) { 203 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK) 204 .message(tr("Highway link is not linked to adequate highway/link")) 205 .primitives(way) 206 .build()); 207 } 208 } 209 210 private void testMissingPedestrianCrossing(Node n) { 211 leftByPedestrians = false; 212 leftByCyclists = false; 213 leftByCars = false; 214 pedestrianWays = 0; 215 cyclistWays = 0; 216 carsWays = 0; 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("Missing pedestrian crossing information. Add the tag crossing=*.")) 240 .primitives(n) 241 .build()); 242 return; 243 } 244 } 245 } 246 } 247 248 private void handleCarWay(Node n, Way w) { 249 carsWays++; 250 if (!w.isFirstLastNode(n) || carsWays > 1) { 251 leftByCars = true; 252 } 253 } 254 255 private void handleCyclistWay(Node n, Way w) { 256 cyclistWays++; 257 if (!w.isFirstLastNode(n) || cyclistWays > 1) { 258 leftByCyclists = true; 259 } 260 } 261 262 private void handlePedestrianWay(Node n, Way w) { 263 pedestrianWays++; 264 if (!w.isFirstLastNode(n) || pedestrianWays > 1) { 265 leftByPedestrians = true; 266 } 267 } 268 269 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) { 270 String value = p.get(SOURCE_MAXSPEED); 271 if (value.matches("[A-Z]{2}:.+")) { 272 int index = value.indexOf(':'); 273 // Check country 274 String country = value.substring(0, index); 275 if (!ISO_COUNTRIES.contains(country)) { 276 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE) 277 .message(tr("Unknown country code: {0}", country)) 278 .primitives(p); 279 if ("UK".equals(country)) { 280 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build()); 281 } else { 282 errors.add(error.build()); 283 } 284 } 285 // Check context 286 String context = value.substring(index+1); 287 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 288 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT) 289 .message(tr("Unknown source:maxspeed context: {0}", context)) 290 .primitives(p) 291 .build()); 292 } 293 if (testContextHighway) { 294 // TODO: Check coherence of context against maxspeed 295 // TODO: Check coherence of context against highway 296 } 297 } 298 } 299}