Ignore:
Timestamp:
2018-06-23T01:24:18+02:00 (6 years ago)
Author:
Don-vip
Message:

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/validation/tests/Addresses.java

    r12846 r13968  
    66
    77import java.util.ArrayList;
     8import java.util.Arrays;
    89import java.util.Collection;
    910import java.util.HashMap;
     
    1314import java.util.Map;
    1415import java.util.Map.Entry;
     16import java.util.Objects;
    1517import java.util.Set;
     18import java.util.stream.Collectors;
     19import java.util.stream.Stream;
    1620
    1721import org.openstreetmap.josm.data.coor.EastNorth;
     22import org.openstreetmap.josm.data.coor.LatLon;
    1823import org.openstreetmap.josm.data.osm.Node;
    1924import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    2126import org.openstreetmap.josm.data.osm.RelationMember;
    2227import org.openstreetmap.josm.data.osm.Way;
     28import org.openstreetmap.josm.data.preferences.DoubleProperty;
    2329import org.openstreetmap.josm.data.validation.Severity;
    2430import org.openstreetmap.josm.data.validation.Test;
    2531import org.openstreetmap.josm.data.validation.TestError;
    26 import org.openstreetmap.josm.spi.preferences.Config;
    2732import org.openstreetmap.josm.tools.Geometry;
    2833import org.openstreetmap.josm.tools.Logging;
    2934import org.openstreetmap.josm.tools.Pair;
    3035import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     36import org.openstreetmap.josm.tools.Utils;
    3137
    3238/**
     
    4147    protected static final int MULTIPLE_STREET_RELATIONS = 2604;
    4248    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);
    4352
    4453    // CHECKSTYLE.OFF: SingleSpaceSeparator
     
    4857    protected static final String ADDR_PLACE         = "addr:place";
    4958    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";
    5064    protected static final String ASSOCIATED_STREET  = "associatedStreet";
    5165    // CHECKSTYLE.ON: SingleSpaceSeparator
     66
     67    private Map<String, Collection<OsmPrimitive>> addresses = null;
     68    private Set<String> ignoredAddresses = null;
    5269
    5370    /**
     
    103120    }
    104121
     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
    105248    @Override
    106249    public void visit(Node n) {
    107250        checkHouseNumbersWithoutStreet(n);
     251        checkForDuplicate(n);
    108252    }
    109253
     
    111255    public void visit(Way w) {
    112256        checkHouseNumbersWithoutStreet(w);
     257        checkForDuplicate(w);
    113258    }
    114259
     
    116261    public void visit(Relation r) {
    117262        checkHouseNumbersWithoutStreet(r);
     263        checkForDuplicate(r);
    118264        if (r.hasTag("type", ASSOCIATED_STREET)) {
    119265            // Used to count occurences of each house number in order to find duplicates
     
    186332    }
    187333
     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
    188346    protected void checkDistance(OsmPrimitive house, Collection<Way> street) {
    189347        EastNorth centroid;
     
    205363        }
    206364        if (centroid == null) return; // fix #8305
    207         double maxDistance = Config.getPref().getDouble("validator.addresses.max_street_distance", 200.0);
     365        double maxDistance = MAX_STREET_DISTANCE.get();
    208366        boolean hasIncompleteWays = false;
    209367        for (Way streetPart : street) {
Note: See TracChangeset for help on using the changeset viewer.