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 class WrongRoundaboutHighway extends TestError { 078 079 public final String correctValue; 080 081 public WrongRoundaboutHighway(Way w, String key) { 082 super(Highways.this, 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.hasKey("highway") && CLASSIFIED_HIGHWAYS.contains(w.get("highway")) 109 && w.hasKey("junction") && "roundabout".equals(w.get("junction"))) { 110 testWrongRoundabout(w); 111 } 112 if (w.hasKey(SOURCE_MAXSPEED)) { 113 // Check maxspeed, including context against highway 114 testSourceMaxspeed(w, true); 115 } 116 testHighwayLink(w); 117 } 118 } 119 120 private void testWrongRoundabout(Way w) { 121 Map<String, List<Way>> map = new HashMap<>(); 122 // Count all highways (per type) connected to this roundabout, except links 123 // As roundabouts are closed ways, take care of not processing the first/last node twice 124 for (Node n : new HashSet<>(w.getNodes())) { 125 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) { 126 String value = h.get("highway"); 127 if (h != w && value != null && !value.endsWith("_link")) { 128 List<Way> list = map.get(value); 129 if (list == null) { 130 map.put(value, list = new ArrayList<>()); 131 } 132 list.add(h); 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 if (!w.get("highway").equals(s)) { 146 errors.add(new WrongRoundaboutHighway(w, s)); 147 } 148 break; 149 } 150 } 151 } 152 } 153 154 public static boolean isHighwayLinkOkay(final Way way) { 155 final String highway = way.get("highway"); 156 if (highway == null || !highway.endsWith("_link")) { 157 return true; 158 } 159 160 final Set<OsmPrimitive> referrers = new HashSet<>(); 161 162 if (way.isClosed()) { 163 // for closed way we need to check all adjacent ways 164 for (Node n: way.getNodes()) { 165 referrers.addAll(n.getReferrers()); 166 } 167 } else { 168 referrers.addAll(way.firstNode().getReferrers()); 169 referrers.addAll(way.lastNode().getReferrers()); 170 } 171 172 return Utils.exists(Utils.filteredCollection(referrers, Way.class), new Predicate<Way>() { 173 @Override 174 public boolean evaluate(final Way otherWay) { 175 return !way.equals(otherWay) && otherWay.hasTag("highway", highway, highway.replaceAll("_link$", "")); 176 } 177 }); 178 } 179 180 private void testHighwayLink(final Way way) { 181 if (!isHighwayLinkOkay(way)) { 182 errors.add(new TestError(this, Severity.WARNING, 183 tr("Highway link is not linked to adequate highway/link"), SOURCE_WRONG_LINK, way)); 184 } 185 } 186 187 private void testMissingPedestrianCrossing(Node n) { 188 leftByPedestrians = false; 189 leftByCyclists = false; 190 leftByCars = false; 191 pedestrianWays = 0; 192 cyclistWays = 0; 193 carsWays = 0; 194 195 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 196 String highway = w.get("highway"); 197 if (highway != null) { 198 if ("footway".equals(highway) || "path".equals(highway)) { 199 handlePedestrianWay(n, w); 200 if (w.hasTag("bicycle", "yes", "designated")) { 201 handleCyclistWay(n, w); 202 } 203 } else if ("cycleway".equals(highway)) { 204 handleCyclistWay(n, w); 205 if (w.hasTag("foot", "yes", "designated")) { 206 handlePedestrianWay(n, w); 207 } 208 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) { 209 // Only look at classified highways for now: 210 // - service highways support is TBD (see #9141 comments) 211 // - roads should be determined first. Another warning is raised anyway 212 handleCarWay(n, w); 213 } 214 if ((leftByPedestrians || leftByCyclists) && leftByCars) { 215 errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"), 216 MISSING_PEDESTRIAN_CROSSING, n)); 217 return; 218 } 219 } 220 } 221 } 222 223 private void handleCarWay(Node n, Way w) { 224 carsWays++; 225 if (!w.isFirstLastNode(n) || carsWays > 1) { 226 leftByCars = true; 227 } 228 } 229 230 private void handleCyclistWay(Node n, Way w) { 231 cyclistWays++; 232 if (!w.isFirstLastNode(n) || cyclistWays > 1) { 233 leftByCyclists = true; 234 } 235 } 236 237 private void handlePedestrianWay(Node n, Way w) { 238 pedestrianWays++; 239 if (!w.isFirstLastNode(n) || pedestrianWays > 1) { 240 leftByPedestrians = true; 241 } 242 } 243 244 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) { 245 String value = p.get(SOURCE_MAXSPEED); 246 if (value.matches("[A-Z]{2}:.+")) { 247 int index = value.indexOf(':'); 248 // Check country 249 String country = value.substring(0, index); 250 if (!ISO_COUNTRIES.contains(country)) { 251 if ("UK".equals(country)) { 252 errors.add(new FixableTestError(this, Severity.WARNING, 253 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p, 254 new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:")))); 255 } else { 256 errors.add(new TestError(this, Severity.WARNING, 257 tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p)); 258 } 259 } 260 // Check context 261 String context = value.substring(index+1); 262 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 263 errors.add(new TestError(this, Severity.WARNING, 264 tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p)); 265 } 266 // TODO: Check coherence of context against maxspeed 267 // TODO: Check coherence of context against highway 268 } 269 } 270 271 @Override 272 public boolean isFixable(TestError testError) { 273 return testError instanceof WrongRoundaboutHighway; 274 } 275 276 @Override 277 public Command fixError(TestError testError) { 278 if (testError instanceof WrongRoundaboutHighway) { 279 // primitives list can be empty if all primitives have been purged 280 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); 281 if (it.hasNext()) { 282 return new ChangePropertyCommand(it.next(), 283 "highway", ((WrongRoundaboutHighway) testError).correctValue); 284 } 285 } 286 return null; 287 } 288}