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