001// License: GPL. See LICENSE file for details. 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; 014 015import org.openstreetmap.josm.command.ChangePropertyCommand; 016import org.openstreetmap.josm.command.Command; 017import org.openstreetmap.josm.data.osm.Node; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.data.osm.OsmUtils; 020import org.openstreetmap.josm.data.osm.Way; 021import org.openstreetmap.josm.data.validation.Severity; 022import org.openstreetmap.josm.data.validation.Test; 023import org.openstreetmap.josm.data.validation.TestError; 024import org.openstreetmap.josm.tools.Predicate; 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 /** 042 * Classified highways in order of importance 043 */ 044 protected static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 045 "motorway", "motorway_link", 046 "trunk", "trunk_link", 047 "primary", "primary_link", 048 "secondary", "secondary_link", 049 "tertiary", "tertiary_link", 050 "unclassified", 051 "residential", 052 "living_street"); 053 054 protected static final List<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = Arrays.asList( 055 "urban", "rural", "zone", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road"); 056 057 protected static final List<String> ISO_COUNTRIES = Arrays.asList(Locale.getISOCountries()); 058 059 boolean leftByPedestrians = false; 060 boolean leftByCyclists = false; 061 boolean leftByCars = false; 062 int pedestrianWays = 0; 063 int cyclistWays = 0; 064 int carsWays = 0; 065 066 /** 067 * Constructs a new {@code Highways} test. 068 */ 069 public Highways() { 070 super(tr("Highways"), tr("Performs semantic checks on highways.")); 071 } 072 073 protected class WrongRoundaboutHighway extends TestError { 074 075 public final String correctValue; 076 077 public WrongRoundaboutHighway(Way w, String key) { 078 super(Highways.this, Severity.WARNING, 079 tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key), 080 WRONG_ROUNDABOUT_HIGHWAY, w); 081 this.correctValue = key; 082 } 083 } 084 085 @Override 086 public void visit(Node n) { 087 if (n.isUsable()) { 088 if (!n.hasTag("highway", "crossing") && !n.hasTag("crossing", "no") && n.isReferredByWays(2)) { 089 testMissingPedestrianCrossing(n); 090 } 091 if (n.hasKey("source:maxspeed")) { 092 // Check maxspeed but not context against highway for nodes as maxspeed is not set on highways here but on signs, speed cameras, etc. 093 testSourceMaxspeed(n, false); 094 } 095 } 096 } 097 098 @Override 099 public void visit(Way w) { 100 if (w.isUsable()) { 101 if (w.hasKey("highway") && CLASSIFIED_HIGHWAYS.contains(w.get("highway")) && w.hasKey("junction") && "roundabout".equals(w.get("junction"))) { 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 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 : Utils.filteredCollection(n.getReferrers(), Way.class)) { 118 String value = h.get("highway"); 119 if (h != w && value != null && !value.endsWith("_link")) { 120 List<Way> list = map.get(value); 121 if (list == null) { 122 map.put(value, list = new ArrayList<>()); 123 } 124 list.add(h); 125 } 126 } 127 } 128 // The roundabout should carry the highway tag of its two biggest highways 129 for (String s : CLASSIFIED_HIGHWAYS) { 130 List<Way> list = map.get(s); 131 if (list != null && list.size() >= 2) { 132 // Except when a single road is connected, but with two oneway segments 133 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway")); 134 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway")); 135 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) { 136 // Error when the highway tags do not match 137 if (!w.get("highway").equals(s)) { 138 errors.add(new WrongRoundaboutHighway(w, s)); 139 } 140 break; 141 } 142 } 143 } 144 } 145 146 public static boolean isHighwayLinkOkay(final Way way) { 147 final String highway = way.get("highway"); 148 if (highway == null || !highway.endsWith("_link")) { 149 return true; 150 } 151 152 final HashSet<OsmPrimitive> referrers = new HashSet<>(); 153 154 if (way.isClosed()) { 155 // for closed way we need to check all adjacent ways 156 for (Node n: way.getNodes()) { 157 referrers.addAll(n.getReferrers()); 158 } 159 } else { 160 referrers.addAll(way.firstNode().getReferrers()); 161 referrers.addAll(way.lastNode().getReferrers()); 162 } 163 164 return Utils.exists(Utils.filteredCollection(referrers, Way.class), new Predicate<Way>() { 165 @Override 166 public boolean evaluate(final Way otherWay) { 167 return !way.equals(otherWay) && otherWay.hasTag("highway", highway, highway.replaceAll("_link$", "")); 168 } 169 }); 170 } 171 172 private void testHighwayLink(final Way way) { 173 if (!isHighwayLinkOkay(way)) { 174 errors.add(new TestError(this, Severity.WARNING, 175 tr("Highway link is not linked to adequate highway/link"), SOURCE_WRONG_LINK, way)); 176 } 177 } 178 179 private void testMissingPedestrianCrossing(Node n) { 180 leftByPedestrians = false; 181 leftByCyclists = false; 182 leftByCars = false; 183 pedestrianWays = 0; 184 cyclistWays = 0; 185 carsWays = 0; 186 187 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 188 String highway = w.get("highway"); 189 if (highway != null) { 190 if ("footway".equals(highway) || "path".equals(highway)) { 191 handlePedestrianWay(n, w); 192 if (w.hasTag("bicycle", "yes", "designated")) { 193 handleCyclistWay(n, w); 194 } 195 } else if ("cycleway".equals(highway)) { 196 handleCyclistWay(n, w); 197 if (w.hasTag("foot", "yes", "designated")) { 198 handlePedestrianWay(n, w); 199 } 200 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) { 201 // Only look at classified highways for now: 202 // - service highways support is TBD (see #9141 comments) 203 // - roads should be determined first. Another warning is raised anyway 204 handleCarWay(n, w); 205 } 206 if ((leftByPedestrians || leftByCyclists) && leftByCars) { 207 errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"), MISSING_PEDESTRIAN_CROSSING, n)); 208 return; 209 } 210 } 211 } 212 } 213 214 private void handleCarWay(Node n, Way w) { 215 carsWays++; 216 if (!w.isFirstLastNode(n) || carsWays > 1) { 217 leftByCars = true; 218 } 219 } 220 221 private void handleCyclistWay(Node n, Way w) { 222 cyclistWays++; 223 if (!w.isFirstLastNode(n) || cyclistWays > 1) { 224 leftByCyclists = true; 225 } 226 } 227 228 private void handlePedestrianWay(Node n, Way w) { 229 pedestrianWays++; 230 if (!w.isFirstLastNode(n) || pedestrianWays > 1) { 231 leftByPedestrians = true; 232 } 233 } 234 235 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) { 236 String value = p.get("source:maxspeed"); 237 if (value.matches("[A-Z]{2}:.+")) { 238 int index = value.indexOf(':'); 239 // Check country 240 String country = value.substring(0, index); 241 if (!ISO_COUNTRIES.contains(country)) { 242 errors.add(new TestError(this, Severity.WARNING, tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p)); 243 } 244 // Check context 245 String context = value.substring(index+1); 246 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 247 errors.add(new TestError(this, Severity.WARNING, tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p)); 248 } 249 // TODO: Check coherence of context against maxspeed 250 // TODO: Check coherence of context against highway 251 } 252 } 253 254 @Override 255 public boolean isFixable(TestError testError) { 256 return testError instanceof WrongRoundaboutHighway; 257 } 258 259 @Override 260 public Command fixError(TestError testError) { 261 if (testError instanceof WrongRoundaboutHighway) { 262 // primitives list can be empty if all primitives have been purged 263 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); 264 if (it.hasNext()) { 265 return new ChangePropertyCommand(it.next(), 266 "highway", ((WrongRoundaboutHighway) testError).correctValue); 267 } 268 } 269 return null; 270 } 271}