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 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 058 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( 059 "urban", "rural", "zone", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road")); 060 061 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries())); 062 063 private boolean leftByPedestrians; 064 private boolean leftByCyclists; 065 private boolean leftByCars; 066 private int pedestrianWays; 067 private int cyclistWays; 068 private int carsWays; 069 070 /** 071 * Constructs a new {@code Highways} test. 072 */ 073 public Highways() { 074 super(tr("Highways"), tr("Performs semantic checks on highways.")); 075 } 076 077 protected static class WrongRoundaboutHighway extends TestError { 078 079 public final String correctValue; 080 081 public WrongRoundaboutHighway(Highways tester, Way w, String key) { 082 super(tester, Severity.WARNING, 083 tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key), 084 WRONG_ROUNDABOUT_HIGHWAY, w); 085 this.correctValue = key; 086 } 087 } 088 089 @Override 090 public void visit(Node n) { 091 if (n.isUsable()) { 092 if (!n.hasTag("crossing", "no") 093 && !(n.hasKey("crossing") && (n.hasTag("highway", "crossing") || n.hasTag("highway", "traffic_signals"))) 094 && n.isReferredByWays(2)) { 095 testMissingPedestrianCrossing(n); 096 } 097 if (n.hasKey(SOURCE_MAXSPEED)) { 098 // Check maxspeed but not context against highway for nodes 099 // as maxspeed is not set on highways here but on signs, speed cameras, etc. 100 testSourceMaxspeed(n, false); 101 } 102 } 103 } 104 105 @Override 106 public void visit(Way w) { 107 if (w.isUsable()) { 108 if (w.isClosed() && w.hasKey("highway") && CLASSIFIED_HIGHWAYS.contains(w.get("highway")) 109 && w.hasKey("junction") && "roundabout".equals(w.get("junction"))) { 110 // TODO: find out how to handle splitted roundabouts (see #12841) 111 testWrongRoundabout(w); 112 } 113 if (w.hasKey(SOURCE_MAXSPEED)) { 114 // Check maxspeed, including context against highway 115 testSourceMaxspeed(w, true); 116 } 117 testHighwayLink(w); 118 } 119 } 120 121 private void testWrongRoundabout(Way w) { 122 Map<String, List<Way>> map = new HashMap<>(); 123 // Count all highways (per type) connected to this roundabout, except links 124 // As roundabouts are closed ways, take care of not processing the first/last node twice 125 for (Node n : new HashSet<>(w.getNodes())) { 126 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) { 127 String value = h.get("highway"); 128 if (h != w && value != null && !value.endsWith("_link")) { 129 List<Way> list = map.get(value); 130 if (list == null) { 131 list = new ArrayList<>(); 132 map.put(value, list); 133 } 134 list.add(h); 135 } 136 } 137 } 138 // The roundabout should carry the highway tag of its two biggest highways 139 for (String s : CLASSIFIED_HIGHWAYS) { 140 List<Way> list = map.get(s); 141 if (list != null && list.size() >= 2) { 142 // Except when a single road is connected, but with two oneway segments 143 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway")); 144 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway")); 145 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) { 146 // Error when the highway tags do not match 147 if (!w.get("highway").equals(s)) { 148 errors.add(new WrongRoundaboutHighway(this, w, s)); 149 } 150 break; 151 } 152 } 153 } 154 } 155 156 public static boolean isHighwayLinkOkay(final Way way) { 157 final String highway = way.get("highway"); 158 if (highway == null || !highway.endsWith("_link") 159 || !IN_DOWNLOADED_AREA.evaluate(way.getNode(0)) || !IN_DOWNLOADED_AREA.evaluate(way.getNode(way.getNodesCount()-1))) { 160 return true; 161 } 162 163 final Set<OsmPrimitive> referrers = new HashSet<>(); 164 165 if (way.isClosed()) { 166 // for closed way we need to check all adjacent ways 167 for (Node n: way.getNodes()) { 168 referrers.addAll(n.getReferrers()); 169 } 170 } else { 171 referrers.addAll(way.firstNode().getReferrers()); 172 referrers.addAll(way.lastNode().getReferrers()); 173 } 174 175 return Utils.exists(Utils.filteredCollection(referrers, Way.class), new Predicate<Way>() { 176 @Override 177 public boolean evaluate(final Way otherWay) { 178 return !way.equals(otherWay) && otherWay.hasTag("highway", highway, highway.replaceAll("_link$", "")); 179 } 180 }); 181 } 182 183 private void testHighwayLink(final Way way) { 184 if (!isHighwayLinkOkay(way)) { 185 errors.add(new TestError(this, Severity.WARNING, 186 tr("Highway link is not linked to adequate highway/link"), SOURCE_WRONG_LINK, way)); 187 } 188 } 189 190 private void testMissingPedestrianCrossing(Node n) { 191 leftByPedestrians = false; 192 leftByCyclists = false; 193 leftByCars = false; 194 pedestrianWays = 0; 195 cyclistWays = 0; 196 carsWays = 0; 197 198 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 199 String highway = w.get("highway"); 200 if (highway != null) { 201 if ("footway".equals(highway) || "path".equals(highway)) { 202 handlePedestrianWay(n, w); 203 if (w.hasTag("bicycle", "yes", "designated")) { 204 handleCyclistWay(n, w); 205 } 206 } else if ("cycleway".equals(highway)) { 207 handleCyclistWay(n, w); 208 if (w.hasTag("foot", "yes", "designated")) { 209 handlePedestrianWay(n, w); 210 } 211 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) { 212 // Only look at classified highways for now: 213 // - service highways support is TBD (see #9141 comments) 214 // - roads should be determined first. Another warning is raised anyway 215 handleCarWay(n, w); 216 } 217 if ((leftByPedestrians || leftByCyclists) && leftByCars) { 218 errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"), 219 MISSING_PEDESTRIAN_CROSSING, n)); 220 return; 221 } 222 } 223 } 224 } 225 226 private void handleCarWay(Node n, Way w) { 227 carsWays++; 228 if (!w.isFirstLastNode(n) || carsWays > 1) { 229 leftByCars = true; 230 } 231 } 232 233 private void handleCyclistWay(Node n, Way w) { 234 cyclistWays++; 235 if (!w.isFirstLastNode(n) || cyclistWays > 1) { 236 leftByCyclists = true; 237 } 238 } 239 240 private void handlePedestrianWay(Node n, Way w) { 241 pedestrianWays++; 242 if (!w.isFirstLastNode(n) || pedestrianWays > 1) { 243 leftByPedestrians = true; 244 } 245 } 246 247 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) { 248 String value = p.get(SOURCE_MAXSPEED); 249 if (value.matches("[A-Z]{2}:.+")) { 250 int index = value.indexOf(':'); 251 // Check country 252 String country = value.substring(0, index); 253 if (!ISO_COUNTRIES.contains(country)) { 254 if ("UK".equals(country)) { 255 errors.add(new FixableTestError(this, Severity.WARNING, 256 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p, 257 new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:")))); 258 } else { 259 errors.add(new TestError(this, Severity.WARNING, 260 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p)); 261 } 262 } 263 // Check context 264 String context = value.substring(index+1); 265 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 266 errors.add(new TestError(this, Severity.WARNING, 267 tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p)); 268 } 269 // TODO: Check coherence of context against maxspeed 270 // TODO: Check coherence of context against highway 271 } 272 } 273 274 @Override 275 public boolean isFixable(TestError testError) { 276 return testError instanceof WrongRoundaboutHighway; 277 } 278 279 @Override 280 public Command fixError(TestError testError) { 281 if (testError instanceof WrongRoundaboutHighway) { 282 // primitives list can be empty if all primitives have been purged 283 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); 284 if (it.hasNext()) { 285 return new ChangePropertyCommand(it.next(), 286 "highway", ((WrongRoundaboutHighway) testError).correctValue); 287 } 288 } 289 return null; 290 } 291}