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