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.Utils; 025 026/** 027 * Test that performs semantic checks on highways. 028 * @since 5902 029 */ 030public class Highways extends Test { 031 032 protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701; 033 protected static final int MISSING_PEDESTRIAN_CROSSING = 2702; 034 protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703; 035 protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704; 036 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705; 037 protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706; 038 039 /** 040 * Classified highways in order of importance 041 */ 042 protected static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList( 043 "motorway", "motorway_link", 044 "trunk", "trunk_link", 045 "primary", "primary_link", 046 "secondary", "secondary_link", 047 "tertiary", "tertiary_link", 048 "unclassified", 049 "residential", 050 "living_street"); 051 052 protected static final List<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = Arrays.asList( 053 "urban", "rural", "zone", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street"); 054 055 protected static final List<String> ISO_COUNTRIES = Arrays.asList(Locale.getISOCountries()); 056 057 boolean leftByPedestrians = false; 058 boolean leftByCyclists = false; 059 boolean leftByCars = false; 060 int pedestrianWays = 0; 061 int cyclistWays = 0; 062 int carsWays = 0; 063 064 /** 065 * Constructs a new {@code Highways} test. 066 */ 067 public Highways() { 068 super(tr("Highways"), tr("Performs semantic checks on highways.")); 069 } 070 071 protected class WrongRoundaboutHighway extends TestError { 072 073 public final String correctValue; 074 075 public WrongRoundaboutHighway(Way w, String key) { 076 super(Highways.this, Severity.WARNING, 077 tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key), 078 WRONG_ROUNDABOUT_HIGHWAY, w); 079 this.correctValue = key; 080 } 081 } 082 083 @Override 084 public void visit(Node n) { 085 if (n.isUsable()) { 086 if (!n.hasTag("highway", "crossing") && !n.hasTag("crossing", "no") && n.isReferredByWays(2)) { 087 testMissingPedestrianCrossing(n); 088 } 089 if (n.hasKey("source:maxspeed")) { 090 // Check maxspeed but not context against highway for nodes as maxspeed is not set on highways here but on signs, speed cameras, etc. 091 testSourceMaxspeed(n, false); 092 } 093 } 094 } 095 096 @Override 097 public void visit(Way w) { 098 if (w.isUsable()) { 099 if (w.hasKey("highway") && w.hasKey("junction") && w.get("junction").equals("roundabout")) { 100 testWrongRoundabout(w); 101 } 102 if (w.hasKey("source:maxspeed")) { 103 // Check maxspeed, including context against highway 104 testSourceMaxspeed(w, true); 105 } 106 } 107 } 108 109 private void testWrongRoundabout(Way w) { 110 Map<String, List<Way>> map = new HashMap<String, List<Way>>(); 111 // Count all highways (per type) connected to this roundabout 112 // As roundabouts are closed ways, take care of not processing the first/last node twice 113 for (Node n : new HashSet<Node>(w.getNodes())) { 114 for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) { 115 if (h != w && h.hasKey("highway")) { 116 List<Way> list = map.get(h.get("highway")); 117 if (list == null) { 118 map.put(h.get("highway"), list = new ArrayList<Way>()); 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(new WrongRoundaboutHighway(w, s)); 135 } 136 break; 137 } 138 } 139 } 140 } 141 142 private void testMissingPedestrianCrossing(Node n) { 143 leftByPedestrians = false; 144 leftByCyclists = false; 145 leftByCars = false; 146 pedestrianWays = 0; 147 cyclistWays = 0; 148 carsWays = 0; 149 150 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 151 String highway = w.get("highway"); 152 if (highway != null) { 153 if (highway.equals("footway") || highway.equals("path")) { 154 handlePedestrianWay(n, w); 155 if (w.hasTag("bicycle", "yes", "designated")) { 156 handleCyclistWay(n, w); 157 } 158 } else if (highway.equals("cycleway")) { 159 handleCyclistWay(n, w); 160 if (w.hasTag("foot", "yes", "designated")) { 161 handlePedestrianWay(n, w); 162 } 163 } else if (CLASSIFIED_HIGHWAYS.contains(highway)) { 164 // Only look at classified highways for now: 165 // - service highways support is TBD (see #9141 comments) 166 // - roads should be determined first. Another warning is raised anyway 167 handleCarWay(n, w); 168 } 169 if ((leftByPedestrians || leftByCyclists) && leftByCars) { 170 errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"), MISSING_PEDESTRIAN_CROSSING, n)); 171 return; 172 } 173 } 174 } 175 } 176 177 private void handleCarWay(Node n, Way w) { 178 carsWays++; 179 if (!w.isFirstLastNode(n) || carsWays > 1) { 180 leftByCars = true; 181 } 182 } 183 184 private void handleCyclistWay(Node n, Way w) { 185 cyclistWays++; 186 if (!w.isFirstLastNode(n) || cyclistWays > 1) { 187 leftByCyclists = true; 188 } 189 } 190 191 private void handlePedestrianWay(Node n, Way w) { 192 pedestrianWays++; 193 if (!w.isFirstLastNode(n) || pedestrianWays > 1) { 194 leftByPedestrians = true; 195 } 196 } 197 198 private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) { 199 String value = p.get("source:maxspeed"); 200 if (value.matches("[A-Z]{2}:.+")) { 201 int index = value.indexOf(':'); 202 // Check country 203 String country = value.substring(0, index); 204 if (!ISO_COUNTRIES.contains(country)) { 205 errors.add(new TestError(this, Severity.WARNING, tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p)); 206 } 207 // Check context 208 String context = value.substring(index+1); 209 if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) { 210 errors.add(new TestError(this, Severity.WARNING, tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p)); 211 } 212 // TODO: Check coherence of context against maxspeed 213 // TODO: Check coherence of context against highway 214 } 215 } 216 217 @Override 218 public boolean isFixable(TestError testError) { 219 return testError instanceof WrongRoundaboutHighway; 220 } 221 222 @Override 223 public Command fixError(TestError testError) { 224 if (testError instanceof WrongRoundaboutHighway) { 225 // primitives list can be empty if all primitives have been purged 226 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); 227 if (it.hasNext()) { 228 return new ChangePropertyCommand(it.next(), 229 "highway", ((WrongRoundaboutHighway) testError).correctValue); 230 } 231 } 232 return null; 233 } 234}