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