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