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}