source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/Addresses.java@ 14501

Last change on this file since 14501 was 14273, checked in by stoecker, 6 years ago

fix typos - patch by naoliv - fix #16781 - Thanks a lot

  • Property svn:eol-style set to native
File size: 17.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.List;
13import java.util.Locale;
14import java.util.Map;
15import java.util.Map.Entry;
16import java.util.Objects;
17import java.util.Set;
18import java.util.stream.Collectors;
19import java.util.stream.Stream;
20
21import org.openstreetmap.josm.data.coor.EastNorth;
22import org.openstreetmap.josm.data.coor.LatLon;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.RelationMember;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.data.preferences.DoubleProperty;
29import org.openstreetmap.josm.data.validation.Severity;
30import org.openstreetmap.josm.data.validation.Test;
31import org.openstreetmap.josm.data.validation.TestError;
32import org.openstreetmap.josm.tools.Geometry;
33import org.openstreetmap.josm.tools.Logging;
34import org.openstreetmap.josm.tools.Pair;
35import org.openstreetmap.josm.tools.SubclassFilteredCollection;
36import org.openstreetmap.josm.tools.Utils;
37
38/**
39 * Performs validation tests on addresses (addr:housenumber) and associatedStreet relations.
40 * @since 5644
41 */
42public class Addresses extends Test {
43
44 protected static final int HOUSE_NUMBER_WITHOUT_STREET = 2601;
45 protected static final int DUPLICATE_HOUSE_NUMBER = 2602;
46 protected static final int MULTIPLE_STREET_NAMES = 2603;
47 protected static final int MULTIPLE_STREET_RELATIONS = 2604;
48 protected static final int HOUSE_NUMBER_TOO_FAR = 2605;
49
50 protected static final DoubleProperty MAX_DUPLICATE_DISTANCE = new DoubleProperty("validator.addresses.max_duplicate_distance", 200.0);
51 protected static final DoubleProperty MAX_STREET_DISTANCE = new DoubleProperty("validator.addresses.max_street_distance", 200.0);
52
53 // CHECKSTYLE.OFF: SingleSpaceSeparator
54 protected static final String ADDR_HOUSE_NUMBER = "addr:housenumber";
55 protected static final String ADDR_INTERPOLATION = "addr:interpolation";
56 protected static final String ADDR_NEIGHBOURHOOD = "addr:neighbourhood";
57 protected static final String ADDR_PLACE = "addr:place";
58 protected static final String ADDR_STREET = "addr:street";
59 protected static final String ADDR_CITY = "addr:city";
60 protected static final String ADDR_UNIT = "addr:unit";
61 protected static final String ADDR_FLATS = "addr:flats";
62 protected static final String ADDR_HOUSE_NAME = "addr:housename";
63 protected static final String ADDR_POSTCODE = "addr:postcode";
64 protected static final String ASSOCIATED_STREET = "associatedStreet";
65 // CHECKSTYLE.ON: SingleSpaceSeparator
66
67 private Map<String, Collection<OsmPrimitive>> knownAddresses;
68 private Set<String> ignoredAddresses;
69
70 /**
71 * Constructor
72 */
73 public Addresses() {
74 super(tr("Addresses"), tr("Checks for errors in addresses and associatedStreet relations."));
75 }
76
77 protected List<Relation> getAndCheckAssociatedStreets(OsmPrimitive p) {
78 List<Relation> list = OsmPrimitive.getFilteredList(p.getReferrers(), Relation.class);
79 list.removeIf(r -> !r.hasTag("type", ASSOCIATED_STREET));
80 if (list.size() > 1) {
81 Severity level;
82 // warning level only if several relations have different names, see #10945
83 final String name = list.get(0).get("name");
84 if (name == null || SubclassFilteredCollection.filter(list, r -> r.hasTag("name", name)).size() < list.size()) {
85 level = Severity.WARNING;
86 } else {
87 level = Severity.OTHER;
88 }
89 List<OsmPrimitive> errorList = new ArrayList<>(list);
90 errorList.add(0, p);
91 errors.add(TestError.builder(this, level, MULTIPLE_STREET_RELATIONS)
92 .message(tr("Multiple associatedStreet relations"))
93 .primitives(errorList)
94 .build());
95 }
96 return list;
97 }
98
99 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 occurrences 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}
Note: See TracBrowser for help on using the repository browser.