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