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

Last change on this file since 13968 was 13968, checked in by Don-vip, 6 months ago

fix #16310 - check for duplicate addresses (patch by Luzandro, modified)

  • 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>> addresses = null;
68    private Set<String> ignoredAddresses = null;
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    private 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                addresses.computeIfAbsent(simplifiedAddress, x -> new ArrayList<>()).add(p);
139            }
140        }
141    }
142
143    protected void initAddressMap(OsmPrimitive primitive) {
144        addresses = 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 (addresses.containsKey(simplifiedAddress)) {
156                            addresses.remove(simplifiedAddress);
157                        }
158                    }
159                }
160            }
161            if (hasAddress(p)) {
162                collectAddress(p);
163            }
164        }
165    }
166
167    @Override
168    public void endTest() {
169        addresses = null;
170        ignoredAddresses = null;
171        super.endTest();
172    }
173
174    protected void checkForDuplicate(OsmPrimitive p) {
175        if (addresses == 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 (addresses.containsKey(simplifiedAddress)) {
184                double maxDistance = MAX_DUPLICATE_DISTANCE.get();
185                for (OsmPrimitive p2 : addresses.get(simplifiedAddress)) {
186                    if (p == p2) {
187                        continue;
188                    }
189                    Severity severityLevel = Severity.WARNING;
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                addresses.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}
Note: See TracBrowser for help on using the repository browser.