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