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.List; 011import java.util.Locale; 012import java.util.Map; 013import java.util.Set; 014 015import org.openstreetmap.josm.command.ChangePropertyCommand; 016import org.openstreetmap.josm.data.osm.Node; 017import org.openstreetmap.josm.data.osm.OsmPrimitive; 018import org.openstreetmap.josm.data.osm.OsmUtils; 019import org.openstreetmap.josm.data.osm.Way; 020import org.openstreetmap.josm.data.validation.Severity; 021import org.openstreetmap.josm.data.validation.Test; 022import org.openstreetmap.josm.data.validation.TestError; 023import org.openstreetmap.josm.tools.Utils; 024 025/** 026 * Test that performs semantic checks on highways. 027 * @since 5902 028 */ 029public class Highways extends Test { 030 031 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701; 032 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702; 033 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703; 034 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704; 035 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705; 036 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706; 037 protected static final int SOURCE_WRONG_LINK = 2707; 038 039 protected static final String SOURCE_MAXSPEED = "source:maxspeed"; 040 041 /** 042 * Classified highways in order of importance 043 */ 044 // CHECKSTYLE.OFF: SingleSpaceSeparator 045 private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 046 "motorway", "motorway_link", 047 "trunk", "trunk_link", 048 "primary", "primary_link", 049 "secondary", "secondary_link", 050 "tertiary", "tertiary_link", 051 "unclassified", 052 "residential", 053 "living_street"); 054 // CHECKSTYLE.ON: SingleSpaceSeparator 055 056 private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList( 057 "urban", "rural", "zone", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road")); 058 059 private static final Set<String> ISO_COUNTRIES = new HashSet<>(Arrays.asList(Locale.getISOCountries())); 060 061 private boolean leftByPedestrians; 062 private boolean leftByCyclists; 063 private boolean leftByCars; 064 private int pedestrianWays; 065 private int cyclistWays; 066 private int carsWays; 067 068 /** 069 * Constructs a new {@code Highways} test. 070 */ 071 public Highways() { 072 super(tr("Highways"), tr("Performs semantic checks on highways.")); 073 } 074 075 @Override 076 public void visit(Node n) { 077 if (n.isUsable()) { 078 if (!n.hasTag("crossing", "no") 079 && !(n.hasKey("crossing") && (n.hasTag("highway", "crossing") || n.hasTag("highway", "traffic_signals"))) 080 && n.isReferredByWays(2)) { 081 testMissingPedestrianCrossing(n); 082 } 083 if (n.hasKey(SOURCE_MAXSPEED)) { 084 // Check maxspeed but not context against highway for nodes 085 // as maxspeed is not set on highways here but on signs, speed cameras, etc. 086 testSourceMaxspeed(n, false); 087 } 088 } 089 } 090 091 @Override 092 public void visit(Way w) { 093 if (w.isUsable()) { 094 if (w.isClosed() && w.hasKey("highway") && CLASSIFIED_HIGHWAYS.contains(w.get("highway")) 095 && w.hasKey("junction") && "roundabout".equals(w.get("junction"))) { 096 // TODO: find out how to handle splitted roundabouts (see #12841) 097 testWrongRoundabout(w); 098 } 099 if (w.hasKey(SOURCE_MAXSPEED)) { 100 // Check maxspeed, including context against highway 101 testSourceMaxspeed(w, true); 102 } 103 testHighwayLink(w); 104 } 105 } 106 107 private void testWrongRoundabout(Way w) { 108 Map<String, List<Way>> map = new HashMap<>(); 109 // Count all highways (per type) connected to this roundabout, except links 110 // As roundabouts are closed ways, take care of not processing the first/last node twice 111 for (Node n : new HashSet<>(w.getNodes())) { 112 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) { 113 String value = h.get("highway"); 114 if (h != w && value != null && !value.endsWith("_link")) { 115 List<Way> list = map.get(value); 116 if (list == null) { 117 list = new ArrayList<>(); 118 map.put(value, list); 119 } 120 list.add(h); 121 } 122 } 123 } 124 // The roundabout should carry the highway tag of its two biggest highways 125 for (String s : CLASSIFIED_HIGHWAYS) { 126 List<Way> list = map.get(s); 127 if (list != null && list.size() >= 2) { 128 // Except when a single road is connected, but with two oneway segments 129 Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway")); 130 Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway")); 131 if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) { 132 // Error when the highway tags do not match 133 if (!w.get("highway").equals(s)) { 134 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ROUNDABOUT_HIGHWAY) 135 .message(tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), s)) 136 .primitives(w) 137 .fix(() -> new ChangePropertyCommand(w, "highway", s)) 138 .build()); 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 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) { 150 return true; 151 } 152 153 final Set<OsmPrimitive> referrers = new HashSet<>(); 154 155 if (way.isClosed()) { 156 // for closed way we need to check all adjacent ways 157 for (Node n: way.getNodes()) { 158 referrers.addAll(n.getReferrers()); 159 } 160 } else { 161 referrers.addAll(way.firstNode().getReferrers()); 162 referrers.addAll(way.lastNode().getReferrers()); 163 } 164 165 return Utils.filteredCollection(referrers, Way.class).stream().anyMatch( 166 otherWay -> !way.equals(otherWay) && otherWay.hasTag("highway", highway, highway.replaceAll("_link$", ""))); 167 } 168 169 private void testHighwayLink(final Way way) { 170 if (!isHighwayLinkOkay(way)) { 171 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK) 172 .message(tr("Highway link is not linked to adequate highway/link")) 173 .primitives(way) 174 .build()); 175 } 176 } 177 178 private void testMissingPedestrianCrossing(Node n) { 179 leftByPedestrians = false; 180 leftByCyclists = false; 181 leftByCars = false; 182 pedestrianWays = 0; 183 cyclistWays = 0; 184 carsWays = 0; 185 186 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 187 String highway = w.get("highway"); 188 if (highway != null) { 189 if ("footway".equals(highway) || "path".equals(highway)) { 190 handlePedestrianWay(n, w); 191 if (w.hasTag("bicycle", "yes", "designated")) { 192 handleCyclistWay(n, w); 193 } 194 } else if ("cycleway".equals(highway)) { 195 handleCyclistWay(n, w); 196 if (w.hasTag("foot", "yes", "designated")) { 197 handlePedestrianWay(n, w); 198 } 199 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) { 200 // Only look at classified highways for now: 201 // - service highways support is TBD (see #9141 comments) 202 // - roads should be determined first. Another warning is raised anyway 203 handleCarWay(n, w); 204 } 205 if ((leftByPedestrians || leftByCyclists) && leftByCars) { 206 errors.add(TestError.builder(this, Severity.OTHER, MISSING_PEDESTRIAN_CROSSING) 207 .message(tr("Missing pedestrian crossing information")) 208 .primitives(n) 209 .build()); 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 final TestError.Builder error = TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE) 245 .message(tr("Unknown country code: {0}", country)) 246 .primitives(p); 247 if ("UK".equals(country)) { 248 errors.add(error.fix(() -> new ChangePropertyCommand(p, SOURCE_MAXSPEED, value.replace("UK:", "GB:"))).build()); 249 } else { 250 errors.add(error.build()); 251 } 252 } 253 // Check context 254 String context = value.substring(index+1); 255 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 256 errors.add(TestError.builder(this, Severity.WARNING, SOURCE_MAXSPEED_UNKNOWN_CONTEXT) 257 .message(tr("Unknown source:maxspeed context: {0}", context)) 258 .primitives(p) 259 .build()); 260 } 261 // TODO: Check coherence of context against maxspeed 262 // TODO: Check coherence of context against highway 263 } 264 } 265}