001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.HashMap; 011import java.util.HashSet; 012import java.util.List; 013import java.util.Locale; 014import java.util.Map; 015import java.util.Map.Entry; 016import java.util.Objects; 017import java.util.Set; 018import java.util.stream.Collectors; 019import java.util.stream.Stream; 020 021import org.openstreetmap.josm.data.coor.EastNorth; 022import org.openstreetmap.josm.data.coor.LatLon; 023import org.openstreetmap.josm.data.osm.Node; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.data.osm.Relation; 026import org.openstreetmap.josm.data.osm.RelationMember; 027import org.openstreetmap.josm.data.osm.Way; 028import org.openstreetmap.josm.data.preferences.DoubleProperty; 029import org.openstreetmap.josm.data.validation.Severity; 030import org.openstreetmap.josm.data.validation.Test; 031import org.openstreetmap.josm.data.validation.TestError; 032import org.openstreetmap.josm.tools.Geometry; 033import org.openstreetmap.josm.tools.Logging; 034import org.openstreetmap.josm.tools.Pair; 035import org.openstreetmap.josm.tools.SubclassFilteredCollection; 036import org.openstreetmap.josm.tools.Utils; 037 038/** 039 * Performs validation tests on addresses (addr:housenumber) and associatedStreet relations. 040 * @since 5644 041 */ 042public class Addresses extends Test { 043 044 protected static final int HOUSE_NUMBER_WITHOUT_STREET = 2601; 045 protected static final int DUPLICATE_HOUSE_NUMBER = 2602; 046 protected static final int MULTIPLE_STREET_NAMES = 2603; 047 protected static final int MULTIPLE_STREET_RELATIONS = 2604; 048 protected static final int HOUSE_NUMBER_TOO_FAR = 2605; 049 050 protected static final DoubleProperty MAX_DUPLICATE_DISTANCE = new DoubleProperty("validator.addresses.max_duplicate_distance", 200.0); 051 protected static final DoubleProperty MAX_STREET_DISTANCE = new DoubleProperty("validator.addresses.max_street_distance", 200.0); 052 053 // CHECKSTYLE.OFF: SingleSpaceSeparator 054 protected static final String ADDR_HOUSE_NUMBER = "addr:housenumber"; 055 protected static final String ADDR_INTERPOLATION = "addr:interpolation"; 056 protected static final String ADDR_NEIGHBOURHOOD = "addr:neighbourhood"; 057 protected static final String ADDR_PLACE = "addr:place"; 058 protected static final String ADDR_STREET = "addr:street"; 059 protected static final String ADDR_CITY = "addr:city"; 060 protected static final String ADDR_UNIT = "addr:unit"; 061 protected static final String ADDR_FLATS = "addr:flats"; 062 protected static final String ADDR_HOUSE_NAME = "addr:housename"; 063 protected static final String ADDR_POSTCODE = "addr:postcode"; 064 protected static final String ASSOCIATED_STREET = "associatedStreet"; 065 // CHECKSTYLE.ON: SingleSpaceSeparator 066 067 private Map<String, Collection<OsmPrimitive>> knownAddresses; 068 private Set<String> ignoredAddresses; 069 070 /** 071 * Constructor 072 */ 073 public Addresses() { 074 super(tr("Addresses"), tr("Checks for errors in addresses and associatedStreet relations.")); 075 } 076 077 protected List<Relation> getAndCheckAssociatedStreets(OsmPrimitive p) { 078 List<Relation> list = OsmPrimitive.getFilteredList(p.getReferrers(), Relation.class); 079 list.removeIf(r -> !r.hasTag("type", ASSOCIATED_STREET)); 080 if (list.size() > 1) { 081 Severity level; 082 // warning level only if several relations have different names, see #10945 083 final String name = list.get(0).get("name"); 084 if (name == null || SubclassFilteredCollection.filter(list, r -> r.hasTag("name", name)).size() < list.size()) { 085 level = Severity.WARNING; 086 } else { 087 level = Severity.OTHER; 088 } 089 List<OsmPrimitive> errorList = new ArrayList<>(list); 090 errorList.add(0, p); 091 errors.add(TestError.builder(this, level, MULTIPLE_STREET_RELATIONS) 092 .message(tr("Multiple associatedStreet relations")) 093 .primitives(errorList) 094 .build()); 095 } 096 return list; 097 } 098 099 protected void checkHouseNumbersWithoutStreet(OsmPrimitive p) { 100 List<Relation> associatedStreets = getAndCheckAssociatedStreets(p); 101 // Find house number without proper location 102 // (neither addr:street, associatedStreet, addr:place, addr:neighbourhood or addr:interpolation) 103 if (p.hasKey(ADDR_HOUSE_NUMBER) && !p.hasKey(ADDR_STREET, ADDR_PLACE, ADDR_NEIGHBOURHOOD)) { 104 for (Relation r : associatedStreets) { 105 if (r.hasTag("type", ASSOCIATED_STREET)) { 106 return; 107 } 108 } 109 for (Way w : OsmPrimitive.getFilteredList(p.getReferrers(), Way.class)) { 110 if (w.hasKey(ADDR_INTERPOLATION) && w.hasKey(ADDR_STREET)) { 111 return; 112 } 113 } 114 // No street found 115 errors.add(TestError.builder(this, Severity.WARNING, HOUSE_NUMBER_WITHOUT_STREET) 116 .message(tr("House number without street")) 117 .primitives(p) 118 .build()); 119 } 120 } 121 122 static boolean isPOI(OsmPrimitive p) { 123 return p.hasKey("shop", "amenity", "tourism", "leisure", "emergency", "craft", "office", "name"); 124 } 125 126 static boolean hasAddress(OsmPrimitive p) { 127 return p.hasKey(ADDR_HOUSE_NUMBER) && p.hasKey(ADDR_STREET, ADDR_PLACE); 128 } 129 130 /** 131 * adds the OsmPrimitive to the address map if it complies to the restrictions 132 * @param p OsmPrimitive that has an address 133 */ 134 private void collectAddress(OsmPrimitive p) { 135 if (!isPOI(p)) { 136 String simplifiedAddress = getSimplifiedAddress(p); 137 if (!ignoredAddresses.contains(simplifiedAddress)) { 138 knownAddresses.computeIfAbsent(simplifiedAddress, x -> new ArrayList<>()).add(p); 139 } 140 } 141 } 142 143 protected void initAddressMap(OsmPrimitive primitive) { 144 knownAddresses = new HashMap<>(); 145 ignoredAddresses = new HashSet<>(); 146 for (OsmPrimitive p : primitive.getDataSet().allNonDeletedPrimitives()) { 147 if (p instanceof Node && p.hasKey(ADDR_UNIT, ADDR_FLATS)) { 148 for (OsmPrimitive r : p.getReferrers()) { 149 if (hasAddress(r)) { 150 // ignore addresses of buildings that are connected to addr:unit nodes 151 // it's quite reasonable that there are more buildings with this address 152 String simplifiedAddress = getSimplifiedAddress(r); 153 if (!ignoredAddresses.contains(simplifiedAddress)) { 154 ignoredAddresses.add(simplifiedAddress); 155 } else if (knownAddresses.containsKey(simplifiedAddress)) { 156 knownAddresses.remove(simplifiedAddress); 157 } 158 } 159 } 160 } 161 if (hasAddress(p)) { 162 collectAddress(p); 163 } 164 } 165 } 166 167 @Override 168 public void endTest() { 169 knownAddresses = null; 170 ignoredAddresses = null; 171 super.endTest(); 172 } 173 174 protected void checkForDuplicate(OsmPrimitive p) { 175 if (knownAddresses == null) { 176 initAddressMap(p); 177 } 178 if (!isPOI(p) && hasAddress(p)) { 179 String simplifiedAddress = getSimplifiedAddress(p); 180 if (ignoredAddresses.contains(simplifiedAddress)) { 181 return; 182 } 183 if (knownAddresses.containsKey(simplifiedAddress)) { 184 double maxDistance = MAX_DUPLICATE_DISTANCE.get(); 185 for (OsmPrimitive p2 : knownAddresses.get(simplifiedAddress)) { 186 if (p == p2) { 187 continue; 188 } 189 Severity severityLevel; 190 String city1 = p.get(ADDR_CITY); 191 String city2 = p2.get(ADDR_CITY); 192 double distance = getDistance(p, p2); 193 if (city1 != null && city2 != null) { 194 if (city1.equals(city2)) { 195 if (!p.hasKey(ADDR_POSTCODE) || !p2.hasKey(ADDR_POSTCODE) || p.get(ADDR_POSTCODE).equals(p2.get(ADDR_POSTCODE))) { 196 severityLevel = Severity.WARNING; 197 } else { 198 // address including city identical but postcode differs 199 // most likely perfectly fine 200 severityLevel = Severity.OTHER; 201 } 202 } else { 203 // address differs only by city - notify if very close, otherwise ignore 204 if (distance < maxDistance) { 205 severityLevel = Severity.OTHER; 206 } else { 207 continue; 208 } 209 } 210 } else { 211 // at least one address has no city specified 212 if (p.hasKey(ADDR_POSTCODE) && p2.hasKey(ADDR_POSTCODE) && p.get(ADDR_POSTCODE).equals(p2.get(ADDR_POSTCODE))) { 213 // address including postcode identical 214 severityLevel = Severity.WARNING; 215 } else { 216 // city/postcode unclear - warn if very close, otherwise only notify 217 // TODO: get city from surrounding boundaries? 218 if (distance < maxDistance) { 219 severityLevel = Severity.WARNING; 220 } else { 221 severityLevel = Severity.OTHER; 222 } 223 } 224 } 225 errors.add(TestError.builder(this, severityLevel, DUPLICATE_HOUSE_NUMBER) 226 .message(tr("Duplicate house numbers"), marktr("''{0}'' ({1}m)"), simplifiedAddress, (int) distance) 227 .primitives(Arrays.asList(p, p2)).build()); 228 } 229 knownAddresses.get(simplifiedAddress).remove(p); // otherwise we would get every warning two times 230 } 231 } 232 } 233 234 static String getSimplifiedAddress(OsmPrimitive p) { 235 String simplifiedStreetName = p.hasKey(ADDR_STREET) ? p.get(ADDR_STREET) : p.get(ADDR_PLACE); 236 // ignore whitespaces and dashes in street name, so that "Mozart-Gasse", "Mozart Gasse" and "Mozartgasse" are all seen as equal 237 return Utils.strip(Stream.of( 238 simplifiedStreetName.replaceAll("[ -]", ""), 239 p.get(ADDR_HOUSE_NUMBER), 240 p.get(ADDR_HOUSE_NAME), 241 p.get(ADDR_UNIT), 242 p.get(ADDR_FLATS)) 243 .filter(Objects::nonNull) 244 .collect(Collectors.joining(" "))) 245 .toUpperCase(Locale.ENGLISH); 246 } 247 248 @Override 249 public void visit(Node n) { 250 checkHouseNumbersWithoutStreet(n); 251 checkForDuplicate(n); 252 } 253 254 @Override 255 public void visit(Way w) { 256 checkHouseNumbersWithoutStreet(w); 257 checkForDuplicate(w); 258 } 259 260 @Override 261 public void visit(Relation r) { 262 checkHouseNumbersWithoutStreet(r); 263 checkForDuplicate(r); 264 if (r.hasTag("type", ASSOCIATED_STREET)) { 265 // Used to count occurences of each house number in order to find duplicates 266 Map<String, List<OsmPrimitive>> map = new HashMap<>(); 267 // Used to detect different street names 268 String relationName = r.get("name"); 269 Set<OsmPrimitive> wrongStreetNames = new HashSet<>(); 270 // Used to check distance 271 Set<OsmPrimitive> houses = new HashSet<>(); 272 Set<Way> street = new HashSet<>(); 273 for (RelationMember m : r.getMembers()) { 274 String role = m.getRole(); 275 OsmPrimitive p = m.getMember(); 276 if ("house".equals(role)) { 277 houses.add(p); 278 String number = p.get(ADDR_HOUSE_NUMBER); 279 if (number != null) { 280 number = number.trim().toUpperCase(Locale.ENGLISH); 281 List<OsmPrimitive> list = map.get(number); 282 if (list == null) { 283 list = new ArrayList<>(); 284 map.put(number, list); 285 } 286 list.add(p); 287 } 288 if (relationName != null && p.hasKey(ADDR_STREET) && !relationName.equals(p.get(ADDR_STREET))) { 289 if (wrongStreetNames.isEmpty()) { 290 wrongStreetNames.add(r); 291 } 292 wrongStreetNames.add(p); 293 } 294 } else if ("street".equals(role)) { 295 if (p instanceof Way) { 296 street.add((Way) p); 297 } 298 if (relationName != null && p.hasTagDifferent("name", relationName)) { 299 if (wrongStreetNames.isEmpty()) { 300 wrongStreetNames.add(r); 301 } 302 wrongStreetNames.add(p); 303 } 304 } 305 } 306 // Report duplicate house numbers 307 for (Entry<String, List<OsmPrimitive>> entry : map.entrySet()) { 308 List<OsmPrimitive> list = entry.getValue(); 309 if (list.size() > 1) { 310 errors.add(TestError.builder(this, Severity.WARNING, DUPLICATE_HOUSE_NUMBER) 311 .message(tr("Duplicate house numbers"), marktr("House number ''{0}'' duplicated"), entry.getKey()) 312 .primitives(list) 313 .build()); 314 } 315 } 316 // Report wrong street names 317 if (!wrongStreetNames.isEmpty()) { 318 errors.add(TestError.builder(this, Severity.WARNING, MULTIPLE_STREET_NAMES) 319 .message(tr("Multiple street names in relation")) 320 .primitives(wrongStreetNames) 321 .build()); 322 } 323 // Report addresses too far away 324 if (!street.isEmpty()) { 325 for (OsmPrimitive house : houses) { 326 if (house.isUsable()) { 327 checkDistance(house, street); 328 } 329 } 330 } 331 } 332 } 333 334 /** 335 * returns rough distance between two OsmPrimitives 336 * @param a primitive a 337 * @param b primitive b 338 * @return distance of center of bounding boxes in meters 339 */ 340 static double getDistance(OsmPrimitive a, OsmPrimitive b) { 341 LatLon centerA = a.getBBox().getCenter(); 342 LatLon centerB = b.getBBox().getCenter(); 343 return (centerA.greatCircleDistance(centerB)); 344 } 345 346 protected void checkDistance(OsmPrimitive house, Collection<Way> street) { 347 EastNorth centroid; 348 if (house instanceof Node) { 349 centroid = ((Node) house).getEastNorth(); 350 } else if (house instanceof Way) { 351 List<Node> nodes = ((Way) house).getNodes(); 352 if (house.hasKey(ADDR_INTERPOLATION)) { 353 for (Node n : nodes) { 354 if (n.hasKey(ADDR_HOUSE_NUMBER)) { 355 checkDistance(n, street); 356 } 357 } 358 return; 359 } 360 centroid = Geometry.getCentroid(nodes); 361 } else { 362 return; // TODO handle multipolygon houses ? 363 } 364 if (centroid == null) return; // fix #8305 365 double maxDistance = MAX_STREET_DISTANCE.get(); 366 boolean hasIncompleteWays = false; 367 for (Way streetPart : street) { 368 for (Pair<Node, Node> chunk : streetPart.getNodePairs(false)) { 369 EastNorth p1 = chunk.a.getEastNorth(); 370 EastNorth p2 = chunk.b.getEastNorth(); 371 if (p1 != null && p2 != null) { 372 EastNorth closest = Geometry.closestPointToSegment(p1, p2, centroid); 373 if (closest.distance(centroid) <= maxDistance) { 374 return; 375 } 376 } else { 377 Logging.warn("Addresses test skipped chunck "+chunk+" for street part "+streetPart+" because p1 or p2 is null"); 378 } 379 } 380 if (!hasIncompleteWays && streetPart.isIncomplete()) { 381 hasIncompleteWays = true; 382 } 383 } 384 // No street segment found near this house, report error on if the relation does not contain incomplete street ways (fix #8314) 385 if (hasIncompleteWays) return; 386 List<OsmPrimitive> errorList = new ArrayList<>(street); 387 errorList.add(0, house); 388 errors.add(TestError.builder(this, Severity.WARNING, HOUSE_NUMBER_TOO_FAR) 389 .message(tr("House number too far from street")) 390 .primitives(errorList) 391 .build()); 392 } 393}