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