001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.HashMap; 009import java.util.HashSet; 010import java.util.Iterator; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Set; 015 016import org.openstreetmap.josm.command.ChangePropertyCommand; 017import org.openstreetmap.josm.command.Command; 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.FixableTestError; 023import org.openstreetmap.josm.data.validation.Severity; 024import org.openstreetmap.josm.data.validation.Test; 025import org.openstreetmap.josm.data.validation.TestError; 026import org.openstreetmap.josm.tools.Predicate; 027import org.openstreetmap.josm.tools.Utils; 028 029/** 030 * Test that performs semantic checks on highways. 031 * @since 5902 032 */ 033public class Highways extends Test { 034 035 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701; 036 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702; 037 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703; 038 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704; 039 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705; 040 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706; 041 protected static final int SOURCE_WRONG_LINK = 2707; 042 043 protected static final String SOURCE_MAXSPEED = "source:maxspeed"; 044 045 /** 046 * Classified highways in order of importance 047 */ 048 // CHECKSTYLE.OFF: SingleSpaceSeparator 049 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 050 "motorway", "motorway_link", 051 "trunk", "trunk_link", 052 "primary", "primary_link", 053 "secondary", "secondary_link", 054 "tertiary", "tertiary_link", 055 "unclassified", 056 "residential", 057 "living_street"); 058 // CHECKSTYLE.ON: SingleSpaceSeparator 059 060 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( 061 "urban", "rural", "zone", "zone30", "zone:30", "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 protected static class WrongRoundaboutHighway extends TestError { 080 081 public final String correctValue; 082 083 public WrongRoundaboutHighway(Highways tester, Way w, String key) { 084 super(tester, Severity.WARNING, 085 tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key), 086 WRONG_ROUNDABOUT_HIGHWAY, w); 087 this.correctValue = key; 088 } 089 } 090 091 @Override 092 public void visit(Node n) { 093 if (n.isUsable()) { 094 if (!n.hasTag("crossing", "no") 095 && !(n.hasKey("crossing") && (n.hasTag("highway", "crossing") || n.hasTag("highway", "traffic_signals"))) 096 && n.isReferredByWays(2)) { 097 testMissingPedestrianCrossing(n); 098 } 099 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}