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.Collection;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Set;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.coor.EastNorth;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.RelationMember;
023import org.openstreetmap.josm.data.osm.Way;
024import org.openstreetmap.josm.data.validation.Severity;
025import org.openstreetmap.josm.data.validation.Test;
026import org.openstreetmap.josm.data.validation.TestError;
027import org.openstreetmap.josm.tools.Geometry;
028import org.openstreetmap.josm.tools.Pair;
029import org.openstreetmap.josm.tools.SubclassFilteredCollection;
030
031/**
032 * Performs validation tests on addresses (addr:housenumber) and associatedStreet relations.
033 * @since 5644
034 */
035public class Addresses extends Test {
036
037    protected static final int HOUSE_NUMBER_WITHOUT_STREET = 2601;
038    protected static final int DUPLICATE_HOUSE_NUMBER = 2602;
039    protected static final int MULTIPLE_STREET_NAMES = 2603;
040    protected static final int MULTIPLE_STREET_RELATIONS = 2604;
041    protected static final int HOUSE_NUMBER_TOO_FAR = 2605;
042
043    // CHECKSTYLE.OFF: SingleSpaceSeparator
044    protected static final String ADDR_HOUSE_NUMBER  = "addr:housenumber";
045    protected static final String ADDR_INTERPOLATION = "addr:interpolation";
046    protected static final String ADDR_PLACE         = "addr:place";
047    protected static final String ADDR_STREET        = "addr:street";
048    protected static final String ASSOCIATED_STREET  = "associatedStreet";
049    // CHECKSTYLE.ON: SingleSpaceSeparator
050
051    /**
052     * Constructor
053     */
054    public Addresses() {
055        super(tr("Addresses"), tr("Checks for errors in addresses and associatedStreet relations."));
056    }
057
058    protected List<Relation> getAndCheckAssociatedStreets(OsmPrimitive p) {
059        List<Relation> list = OsmPrimitive.getFilteredList(p.getReferrers(), Relation.class);
060        list.removeIf(r -> !r.hasTag("type", ASSOCIATED_STREET));
061        if (list.size() > 1) {
062            Severity level;
063            // warning level only if several relations have different names, see #10945
064            final String name = list.get(0).get("name");
065            if (name == null || SubclassFilteredCollection.filter(list, r -> name.equals(r.get("name"))).size() < list.size()) {
066                level = Severity.WARNING;
067            } else {
068                level = Severity.OTHER;
069            }
070            List<OsmPrimitive> errorList = new ArrayList<>(list);
071            errorList.add(0, p);
072            errors.add(TestError.builder(this, level, MULTIPLE_STREET_RELATIONS)
073                    .message(tr("Multiple associatedStreet relations"))
074                    .primitives(errorList)
075                    .build());
076        }
077        return list;
078    }
079
080    protected void checkHouseNumbersWithoutStreet(OsmPrimitive p) {
081        List<Relation> associatedStreets = getAndCheckAssociatedStreets(p);
082        // Find house number without proper location (neither addr:street, associatedStreet, addr:place or addr:interpolation)
083        if (p.hasKey(ADDR_HOUSE_NUMBER) && !p.hasKey(ADDR_STREET) && !p.hasKey(ADDR_PLACE)) {
084            for (Relation r : associatedStreets) {
085                if (r.hasTag("type", ASSOCIATED_STREET)) {
086                    return;
087                }
088            }
089            for (Way w : OsmPrimitive.getFilteredList(p.getReferrers(), Way.class)) {
090                if (w.hasKey(ADDR_INTERPOLATION) && w.hasKey(ADDR_STREET)) {
091                    return;
092                }
093            }
094            // No street found
095            errors.add(TestError.builder(this, Severity.WARNING, HOUSE_NUMBER_WITHOUT_STREET)
096                    .message(tr("House number without street"))
097                    .primitives(p)
098                    .build());
099        }
100    }
101
102    @Override
103    public void visit(Node n) {
104        checkHouseNumbersWithoutStreet(n);
105    }
106
107    @Override
108    public void visit(Way w) {
109        checkHouseNumbersWithoutStreet(w);
110    }
111
112    @Override
113    public void visit(Relation r) {
114        checkHouseNumbersWithoutStreet(r);
115        if (r.hasTag("type", ASSOCIATED_STREET)) {
116            // Used to count occurences of each house number in order to find duplicates
117            Map<String, List<OsmPrimitive>> map = new HashMap<>();
118            // Used to detect different street names
119            String relationName = r.get("name");
120            Set<OsmPrimitive> wrongStreetNames = new HashSet<>();
121            // Used to check distance
122            Set<OsmPrimitive> houses = new HashSet<>();
123            Set<Way> street = new HashSet<>();
124            for (RelationMember m : r.getMembers()) {
125                String role = m.getRole();
126                OsmPrimitive p = m.getMember();
127                if ("house".equals(role)) {
128                    houses.add(p);
129                    String number = p.get(ADDR_HOUSE_NUMBER);
130                    if (number != null) {
131                        number = number.trim().toUpperCase(Locale.ENGLISH);
132                        List<OsmPrimitive> list = map.get(number);
133                        if (list == null) {
134                            list = new ArrayList<>();
135                            map.put(number, list);
136                        }
137                        list.add(p);
138                    }
139                    if (relationName != null && p.hasKey(ADDR_STREET) && !relationName.equals(p.get(ADDR_STREET))) {
140                        if (wrongStreetNames.isEmpty()) {
141                            wrongStreetNames.add(r);
142                        }
143                        wrongStreetNames.add(p);
144                    }
145                } else if ("street".equals(role)) {
146                    if (p instanceof Way) {
147                        street.add((Way) p);
148                    }
149                    if (relationName != null && p.hasKey("name") && !relationName.equals(p.get("name"))) {
150                        if (wrongStreetNames.isEmpty()) {
151                            wrongStreetNames.add(r);
152                        }
153                        wrongStreetNames.add(p);
154                    }
155                }
156            }
157            // Report duplicate house numbers
158            for (Entry<String, List<OsmPrimitive>> entry : map.entrySet()) {
159                List<OsmPrimitive> list = entry.getValue();
160                if (list.size() > 1) {
161                    errors.add(TestError.builder(this, Severity.WARNING, DUPLICATE_HOUSE_NUMBER)
162                            .message(tr("Duplicate house numbers"), marktr("House number ''{0}'' duplicated"), entry.getKey())
163                            .primitives(list)
164                            .build());
165                }
166            }
167            // Report wrong street names
168            if (!wrongStreetNames.isEmpty()) {
169                errors.add(TestError.builder(this, Severity.WARNING, MULTIPLE_STREET_NAMES)
170                        .message(tr("Multiple street names in relation"))
171                        .primitives(wrongStreetNames)
172                        .build());
173            }
174            // Report addresses too far away
175            if (!street.isEmpty()) {
176                for (OsmPrimitive house : houses) {
177                    if (house.isUsable()) {
178                        checkDistance(house, street);
179                    }
180                }
181            }
182        }
183    }
184
185    protected void checkDistance(OsmPrimitive house, Collection<Way> street) {
186        EastNorth centroid;
187        if (house instanceof Node) {
188            centroid = ((Node) house).getEastNorth();
189        } else if (house instanceof Way) {
190            List<Node> nodes = ((Way) house).getNodes();
191            if (house.hasKey(ADDR_INTERPOLATION)) {
192                for (Node n : nodes) {
193                    if (n.hasKey(ADDR_HOUSE_NUMBER)) {
194                        checkDistance(n, street);
195                    }
196                }
197                return;
198            }
199            centroid = Geometry.getCentroid(nodes);
200        } else {
201            return; // TODO handle multipolygon houses ?
202        }
203        if (centroid == null) return; // fix #8305
204        double maxDistance = Main.pref.getDouble("validator.addresses.max_street_distance", 200.0);
205        boolean hasIncompleteWays = false;
206        for (Way streetPart : street) {
207            for (Pair<Node, Node> chunk : streetPart.getNodePairs(false)) {
208                EastNorth p1 = chunk.a.getEastNorth();
209                EastNorth p2 = chunk.b.getEastNorth();
210                if (p1 != null && p2 != null) {
211                    EastNorth closest = Geometry.closestPointToSegment(p1, p2, centroid);
212                    if (closest.distance(centroid) <= maxDistance) {
213                        return;
214                    }
215                } else {
216                    Main.warn("Addresses test skipped chunck "+chunk+" for street part "+streetPart+" because p1 or p2 is null");
217                }
218            }
219            if (!hasIncompleteWays && streetPart.isIncomplete()) {
220                hasIncompleteWays = true;
221            }
222        }
223        // No street segment found near this house, report error on if the relation does not contain incomplete street ways (fix #8314)
224        if (hasIncompleteWays) return;
225        List<OsmPrimitive> errorList = new ArrayList<>(street);
226        errorList.add(0, house);
227        errors.add(TestError.builder(this, Severity.WARNING, HOUSE_NUMBER_TOO_FAR)
228                .message(tr("House number too far from street"))
229                .primitives(errorList)
230                .build());
231    }
232}