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