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