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