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