/* * Copyright (C) 2013. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package uk.me.parabola.mkgmap.osmstyle.housenumber; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.net.Numbers; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.CityInfo; import uk.me.parabola.mkgmap.general.LineAdder; import uk.me.parabola.mkgmap.general.MapRoad; import uk.me.parabola.mkgmap.general.ZipCodeInfo; import uk.me.parabola.mkgmap.reader.osm.Element; import uk.me.parabola.mkgmap.reader.osm.HousenumberHooks; import uk.me.parabola.mkgmap.reader.osm.Node; import uk.me.parabola.mkgmap.reader.osm.POIGeneratorHook; import uk.me.parabola.mkgmap.reader.osm.Relation; import uk.me.parabola.mkgmap.reader.osm.TagDict; import uk.me.parabola.mkgmap.reader.osm.Way; import uk.me.parabola.util.EnhancedProperties; import uk.me.parabola.util.KdTree; import uk.me.parabola.util.Locatable; import uk.me.parabola.util.MultiHashMap; /** * Collects all data required for OSM house number handling and adds the * house number information to the roads. * * @author WanMil, Gerd Petermann */ public class HousenumberGenerator { private static final Logger log = Logger.getLogger(HousenumberGenerator.class); /** Gives the maximum distance between house number element and the matching road */ public static final double MAX_DISTANCE_TO_ROAD = 150d; /** Gives the maximum distance for different elements with the same address */ public static final double MAX_DISTANCE_SAME_NUM = 100d; private boolean numbersEnabled; // options for handling of unnamed (service?) roads private int nameSearchDepth = 3; private MultiHashMap<String, HousenumberIvl> interpolationWays; private List<MapRoad> allRoads; private Map<Long,Integer> interpolationNodes; private List<HousenumberElem> houseElems; private HashMap<CityInfo, CityInfo> cityInfos = new HashMap<>(); private HashMap<ZipCodeInfo, ZipCodeInfo> zipInfos = new HashMap<>(); private static final short housenumberTagKey1 = TagDict.getInstance().xlate("mkgmap:housenumber"); private static final short housenumberTagKey2 = TagDict.getInstance().xlate("addr:housenumber"); private static final short streetTagKey = TagDict.getInstance().xlate("mkgmap:street"); private static final short addrStreetTagKey = TagDict.getInstance().xlate("addr:street"); private static final short addrInterpolationTagKey = TagDict.getInstance().xlate("addr:interpolation"); private static final short addrPlaceTagKey = TagDict.getInstance().xlate("addr:place"); private static final short cityTagKey = TagDict.getInstance().xlate("mkgmap:city"); private static final short regionTagKey = TagDict.getInstance().xlate("mkgmap:region"); private static final short countryTagKey = TagDict.getInstance().xlate("mkgmap:country"); private static final short postalCodeTagKey = TagDict.getInstance().xlate("mkgmap:postal_code"); private static final short numbersTagKey = TagDict.getInstance().xlate("mkgmap:numbers"); public HousenumberGenerator(EnhancedProperties props) { this.interpolationWays = new MultiHashMap<>(); this.allRoads = new ArrayList<>(); this.interpolationNodes = new HashMap<>(); this.houseElems = new ArrayList<>(); numbersEnabled = props.containsKey("housenumbers"); int n = props.getProperty("name-service-roads", 3); if (n != nameSearchDepth){ nameSearchDepth = Math.min(25, Math.max(0, n)); if (nameSearchDepth != n) System.err.println("name-service-roads=" + n + " was changed to name-service-roads=" + nameSearchDepth); } } /** * Retrieves the street name of this element. * @param e an OSM element * @return the street name (or {@code null} if no street name set) */ private static String getStreetname(Element e) { String streetname = e.getTag(streetTagKey); if (streetname == null) { streetname = e.getTag(addrStreetTagKey); } return streetname; } /** * Retrieves the house number of this element. * @param e an OSM element * @return the house number (or {@code null} if no house number set) */ public static String getHousenumber(Element e) { String res = e.getTag(housenumberTagKey1); if (res != null) return res; return e.getTag(housenumberTagKey2); } /** * Parses the house number string. It accepts the first positive number part * of a string. So all leading and preceding non number parts are ignored. * So the following strings are accepted: * <table> * <tr> * <th>Input</th> * <th>Output</th> * </tr> * <tr> * <td>23</td> * <td>23</td> * </tr> * <tr> * <td>-23</td> * <td>23</td> * </tr> * <tr> * <td>21-23</td> * <td>21</td> * </tr> * <tr> * <td>Abc 21</td> * <td>21</td> * </tr> * <tr> * <td>Abc 21.45</td> * <td>21</td> * </tr> * <tr> * <td>21 Main Street</td> * <td>21</td> * </tr> * <tr> * <td>Main Street</td> * <td><i>IllegalArgumentException</i></td> * </tr> * </table> * @throws IllegalArgumentException if parsing fails */ private static Integer parseHousenumber(String housenumberString) { if (housenumberString == null) { return null; } // the housenumber must match against the pattern <anything>number<notnumber><anything> int housenumber; Pattern p = Pattern.compile("\\D*(\\d+)\\D?.*"); Matcher m = p.matcher(housenumberString); if (m.matches() == false) { return null; } try { // get the number part and parse it housenumber = Integer.parseInt(m.group(1)); } catch (NumberFormatException exp) { return null; } return housenumber; } private HousenumberElem parseElement(Element el, String sign){ String city = el.getTag(cityTagKey); String region = el.getTag(regionTagKey); String country = el.getTag(countryTagKey); CityInfo ci = getCityInfos(city,region,country); HousenumberElem house = new HousenumberElem(el, ci); if (house.getLocation() == null){ // there has been a report that indicates match.getLocation() == null // could not reproduce so far but catching it here with some additional // information. (WanMil) log.error("OSM element seems to have no point."); log.error("Element: " + el.toBrowseURL() + " " + el); log.error("Please report on the mkgmap mailing list."); log.error("Continue creating the map. This should be possible without a problem."); return null; } house.setSign(sign); Integer hn = parseHousenumber(sign); if (hn == null){ if (log.isDebugEnabled()) log.debug("No housenumber (", el.toBrowseURL(), "): ", sign); return null; } if (hn < 0 || hn > 1_000_000){ log.warn("Number looks wrong, is ignored",house.getSign(),hn,"element",el.toBrowseURL()); return null; } house.setHousenumber(hn); house.setStreet(getStreetname(el)); house.setPlace(el.getTag(addrPlaceTagKey)); String zipStr = el.getTag(postalCodeTagKey); ZipCodeInfo zip = getZipInfos(zipStr); house.setZipCode(zip); return house; } private CityInfo getCityInfos(String city, String region, String country) { CityInfo ci = new CityInfo(city, region, country); CityInfo ciOld = cityInfos.get(ci); if (ciOld != null) return ciOld; // log.debug(ci); cityInfos.put(ci, ci); return ci; } private ZipCodeInfo getZipInfos(String zipStr) { ZipCodeInfo zip = new ZipCodeInfo(zipStr); ZipCodeInfo zipOld = zipInfos.get(zip); if (zipOld != null) return zipOld; zipInfos.put(zip, zip); return zip; } private HousenumberElem handleElement(Element el){ String sign = getHousenumber(el); if (sign == null) return null; HousenumberElem he = parseElement(el, sign); if (he == null) return null; houseElems.add(he); return he; } /** * Adds a node for house number processing. * @param n an OSM node */ public void addNode(Node n) { if (numbersEnabled == false) { return; } if("false".equals(n.getTag(numbersTagKey))) return; if ("true".equals(n.getTag(POIGeneratorHook.AREA2POI_TAG))){ // ignore POI created for buildings return; } HousenumberElem houseElem = handleElement(n); if (houseElem == null) return; if (n.getTag(HousenumberHooks.partOfInterpolationTagKey) != null) interpolationNodes.put(n.getId(),houseElems.size()-1); } /** * Adds a way for house number processing. * @param w a way */ public void addWay(Way w) { if (numbersEnabled == false) { return; } if("false".equals(w.getTag(numbersTagKey))) return; String ai = w.getTag(addrInterpolationTagKey); if (ai != null){ // the way has the addr:interpolation=* tag, parse info // created by the HousenumberHook List<HousenumberElem> nodes = new ArrayList<>(); String nodeIds = w.getTag(HousenumberHooks.mkgmapNodeIdsTagKey); if (nodeIds == null){ // way was rejected by hook } else { String[] ids = nodeIds.split(","); for (String idString : ids){ Long id = Long.decode(idString); Integer elemPos = interpolationNodes.get(id); if (elemPos != null){ HousenumberElem node = houseElems.get(elemPos); if (node != null){ assert node.getElement().getId() == id; nodes.add(node); } } } interpretInterpolationWay(w, nodes); } return; } if (w.hasIdenticalEndPoints()){ // we are only interested in polygons now handleElement(w); } } /** * Use the information provided by the addr:interpolation tag * to generate additional house number elements. This increases * the likelihood that a road segment is associated with the right * number ranges. * @param w the way * @param nodes2 * @param nodes list of nodes */ private void interpretInterpolationWay(Way w, List<HousenumberElem> nodes) { int numNodes = nodes.size(); String addrInterpolationMethod = w.getTag(addrInterpolationTagKey); int step = 0; switch (addrInterpolationMethod) { case "all": case "1": step = 1; break; case "even": case "odd": case "2": step = 2; break; default: break; } if (step == 0) return; // should not happen here int pos = 0; List<HousenumberIvl> hivls = new ArrayList<>(); String streetName = null; for (int i = 0; i+1 < numNodes; i++){ // the way have other points, find the sequence including the pair of nodes HousenumberElem he1 = nodes.get(i); HousenumberElem he2 = nodes.get(i+1); int pos1 = -1, pos2 = -1; for (int k = pos; k < w.getPoints().size(); k++){ if (w.getPoints().get(k) == he1.getLocation()){ pos1 = k; break; } } if (pos1 < 0){ log.error("addr:interpolation node not found in way",w); return; } for (int k = pos1+1; k < w.getPoints().size(); k++){ if (w.getPoints().get(k) == he2.getLocation()){ pos2 = k; break; } } if (pos2 < 0){ log.error("addr:interpolation node not found in way",w); return; } pos = pos2; String street = he1.getStreet(); if (street != null && street.equals(he2.getStreet())){ if (streetName == null) streetName = street; else if (streetName.equals(street) == false){ log.warn(w.toBrowseURL(),"addr:interpolation=even is used with different street names",streetName,street); return; } int start = he1.getHousenumber(); int end = he2.getHousenumber(); HousenumberIvl hivl = new HousenumberIvl(street, w, (Node)he1.element, (Node)he2.element); hivl.setStart(start); hivl.setEnd(end); hivl.setStep(step); hivl.calcSteps(); hivl.setPoints(w.getPoints().subList(pos1, pos2+1)); // if (pos1 > 0){ // double angle = Utils.getAngle(w.getPoints().get(pos1-1), w.getPoints().get(pos1), w.getPoints().get(pos1+1)); // if (Math.abs(angle) > 75){ // log.warn(w.toBrowseURL(),"addr:interpolation way has sharp angle at number",start,"cannot use it"); // return; // } // // } hivls.add(hivl); if ("even".equals(addrInterpolationMethod) && (start % 2 != 0 || end % 2 != 0)){ log.warn(w.toBrowseURL(),"addr:interpolation=even is used with odd housenumber(s)",start,end); return; } if ("odd".equals(addrInterpolationMethod) && (start % 2 == 0 || end % 2 == 0)){ log.warn(w.toBrowseURL(),"addr:interpolation=odd is used with even housenumber(s)",start,end); return; } if (start == end && he1.getSign().equals(he2.getSign())){ // handle special case from CanVec imports if (pos1 == 0 && pos2 +1 == w.getPoints().size()){ hivl.setEqualEnds(); log.warn(w.toBrowseURL(),"addr:interpolation way connects two points with equal numbers, numbers are ignored"); } } } } for (HousenumberIvl hivl : hivls) interpolationWays.add(streetName, hivl); } private MapRoad firstRoadSameOSMWay = null; /** * Adds a road to be processed by the house number generator. * @param osmRoad the OSM way the defines the road * @param road a road */ public void addRoad(Way osmRoad, MapRoad road) { allRoads.add(road); if (numbersEnabled) { if("false".equals(osmRoad.getTag(numbersTagKey))) road.setSkipHousenumberProcessing(true); /* * If the style adds the same OSM way as two or more routable ways, we use * only the first. This ensures that we don't try to assign numbers from bad * matches to these copies. */ if(!road.isSkipHousenumberProcessing()){ if (firstRoadSameOSMWay != null){ if (firstRoadSameOSMWay.getRoadDef().getId() == road.getRoadDef().getId()){ if (firstRoadSameOSMWay.getPoints().equals(road.getPoints())){ road.setSkipHousenumberProcessing(true); return; } } } firstRoadSameOSMWay = road; String name = road.getStreet(); if (name != null) { if (log.isDebugEnabled()) log.debug("Housenumber - Streetname:", name, "Way:",osmRoad.getId(),osmRoad.toTagString()); } } } } /** * Evaluate type=associatedStreet relations. */ public void addRelation(Relation r) { String relType = r.getTag("type"); // the wiki says that we should also evaluate type=street if ("associatedStreet".equals(relType) || "street".equals(relType)){ List<Element> houses= new ArrayList<>(); List<Element> streets = new ArrayList<>(); for (Map.Entry<String, Element> member : r.getElements()) { if (member.getValue() instanceof Node) { Node node = (Node) member.getValue(); houses.add(node); } else if (member.getValue() instanceof Way) { Way w = (Way) member.getValue(); String role = member.getKey(); switch (role) { case "house": case "addr:houselink": case "address": houses.add(w); break; case "street": streets.add(w); break; case "": if (w.getTag("highway") != null){ streets.add(w); continue; } String buildingTag = w.getTag("building"); if (buildingTag != null) houses.add(w); else log.warn("Relation",r.toBrowseURL(),": role of member",w.toBrowseURL(),"unclear"); break; default: if ("associatedStreet".equals(relType)) log.warn("Relation",r.toBrowseURL(),": don't know how to handle member with role",role); break; } } } String streetName = r.getTag("name"); String streetNameFromRoads = null; List<Element> unnamedStreetElems = new ArrayList<>(); boolean nameFromStreetsIsUnclear = false; if (streets.isEmpty() == false) { for (Element street : streets) { String roadName = street.getTag(streetTagKey); if (roadName == null) roadName = street.getTag("name"); if (roadName == null){ unnamedStreetElems.add(street); continue; } if (streetNameFromRoads == null) streetNameFromRoads = roadName; else if (streetNameFromRoads.equals(roadName) == false) nameFromStreetsIsUnclear = true; } } if (streetName == null){ if (nameFromStreetsIsUnclear == false) streetName = streetNameFromRoads; else { log.warn("Relation",r.toBrowseURL(),": ignored, street name is not clear."); return; } } else { if (streetNameFromRoads != null){ if (nameFromStreetsIsUnclear == false && streetName.equals(streetNameFromRoads) == false){ if (unnamedStreetElems.isEmpty() == false){ log.warn("Relation",r.toBrowseURL(),": ignored, street name is not clear."); return; } log.warn("Relation",r.toBrowseURL(),": street name is not clear, using the name from the way, not that of the relation."); streetName = streetNameFromRoads; } else if (nameFromStreetsIsUnclear == true){ log.warn("Relation",r.toBrowseURL(),": street name is not clear, using the name from the relation."); } } } int countModHouses = 0; if (streetName != null && streetName.isEmpty() == false){ for (Element house : houses) { if (addStreetTagFromRel(r, house, streetName) ) countModHouses++; } for (Element street : unnamedStreetElems) { street.addTag(streetTagKey, streetName); street.addTag("name", streetName); } } if (log.isInfoEnabled()){ if (countModHouses > 0 || !unnamedStreetElems.isEmpty()){ if (countModHouses > 0) log.info("Relation",r.toBrowseURL(),": added tag mkgmap:street=",streetName,"to",countModHouses,"of",houses.size(),"house members" ); if (!unnamedStreetElems.isEmpty()) log.info("Relation",r.toBrowseURL(),": added tag mkgmap:street=",streetName,"to",unnamedStreetElems.size(),"of",streets.size(),"street members" ); } else log.info("Relation",r.toBrowseURL(),": ignored, no house or street member was changed"); } } } /** * Add the tag mkgmap:street=streetName to the element of the * relation if it does not already have a street name tag. */ private static boolean addStreetTagFromRel(Relation r, Element house, String streetName){ String addrStreet = getStreetname(house); if (addrStreet == null){ house.addTag(streetTagKey, streetName); if (log.isDebugEnabled()) log.debug("Relation",r.toBrowseURL(),": adding tag mkgmap:street=" + streetName, "to house",house.toBrowseURL()); return true; } else if (addrStreet.equals(streetName) == false){ if (house.getTag(streetTagKey) != null){ log.warn("Relation",r.toBrowseURL(),": street name from relation doesn't match existing mkgmap:street tag for house",house.toBrowseURL(),"the house seems to be member of another type=associatedStreet relation"); house.deleteTag(streetTagKey); } else log.warn("Relation",r.toBrowseURL(),": street name from relation doesn't match existing name for house",house.toBrowseURL()); } return false; } /** * * @param adder * @param naxNodeId the highest nodeId used before */ public void generate(LineAdder adder, int naxNodeId) { if (numbersEnabled) { MultiHashMap<MapRoad,HousenumberMatch> initialHousesForRoads = findClosestRoadsToHouse(); identifyServiceRoads(); handleInterpolationWays(initialHousesForRoads); List<HousenumberRoad> hnrList = createHousenumberRoads(initialHousesForRoads); initialHousesForRoads = null; log.info("found",hnrList.size(),"road candidates for address search"); useAddrPlaceTag(hnrList); Map<MapRoad, HousenumberRoad> road2HousenumberRoadMap = new HashMap<>(); for (HousenumberRoad hnr : hnrList){ road2HousenumberRoadMap.put(hnr.getRoad(), hnr); } Int2ObjectOpenHashMap<HashSet<MapRoad>> nodeId2RoadLists = new Int2ObjectOpenHashMap<>(); for (MapRoad road : allRoads){ for (Coord co : road.getPoints()){ if (co.getId() == 0) continue; HashSet<MapRoad> connectedRoads = nodeId2RoadLists.get(co.getId()); if (connectedRoads == null){ connectedRoads = new HashSet<>(); nodeId2RoadLists.put(co.getId(), connectedRoads); } connectedRoads.add(road); } } List<HousenumberRoad> addedRoads = new ArrayList<>(); Iterator<HousenumberRoad> iter = hnrList.iterator(); while (iter.hasNext()){ HousenumberRoad hnr = iter.next(); List<HousenumberMatch> lostHouses = hnr.checkStreetName(road2HousenumberRoadMap, nodeId2RoadLists); for (HousenumberMatch house : lostHouses){ MapRoad r = house.getRoad(); if (r != null){ HousenumberRoad hnr2 = road2HousenumberRoadMap.get(r); if (hnr2 == null){ CityInfo ci = getCityInfos(r.getCity(), r.getRegion(), r.getCountry()); hnr2 = new HousenumberRoad(r, ci, Arrays.asList(house)); if (r.getZip() != null) hnr2.setZipCodeInfo(getZipInfos(r.getZip())); road2HousenumberRoadMap.put(r,hnr2); addedRoads.add(hnr2); } else { hnr2.addHouse(house); } } } if (hnr.getName() == null){ iter.remove(); for (HousenumberMatch house : hnr.getHouses()){ log.warn("found no plausible road name for address",house.toBrowseURL(),", closest road id:",house.getRoad()); } } } hnrList.addAll(addedRoads); // TODO: interpolate addr:interpolation houses removeDupsGroupedByCityAndName(hnrList); // group by street name and city TreeMap<String, TreeMap<CityInfo, List<HousenumberRoad>>> streetnameCityRoadMap = new TreeMap<>(); for (HousenumberRoad hnr : hnrList){ TreeMap<CityInfo, List<HousenumberRoad>> cluster = streetnameCityRoadMap.get(hnr.getName()); if (cluster == null){ cluster = new TreeMap<>(); streetnameCityRoadMap.put(hnr.getName(), cluster); } List<HousenumberRoad> roadsInCluster = cluster.get(hnr.getRoadCityInfo()); if (roadsInCluster == null){ roadsInCluster = new ArrayList<>(); cluster.put(hnr.getRoadCityInfo(), roadsInCluster); } roadsInCluster.add(hnr); } for (Entry<String, TreeMap<CityInfo, List<HousenumberRoad>>> streetNameEntry : streetnameCityRoadMap.entrySet()){ String streetName = streetNameEntry.getKey(); for (Entry<CityInfo, List<HousenumberRoad>> clusterEntry : streetNameEntry.getValue().entrySet()){ useInterpolationInfo(streetName, clusterEntry.getValue(), road2HousenumberRoadMap); } for (Entry<CityInfo, List<HousenumberRoad>> clusterEntry : streetNameEntry.getValue().entrySet()){ List<HousenumberRoad> roadsInCluster = clusterEntry.getValue(); if (log.isDebugEnabled()){ log.debug("processing road(s) with name",streetName,"in",clusterEntry.getKey() ); } for (HousenumberRoad hnr : roadsInCluster){ hnr.buildIntervals(); } boolean optimized = false; for (int loop = 0; loop < 10; loop++){ for (HousenumberRoad hnr : roadsInCluster){ hnr.checkIntervals(); } checkWrongRoadAssignmments(roadsInCluster); boolean changed = hasChanges(roadsInCluster); if (!optimized && !changed){ for (HousenumberRoad hnr : roadsInCluster){ hnr.improveSearchResults(); } changed = hasChanges(roadsInCluster); optimized = true; } if (!changed) break; } for (HousenumberRoad hnr : roadsInCluster){ hnr.setNumbers(); } } } } if (log.isInfoEnabled()){ for (HousenumberElem house : houseElems){ if (house.getRoad() == null){ if (house.getStreet() != null) log.info("found no plausible road for house number element",house.toBrowseURL(),house.getStreet(),house.getSign()); else log.info("found no plausible road for house number element",house.toBrowseURL()); } } } for (MapRoad r : allRoads) { if (log.isDebugEnabled()){ List<Numbers> finalNumbers = r.getRoadDef().getNumbersList(); if (finalNumbers != null){ log.info("id:"+r.getRoadDef().getId(),", final numbers,",r,"in",r.getCity()); for (Numbers cn : finalNumbers){ if (cn.isEmpty()) continue; log.info("id:"+r.getRoadDef().getId(),", Left: ",cn.getLeftNumberStyle(),cn.getIndex(),"Start:",cn.getLeftStart(),"End:",cn.getLeftEnd()); log.info("id:"+r.getRoadDef().getId(),", Right:",cn.getRightNumberStyle(),cn.getIndex(),"Start:",cn.getRightStart(),"End:",cn.getRightEnd()); } } } adder.add(r); } } private List<HousenumberRoad> createHousenumberRoads( MultiHashMap<MapRoad, HousenumberMatch> initialHousesForRoads) { List<HousenumberRoad> hnrList = new ArrayList<>(); for (MapRoad road : allRoads){ if (road.isSkipHousenumberProcessing()) continue; List<HousenumberMatch> houses = initialHousesForRoads.get(road); if (houses == null || houses.isEmpty()) continue; CityInfo ci = getCityInfos(road.getCity(), road.getRegion(), road.getCountry()); HousenumberRoad hnr = new HousenumberRoad(road, ci, houses); if (road.getZip() != null) hnr.setZipCodeInfo(getZipInfos(road.getZip())); hnrList.add(hnr); } return hnrList; } private MultiHashMap<MapRoad, HousenumberMatch> findClosestRoadsToHouse() { // build road index long t1 = System.currentTimeMillis(); RoadSegmentIndex roadSegmentIndex = new RoadSegmentIndex(allRoads, MAX_DISTANCE_TO_ROAD); long t2 = System.currentTimeMillis(); log.debug("creation of road index took",t2-t1,"ms"); long t3 = System.currentTimeMillis(); MultiHashMap<MapRoad,HousenumberMatch> initialHousesForRoads = new MultiHashMap<>(); for (int i = 0; i < houseElems.size(); i++){ HousenumberElem house = houseElems.get(i); HousenumberMatch bestMatch = roadSegmentIndex.createHousenumberMatch(house); houseElems.set(i, bestMatch); if (bestMatch.getRoad() == null){ bestMatch.setIgnored(true); // XXX maybe create a pseudo road with zero length? // log.warn("found no plausible road for house number element",house.getElement().toBrowseURL()); continue; } initialHousesForRoads.add(bestMatch.getRoad(), bestMatch); } long t4 = System.currentTimeMillis(); log.debug("identification of closest road for each house took",t4-t3,"ms"); return initialHousesForRoads; } private void useAddrPlaceTag(List<HousenumberRoad> hnrList){ HashMap<CityInfo,MultiHashMap<String,HousenumberMatch>> cityPlaceHouseMap = new LinkedHashMap<>(); for (int i = 0; i < houseElems.size(); i++){ HousenumberElem house = houseElems.get(i); if (house.getRoad() == null) continue; if (house.getPlace() == null) continue; MultiHashMap<String, HousenumberMatch> subMap = cityPlaceHouseMap.get(house.getCityInfo()); if (subMap == null){ subMap = new MultiHashMap<>(); cityPlaceHouseMap.put(house.getCityInfo(), subMap); } subMap.add(house.getPlace(), (HousenumberMatch) house); } log.info("analysing",cityPlaceHouseMap.size(),"cities with addr:place=* houses" ); for (Entry<CityInfo, MultiHashMap<String, HousenumberMatch>> topEntry : cityPlaceHouseMap.entrySet()){ CityInfo cityInfo = topEntry.getKey(); List<String> placeNames = new ArrayList<>(topEntry.getValue().keySet()); Collections.sort(placeNames); for (String placeName : placeNames){ List<HousenumberMatch> placeHouses = topEntry.getValue().get(placeName); HashSet<HousenumberRoad> roads = new LinkedHashSet<>(); Int2IntOpenHashMap usedNumbers = new Int2IntOpenHashMap(); HashMap<String,Integer> usedSigns = new HashMap<>(); int dupSigns = 0; int dupNumbers = 0; int housesWithStreet = 0; int housesWithMatchingStreet = 0; int roadsWithNames = 0; int unnamedCloseRoads = 0; for (HousenumberMatch house : placeHouses){ if (house.getStreet() != null ){ ++housesWithStreet; if (house.getStreet().equalsIgnoreCase(house.getRoad().getStreet())){ ++housesWithMatchingStreet; } } else { if (house.getRoad().getStreet() == null) ++unnamedCloseRoads; } boolean added = roads.add(house.getHousenumberRoad()); if (added && house.getRoad().getStreet() != null) ++roadsWithNames; int oldCount = usedNumbers.put(house.getHousenumber(),1); if (oldCount != 0){ usedNumbers.put(house.getHousenumber(), oldCount + 1); ++dupNumbers; } Integer oldSignCount = usedSigns.put(house.getSign(), 1); if (oldSignCount != null){ usedSigns.put(house.getSign(), oldSignCount + 1); ++dupSigns; } } if (log.isDebugEnabled()){ log.debug("place",placeName,"in city",cityInfo, ":", "houses:", placeHouses.size(), ",duplicate numbers/signs:", dupNumbers+"/"+dupSigns, ",roads (named/unnamed):", roads.size(),"("+roadsWithNames+"/"+(roads.size()- roadsWithNames)+")", ",houses without addr:street:", placeHouses.size() - housesWithStreet, ",street = name of closest road:", housesWithMatchingStreet, ",houses without addr:street near named road:", unnamedCloseRoads); } if ((float) dupSigns / placeHouses.size() < 0.25 ){ if (log.isDebugEnabled()) log.debug("will not use gaps in intervals for roads in",placeName ); for (HousenumberRoad hnr : roads){ hnr.setRemoveGaps(true); } } if (placeHouses.size() > housesWithStreet){ // XXX: threshold value? LongArrayList ids = new LongArrayList(); for (HousenumberRoad hnr : roads){ ids.add(hnr.getRoad().getRoadDef().getId()); hnr.addPlaceName(placeName); } if (log.isDebugEnabled()) log.debug("detected",placeName,"as potential address name for roads",ids); } else { if (log.isDebugEnabled()) log.debug("will ignore addr:place for address search in",placeName,"in city",cityInfo); } } } } /** * Update the house number information in the interpolation intervals and * use them to correct wrong road assignments. * @param initialHousesForRoads map that is updated when wrong road assignments were found */ private void handleInterpolationWays(MultiHashMap<MapRoad, HousenumberMatch> initialHousesForRoads) { for (Entry<String, List<HousenumberIvl>> entry : interpolationWays.entrySet()){ List<HousenumberIvl> infos = entry.getValue(); for (HousenumberIvl info : infos){ if (info.isBad()){ continue; } boolean isOK = info.setNodeRefs(interpolationNodes, houseElems); if (!isOK) continue; HousenumberMatch[] houses = info.getHouseNodes(); MapRoad uncheckedRoads[] = new MapRoad[houses.length]; for (int i = 0 ; i < houses.length; i++) uncheckedRoads[i] = houses[i].getRoad(); isOK = info.checkRoads(); // check if houses are assigned to different roads now houses = info.getHouseNodes(); for (int i = 0 ; i < houses.length; i++){ if (houses[i].getRoad() != uncheckedRoads[i]){ initialHousesForRoads.removeMapping(uncheckedRoads[i], houses[i]); if (houses[i].isIgnored() == false) initialHousesForRoads.add(houses[i].getRoad(), houses[i]); else { if (!isOK) log.info("housenumber is assigned to different road after checking addr:interpolation way which turned out to be invalid",houses[i],info ); } } } } } } private void removeDupsGroupedByCityAndName(List<HousenumberRoad> hnrList){ HashMap<CityInfo,MultiHashMap<String,HousenumberMatch>> cityNameHouseMap = new LinkedHashMap<>(); for (int i = 0; i < houseElems.size(); i++){ HousenumberElem house = houseElems.get(i); if (house.getRoad() == null) continue; if (house instanceof HousenumberMatch){ HousenumberMatch hm = (HousenumberMatch) house; if (hm.isIgnored()) continue; HousenumberRoad hnr = hm.getHousenumberRoad(); if (hnr == null || hnr.getName() == null) continue; MultiHashMap<String, HousenumberMatch> subMap = cityNameHouseMap.get(hm.getCityInfo()); if (subMap == null){ subMap = new MultiHashMap<>(); cityNameHouseMap.put(hm.getCityInfo(), subMap); } subMap.add(hnr.getName(), hm); } } for (Entry<CityInfo, MultiHashMap<String, HousenumberMatch>> topEntry : cityNameHouseMap.entrySet()){ for (Entry<String, List<HousenumberMatch>> entry : topEntry.getValue().entrySet()){ markSimpleDuplicates(entry.getKey(), entry.getValue()); } } } private static void checkSegment(HousenumberMatch house, MapRoad road, int seg){ Coord cx = house.getLocation(); Coord c0 = road.getPoints().get(seg); Coord c1 = road.getPoints().get(seg + 1); double frac = getFrac(c0, c1, cx); double dist = distanceToSegment(c0,c1,cx,frac); if (dist < house.getDistance()){ house.setDistance(dist); house.setRoad(road); house.setSegment(seg); house.setSegmentFrac(frac); } } /** * process option --x-name-service-roads=n * The program identifies unnamed roads which are only connected to one * road with a name or to multiple roads with the same name. The process is * repeated n times. If n > 1 the program will also use unnamed roads which * are connected to unnamed roads if those are connected to named roads. * Higher values for n mean deeper search, but reasonable values are * probably between 1 and 5. * * These roads are then used for house number processing like the named * ones. If house numbers are assigned to these roads, they are named so * that address search will find them. */ private void identifyServiceRoads() { Int2ObjectOpenHashMap<String> roadNamesByNodeIds = new Int2ObjectOpenHashMap<>(); HashMap<MapRoad, List<Coord>> coordNodesUnnamedRoads = new HashMap<>(); HashSet<Integer> unclearNodeIds = new HashSet<>(); long t1 = System.currentTimeMillis(); List<MapRoad> unnamedRoads = new ArrayList<>(); for (MapRoad road : allRoads){ if (road.isSkipHousenumberProcessing()) continue; if (road.getStreet() == null){ // if a road has a label but getStreet() returns null, // the road probably has a ref. We assume these are not service roads. if (road.getName() == null){ unnamedRoads.add(road); List<Coord> nodes = new ArrayList<>(); for (Coord co : road.getPoints()){ if (co.getId() != 0) nodes.add(co); } coordNodesUnnamedRoads.put(road, nodes); } } else { identifyNodes(road.getPoints(), road.getStreet(), roadNamesByNodeIds, unclearNodeIds); } } int numUnnamedRoads = unnamedRoads.size(); long t2 = System.currentTimeMillis(); if (log.isDebugEnabled()) log.debug("identifyServiceRoad step 1 took",(t2-t1),"ms, found",roadNamesByNodeIds.size(),"nodes to check and",numUnnamedRoads,"unnamed roads" ); long t3 = System.currentTimeMillis(); int named = 0; for (int pass = 1; pass <= nameSearchDepth; pass ++){ int unnamed = 0; List<MapRoad> namedRoads = new ArrayList<>(); for (int j = 0; j < unnamedRoads.size(); j++){ MapRoad road = unnamedRoads.get(j); if (road == null) continue; unnamed++; List<Coord> coordNodes = coordNodesUnnamedRoads.get(road); String name = null; for (Coord co : coordNodes){ if (unclearNodeIds.contains(co.getId())){ name = null; unnamedRoads.set(j, null); // don't process again break; } String possibleName = roadNamesByNodeIds.get(co.getId()); if (possibleName == null) continue; if (name == null) name = possibleName; else if (name.equals(possibleName) == false){ name = null; unnamedRoads.set(j, null); // don't process again break; } } if (name != null){ named++; road.setStreet(name); namedRoads.add(road); unnamedRoads.set(j, null); // don't process again } } for (MapRoad road : namedRoads){ road.setNamedByHousenumberProcessing(true); String name = road.getStreet(); if (log.isDebugEnabled()) log.debug("pass",pass,"using unnamed road for housenumber processing,id=",road.getRoadDef().getId(),":",name); List<Coord> coordNodes = coordNodesUnnamedRoads.get(road); identifyNodes(coordNodes, name, roadNamesByNodeIds, unclearNodeIds); } if (namedRoads.isEmpty()) break; if (log.isDebugEnabled()) log.debug("pass",pass,unnamed,named); } long t4 = System.currentTimeMillis(); if (log.isDebugEnabled()){ log.debug("indentifyServiceRoad step 2 took",(t4-t3),"ms, found a name for",named,"of",numUnnamedRoads,"roads" ); } return; } private void identifyNodes(List<Coord> roadPoints, String streetName, Int2ObjectOpenHashMap<String> roadNamesByNodeIds, HashSet<Integer> unclearNodes) { for (Coord co : roadPoints){ if (co.getId() != 0){ String prevName = roadNamesByNodeIds.put(co.getId(), streetName); if (prevName != null){ if (prevName.equals(streetName) == false) unclearNodes.add(co.getId()); } } } } /** * Find house number nodes which are parts of addr:interpolation ways. * Check each addr:interpolation way for plausibility. * If the check is OK, interpolate the numbers, if not, ignore * also the numbers connected to the implausible way. * * XXX: Known problem: Doesn't work well when the road was * clipped at the tile boundary. * @param streetName * @param roadsInCluster * @param road2HousenumberRoadMap * @param interpolationInfos */ private void useInterpolationInfo(String streetName, List<HousenumberRoad> roadsInCluster, Map<MapRoad, HousenumberRoad> road2HousenumberRoadMap) { List<HousenumberIvl> interpolationInfos = interpolationWays.get(streetName); if (interpolationInfos.isEmpty()) return; List<HousenumberMatch> housesWithIvlInfo = new ArrayList<>(); for (HousenumberRoad hnr : roadsInCluster){ for (HousenumberMatch house : hnr.getHouses()){ if (house.getIntervalInfoRefs() > 0) housesWithIvlInfo.add(house); } } if (housesWithIvlInfo.isEmpty()) return; HashMap<String, HousenumberIvl> simpleDupCheckSet = new HashMap<>(); HashSet<HousenumberIvl> badIvls = new HashSet<>(); Long2ObjectOpenHashMap<HousenumberIvl> id2IvlMap = new Long2ObjectOpenHashMap<>(); Int2ObjectOpenHashMap<HousenumberMatch> interpolatedNumbers = new Int2ObjectOpenHashMap<>(); Int2ObjectOpenHashMap<HousenumberMatch> existingNumbers = new Int2ObjectOpenHashMap<>(); HashMap<HousenumberIvl, List<HousenumberMatch>> housesToAdd = new LinkedHashMap<>(); for (HousenumberRoad hnr : roadsInCluster){ for (HousenumberMatch house : hnr.getHouses()) existingNumbers.put(house.getHousenumber(), house); } int inCluster = 0; boolean allOK = true; // for loop may change the list for (int i = 0; i < interpolationInfos.size(); i++){ HousenumberIvl hivl = interpolationInfos.get(i); if (hivl.inCluster(housesWithIvlInfo) == false || hivl.ignoreForInterpolation()) continue; ++inCluster; String hivlDesc = hivl.getDesc(); HousenumberIvl hivlTest = simpleDupCheckSet.get(hivlDesc); if (hivlTest != null){ // happens often in Canada (CanVec imports): two or more addr:interpolation ways with similar meaning // sometimes at completely different road parts, sometimes at exactly the same log.warn("found additional addr:interpolation way with same meaning, is ignored:",streetName, hivl, hivlTest); badIvls.add(hivl); allOK = false; continue; } simpleDupCheckSet.put(hivlDesc, hivl); id2IvlMap.put(hivl.getId(), hivl); List<HousenumberMatch> interpolatedHouses = hivl.getInterpolatedHouses(); if (interpolatedHouses.isEmpty() == false){ if (interpolatedHouses.get(0).getRoad() == null){ // the interpolated houses are not all along one road findRoadForInterpolatedHouses(streetName, interpolatedHouses, roadsInCluster); } int dupCount = 0; for (HousenumberMatch house : interpolatedHouses){ if (house.getRoad() == null || house.getDistance() > HousenumberIvl.MAX_INTERPOLATION_DISTANCE_TO_ROAD) continue; boolean ignoreGenOnly = false; HousenumberMatch old = interpolatedNumbers.get(house.getHousenumber()); if (old == null){ ignoreGenOnly = true; old = existingNumbers.get(house.getHousenumber()); } if (old != null){ // try to build new intervals using existing node HousenumberIvl[] splitIvls = hivl.trySplitAt(old); if (splitIvls != null){ log.info("adding address",streetName,old,old.toBrowseURL(),"to addr:interpolation way, replacing", hivl,"by",Arrays.deepToString(splitIvls)); interpolationInfos.add(splitIvls[0]); interpolationInfos.add(splitIvls[1]); hivl.setIgnoreForInterpolation(true); break; } // forget both or only one ? Which one? house.setIgnored(true); double distToOld = old.getLocation().distance(house.getLocation()); if (distToOld > MAX_DISTANCE_SAME_NUM){ if (old.isInterpolated()) log.info("conflict caused by addr:interpolation way",streetName,hivl,"and interpolated address",old,"at",old.getLocation().toDegreeString()); else log.info("conflict caused by addr:interpolation way",streetName,hivl,"and address element",old,"at",old.getLocation().toDegreeString()); dupCount++; if (!ignoreGenOnly){ old.setIgnored(true); long ivlId = old.getElement().getOriginalId(); HousenumberIvl bad = id2IvlMap.get(ivlId); if (bad != null) badIvls.add(bad); } } } } if (hivl.ignoreForInterpolation()) continue; if (dupCount > 0){ log.warn("addr:interpolation way",streetName,hivl,"is ignored, it produces",dupCount,"duplicate number(s) too far from existing nodes"); badIvls.add(hivl); } else { housesToAdd.put(hivl, interpolatedHouses); for (HousenumberMatch hnm : interpolatedHouses) interpolatedNumbers.put(hnm.getHousenumber(), hnm); } } } if (inCluster == 0) return; for (HousenumberIvl badIvl: badIvls){ allOK = false; badIvl.ignoreNodes(); housesToAdd.remove(badIvl); } Iterator<Entry<HousenumberIvl, List<HousenumberMatch>>> iter = housesToAdd.entrySet().iterator(); while (iter.hasNext()){ Entry<HousenumberIvl, List<HousenumberMatch>> entry = iter.next(); if (log.isInfoEnabled()) log.info("using generated house numbers from addr:interpolation way",entry.getKey()); for (HousenumberMatch house : entry.getValue()){ if (house.getRoad() != null && house.isIgnored() == false){ HousenumberRoad hnr = road2HousenumberRoadMap.get(house.getRoad()); if (hnr == null){ log.error("internal error: found no housenumber road for interpolated house",house.toBrowseURL()); continue; } hnr.addHouse(house); } } } if (log.isDebugEnabled()){ if (allOK) log.debug("found no problems with interpolated numbers from addr:interpolations ways for roads with name",streetName); else log.debug("found problems with interpolated numbers from addr:interpolations ways for roads with name",streetName); } } private static void findRoadForInterpolatedHouses(String streetName, List<HousenumberMatch> houses, List<HousenumberRoad> roadsInCluster) { if (houses.isEmpty()) return; Collections.sort(houses, new HousenumberMatchByNumComparator()); HousenumberMatch prev = null; for (HousenumberMatch house : houses) { if (house.isIgnored()) continue; house.setDistance(Double.POSITIVE_INFINITY); // make sure that we don't use an old match house.setRoad(null); List<HousenumberMatch> matches = new ArrayList<>(); for (HousenumberRoad hnr : roadsInCluster){ MapRoad r = hnr.getRoad(); // make sure that we use the street info if available if (house.getPlace() != null){ if (house.getStreet() != null && r.getStreet() != null && house.getStreet().equals(r.getStreet()) == false) continue; } HousenumberMatch test = new HousenumberMatch(house); findClosestRoadSegment(test, r); if (test.getRoad() != null && test.getGroup() != null || test.getDistance() < MAX_DISTANCE_TO_ROAD){ matches.add(test); } } if (matches.isEmpty()){ house.setIgnored(true); continue; } HousenumberMatch closest, best; best = closest = matches.get(0); if (matches.size() > 1){ // multiple roads, we assume that the closest is the best // but we may have to check the alternatives as well Collections.sort(matches, new HousenumberGenerator.HousenumberMatchByDistComparator()); closest = matches.get(0); best = checkAngle(closest, matches); } house.setDistance(best.getDistance()); house.setSegmentFrac(best.getSegmentFrac()); house.setRoad(best.getRoad()); house.setSegment(best.getSegment()); for (HousenumberMatch altHouse : matches){ if (altHouse.getRoad() != best.getRoad() && altHouse.getDistance() < MAX_DISTANCE_TO_ROAD) house.addAlternativeRoad(altHouse.getRoad()); } if (house.getRoad() == null) { house.setIgnored(true); } else { house.calcRoadSide(); } // plausibility check for duplicate house numbers if (prev != null && prev.getHousenumber() == house.getHousenumber()){ // duplicate number (e.g. 10 and 10 or 10 and 10A or 10A and 10B) if (prev.getSign().equals(house.getSign())){ prev.setDuplicate(true); house.setDuplicate(true); } } if (house.getRoad() == null) { if (house.isIgnored() == false) log.warn("found no plausible road for house number element",house.toBrowseURL(),"(",streetName,house.getSign(),")"); } if (!house.isIgnored()) prev = house; } } private static void markSimpleDuplicates(String streetName, List<HousenumberMatch> housesNearCluster) { List<HousenumberMatch> sortedHouses = new ArrayList<>(housesNearCluster); Collections.sort(sortedHouses, new HousenumberMatchByNumComparator()); int n = sortedHouses.size(); for (int i = 1; i < n; i++){ HousenumberMatch house1 = sortedHouses.get(i-1); if (house1.isIgnored()) continue; HousenumberMatch house2 = sortedHouses.get(i); if (house2.isIgnored()) continue; if (house1.getHousenumber() != house2.getHousenumber()) continue; if (house1.getRoad() == house2.getRoad()){ if (house1.isFarDuplicate()) house2.setFarDuplicate(true); continue; // handled later } // we have two equal house numbers in different roads // check if they should be treated alike boolean markFarDup = false; double dist = house1.getLocation().distance(house2.getLocation()); if (dist > MAX_DISTANCE_SAME_NUM) markFarDup = true; else { CityInfo city1 = house1.getCityInfo(); CityInfo city2 = house2.getCityInfo(); if (city1 != null && city1.equals(city2) == false){ markFarDup = true; } } if (markFarDup){ if (log.isDebugEnabled()) log.debug("keeping duplicate numbers assigned to different roads in cluster ", streetName, house1,house2); house1.setFarDuplicate(true); house2.setFarDuplicate(true); continue; } boolean ignore2nd = false; if (dist < 30){ ignore2nd = true; } else { Coord c1s = house1.getRoad().getPoints().get(house1.getSegment()); Coord c1e = house1.getRoad().getPoints().get(house1.getSegment() + 1); Coord c2s = house2.getRoad().getPoints().get(house2.getSegment()); Coord c2e = house2.getRoad().getPoints().get(house2.getSegment() + 1); if (c1s == c2s || c1s == c2e || c1e == c2s || c1e == c2e){ // roads are directly connected ignore2nd = true; } } if (ignore2nd){ house2.setIgnored(true); if (log.isDebugEnabled()){ if (house1.getSign().equals(house2.getSign())) log.debug("duplicate number is ignored",streetName,house2.getSign(),house2.toBrowseURL() ); else log.info("using",streetName,house1.getSign(), "in favor of",house2.getSign(),"as target for address search"); } } else { if (log.isDebugEnabled()) log.debug("keeping duplicate numbers assigned to different roads in cluster ", streetName, house1,house2); house1.setFarDuplicate(true); house2.setFarDuplicate(true); } } } public static void findClosestRoadSegment(HousenumberMatch house, MapRoad r) { findClosestRoadSegment(house, r, 0, r.getPoints().size()); } /** * Fill/overwrite the fields in house which depend on the assigned road. */ public static void findClosestRoadSegment(HousenumberMatch house, MapRoad r, int firstSeg, int stopSeg) { Coord cx = house.getLocation(); double oldDist = house.getDistance(); MapRoad oldRoad = house.getRoad(); house.setRoad(null); house.setDistance(Double.POSITIVE_INFINITY); boolean foundGroupLink = false; int end = Math.min(r.getPoints().size(), stopSeg+1); for (int node = firstSeg; node + 1 < end; node++){ Coord c1 = r.getPoints().get(node); Coord c2 = r.getPoints().get(node + 1); double frac = getFrac(c1, c2, cx); double dist = distanceToSegment(c1,c2,cx,frac); if (house.getGroup() != null && house.getGroup().linkNode == c1){ if (c1.highPrecEquals(c2) == false){ log.debug("block doesn't have zero length segment! Road:",r,house); } foundGroupLink = true; house.setDistance(dist); house.setSegmentFrac(frac); house.setRoad(r); house.setSegment(node); break; } else if (dist < house.getDistance()) { house.setDistance(dist); house.setSegmentFrac(frac); house.setRoad(r); house.setSegment(node); } } if (house.getGroup() != null && house.getGroup().linkNode != null && foundGroupLink == false){ log.debug(r,house,"has a group but the link was not found, should only happen after split of zero-length-segment"); } if (oldRoad == r){ if (house.getDistance() > MAX_DISTANCE_TO_ROAD + 2.5 && oldDist <= MAX_DISTANCE_TO_ROAD ){ log.warn("line distorted? Road segment was moved by more than", String.format("%.2f m", 2.5), ", from address", r, house.getSign()); } } } private static boolean hasChanges( List<HousenumberRoad> housenumberRoads) { for (HousenumberRoad hnr : housenumberRoads){ if (hnr.isChanged()) return true; } return false; } /** * * @param housenumberRoads */ private static void checkWrongRoadAssignmments(List<HousenumberRoad> housenumberRoads) { if (housenumberRoads.size() < 2) return; for (int loop = 0; loop < 10; loop++){ boolean changed = false; for (int i = 0; i+1 < housenumberRoads.size(); i++){ HousenumberRoad hnr1 = housenumberRoads.get(i); hnr1.setChanged(false); for (int j = i+1; j < housenumberRoads.size(); j++){ HousenumberRoad hnr2 = housenumberRoads.get(j); hnr2.setChanged(false); hnr1.checkWrongRoadAssignmments(hnr2); if (hnr1.isChanged()){ changed = true; hnr1.checkIntervals(); } if (hnr2.isChanged()){ changed = true; hnr2.checkIntervals(); } } } if (!changed) return; } } /** * Sorts house numbers by roads, road segments and position of the house number. * @author WanMil */ public static class HousenumberMatchByPosComparator implements Comparator<HousenumberMatch> { public int compare(HousenumberMatch o1, HousenumberMatch o2) { if (o1 == o2) { return 0; } if (o1.getRoad() == null || o2.getRoad() == null){ log.error("road is null in sort comparator",o1,o2); throw new MapFailedException("internal error in housenumber processing"); } if (o1.getRoad() != o2.getRoad()) { // should not happen return o1.getRoad().getRoadId() - o2.getRoad().getRoadId(); } int dSegment = o1.getSegment() - o2.getSegment(); if (dSegment != 0) { return dSegment; } double dFrac = o1.getSegmentFrac() - o2.getSegmentFrac(); if (dFrac != 0d) { return (int)Math.signum(dFrac); } int d = o1.getHousenumber() - o2.getHousenumber(); if (d != 0) return d; d = o1.getSign().compareTo(o2.getSign()); if (d != 0) return d; return 0; } } /** * Sorts house numbers by house number and segment * @author Gerd Petermann */ public static class HousenumberMatchByNumComparator implements Comparator<HousenumberMatch> { public int compare(HousenumberMatch o1, HousenumberMatch o2) { if (o1 == o2) return 0; int d = o1.getHousenumber() - o2.getHousenumber(); if (d != 0) return d; d = o1.getSign().compareTo(o2.getSign()); if (d != 0) return d; d = o1.getSegment() - o2.getSegment(); if (d != 0) return d; double dDist = o1.getDistance() - o2.getDistance(); if (dDist != 0d) { return (int)Math.signum(dDist); } if (d != 0) return d; d = Long.compare(o1.getElement().getId(), o2.getElement().getId()); return d; } } /** * Sorts house numbers by distance. If eqaul, compare segment and road to produce * predictable results. * @author Gerd Petermann */ public static class HousenumberMatchByDistComparator implements Comparator<HousenumberMatch> { public int compare(HousenumberMatch o1, HousenumberMatch o2) { if (o1 == o2) return 0; int d = Double.compare(o1.getDistance(), o2.getDistance()); if (d != 0) return d; d = Integer.compare(o1.getSegment(), o2.getSegment()); if (d != 0) return d; d = Integer.compare(o1.getRoad().getRoadId(), o2.getRoad().getRoadId()); if (d != 0) return d; return 0; } } /** * If the closest point to a road is a junction, try to find the road * segment that forms a right angle with the house * @param closestMatch one match that is closest to the node * @param otherMatches the other matches with the same distance * @return the best match */ private static HousenumberMatch checkAngle(HousenumberMatch closestMatch, List<HousenumberMatch> otherMatches) { if (otherMatches.isEmpty()) return closestMatch; HousenumberMatch bestMatch = closestMatch; for (HousenumberMatch alternative : otherMatches){ if (alternative == closestMatch) continue; if (closestMatch.getDistance() < alternative.getDistance()) break; // a house has the same distance to different road objects // if this happens at a T-junction, make sure not to use the end of the wrong road Coord c1 = closestMatch.getRoad().getPoints().get(closestMatch.getSegment()); Coord c2 = closestMatch.getRoad().getPoints().get(closestMatch.getSegment()+1); Coord cx = closestMatch.getLocation(); double dist = closestMatch.getDistance(); double dist1 = cx.distance(c1); double angle, altAngle; if (dist1 == dist) angle = Utils.getAngle(c2, c1, cx); else angle = Utils.getAngle(c1, c2, cx); Coord c3 = alternative.getRoad().getPoints().get(alternative.getSegment()); Coord c4 = alternative.getRoad().getPoints().get(alternative.getSegment()+1); double dist3 = cx.distance(c3); if (dist3 == dist) altAngle = Utils.getAngle(c4, c3, cx); else altAngle = Utils.getAngle(c3, c4, cx); double delta = 90 - Math.abs(angle); double deltaAlt = 90 - Math.abs(altAngle); if (delta > deltaAlt){ bestMatch = alternative; c1 = c3; c2 = c4; } } if (log.isDebugEnabled()){ if (closestMatch.getRoad() != bestMatch.getRoad()){ log.debug("check angle: using road",bestMatch.getRoad().getRoadDef().getId(),"instead of",closestMatch.getRoad().getRoadDef().getId(),"for house number",bestMatch.getSign(),bestMatch.toBrowseURL()); } else if (closestMatch != bestMatch){ log.debug("check angle: using road segment",bestMatch.getSegment(),"instead of",closestMatch.getSegment(),"for house number element",bestMatch.toBrowseURL()); } } return bestMatch; } /** * Evaluates if the given point lies on the left side of the line spanned by spoint1 and spoint2. * @param spoint1 first point of line * @param spoint2 second point of line * @param point the point to check * @return {@code true} point lies on the left side; {@code false} point lies on the right side */ public static boolean isLeft(Coord spoint1, Coord spoint2, Coord point) { if (spoint1.distance(spoint2) == 0){ log.warn("road segment length is 0 in left/right evaluation"); } return ((spoint2.getHighPrecLon() - spoint1.getHighPrecLon()) * (point.getHighPrecLat() - spoint1.getHighPrecLat()) - (spoint2 .getHighPrecLat() - spoint1.getHighPrecLat()) * (point.getHighPrecLon() - spoint1.getHighPrecLon())) > 0; } /** * Calculates the distance to the given segment in meter. * @param spoint1 segment point 1 * @param spoint2 segment point 2 * @param point point * @return the distance in meter */ public static double distanceToSegment(Coord spoint1, Coord spoint2, Coord point, double frac) { if (frac <= 0) { return spoint1.distance(point); } else if (frac >= 1) { return spoint2.distance(point); } else { return point.distToLineSegment(spoint1, spoint2); } } /** * Calculates the fraction at which the given point is closest to * the infinite line going through both points. * @param spoint1 segment point 1 * @param spoint2 segment point 2 * @param point point * @return the fraction (can be <= 0 or >= 1 if the perpendicular is not on * the line segment between spoint1 and spoint2) */ public static double getFrac(Coord spoint1, Coord spoint2, Coord point) { int aLon = spoint1.getHighPrecLon(); int bLon = spoint2.getHighPrecLon(); int pLon = point.getHighPrecLon(); int aLat = spoint1.getHighPrecLat(); int bLat = spoint2.getHighPrecLat(); int pLat = point.getHighPrecLat(); double deltaLon = bLon - aLon; double deltaLat = bLat - aLat; if (deltaLon == 0 && deltaLat == 0) return 0; else { // scale for longitude deltas by cosine of average latitude double scale = Math.cos(Coord.int30ToRadians((aLat + bLat + pLat) / 3) ); double deltaLonAP = scale * (pLon - aLon); deltaLon = scale * deltaLon; if (deltaLon == 0 && deltaLat == 0) return 0; else return (deltaLonAP * deltaLon + (pLat - aLat) * deltaLat) / (deltaLon * deltaLon + deltaLat * deltaLat); } } /** * @param length * @return string with length, e.g. "0.23 m" or "116.12 m" */ public static String formatLen(double length){ return String.format("%.2f m", length); } private static class RoadPoint implements Locatable{ final Coord p; final MapRoad r; final int segment; final int partOfSeg; public RoadPoint(MapRoad road, Coord co, int s, int part) { this.p = co; this.r = road; this.segment = s; this.partOfSeg = part; } @Override public Coord getLocation() { return p; } @Override public String toString() { return r + " " + segment + " " + partOfSeg; } } /** * A performance critical part: * Index all road segments to be able to find all road segments within a given range * around a point. * @author Gerd Petermann * */ class RoadSegmentIndex { private final KdTree<RoadPoint> kdTree = new KdTree<>(); private final Int2ObjectOpenHashMap<Set<RoadPoint>> nodeId2RoadPointMap = new Int2ObjectOpenHashMap<>(); private final double range; private final double maxSegmentLength; private final double kdSearchRange; public RoadSegmentIndex(List<MapRoad> roads, double rangeInMeter) { this.range = rangeInMeter; this.maxSegmentLength = range * 2 / 3; this.kdSearchRange = Math.sqrt(Math.pow(rangeInMeter, 2) + Math.pow(maxSegmentLength/2, 2)); build(roads); } public void build(List<MapRoad> roads){ for (MapRoad road : roads){ if (road.isSkipHousenumberProcessing()) continue; List<Coord> points = road.getPoints(); if (points.size() < 2) continue; List<RoadPoint> roadPoints = new ArrayList<>(); RoadPoint rp; for (int i = 0; i + 1 < points.size(); i++){ Coord c1 = points.get(i); Coord c2 = points.get(i + 1); int part = 0; rp = new RoadPoint(road, c1, i, part++); roadPoints.add(rp); while (true){ double segLen = c1.distance(c2); double frac = maxSegmentLength / segLen; if (frac >= 1) break; // if points are not close enough, add extra point c1 = c1.makeBetweenPoint(c2, frac); rp = new RoadPoint(road, c1, i, part++); roadPoints.add(rp); segLen -= maxSegmentLength; } } int last = points.size() - 1; rp = new RoadPoint(road, points.get(last) , last, -1); roadPoints.add(rp); Collections.shuffle(roadPoints); for (RoadPoint toAdd : roadPoints){ int id = toAdd.p.getId(); if (id == 0) kdTree.add(toAdd); else { // Coord node, add only once to KD-tree with all roads Set<RoadPoint> set = nodeId2RoadPointMap.get(id); if (set == null){ set = new LinkedHashSet<>(); nodeId2RoadPointMap.put(id, set); kdTree.add(toAdd); } set.add(toAdd); } } } } public List<RoadPoint> getCLoseRoadPoints(HousenumberElem house){ Set<RoadPoint> closeRoadPoints = kdTree.findNextPoint(house, kdSearchRange); List<RoadPoint> result = new ArrayList<>(); for (RoadPoint rp : closeRoadPoints){ int id = rp.p.getId(); if (id != 0) result.addAll(nodeId2RoadPointMap.get(id)); else result.add(rp); } return result; } /** * Find closest road segment and other plausible roads for a house * @param house * @return null if no road was found, else a {@link HousenumberMatch} instance */ public HousenumberMatch createHousenumberMatch(HousenumberElem house){ HousenumberMatch closest = new HousenumberMatch(house); List<RoadPoint> closeRoadPoints = getCLoseRoadPoints(house); if (closeRoadPoints.isEmpty()) return closest; Collections.sort(closeRoadPoints, new Comparator<RoadPoint>() { // sort by distance (smallest first) public int compare(RoadPoint o1, RoadPoint o2) { if (o1 == o2) return 0; int d = Integer.compare(o1.r.getRoadId(), o2.r.getRoadId()); if (d != 0) return d; d = Integer.compare(o1.segment, o2.segment); if (d != 0) return d; return Integer.compare(o1.partOfSeg, o2.partOfSeg); } }); List<HousenumberMatch> matches = new ArrayList<>(40); BitSet testedSegments = new BitSet(); MapRoad lastRoad = null; HousenumberMatch hnm = null; for (RoadPoint rp : closeRoadPoints){ if (house.getStreet() != null){ // we have a given street name, accept only roads with similar name or no name if (rp.r.getStreet() != null && house.getStreet().equalsIgnoreCase(rp.r.getStreet()) == false) continue; } if (rp.r != lastRoad){ hnm = new HousenumberMatch(house); testedSegments.clear(); matches.add(hnm); lastRoad = rp.r; } double oldDist = hnm.getDistance(); if (rp.partOfSeg >= 0){ // rp.p is at start or before end of segment if (testedSegments.get(rp.segment) == false){ testedSegments.set(rp.segment); checkSegment(hnm, rp.r, rp.segment); } } if (rp.partOfSeg < 0){ // rp is at end of road, check (also) the preceding segment if (rp.segment < 1){ log.error("internal error: trying to use invalid roadPoint",rp); } else if (testedSegments.get(rp.segment - 1) == false){ testedSegments.set(rp.segment-1); checkSegment(hnm, rp.r, rp.segment-1); } } if (oldDist == hnm.getDistance()) continue; } if (matches.isEmpty()) return closest; // closest has not yet a road Collections.sort(matches, new HousenumberGenerator.HousenumberMatchByDistComparator()); closest = matches.get(0); closest = checkAngle(closest, matches); closest.calcRoadSide(); HousenumberMatch bestMatchingName = null; if (closest.getStreet() != null && closest.getStreet().equalsIgnoreCase(closest.getRoad().getStreet())) bestMatchingName = closest; for (HousenumberMatch altHouse : matches){ if (altHouse.getDistance() >= MAX_DISTANCE_TO_ROAD) break; if (altHouse.getRoad() != closest.getRoad()){ if (house.getStreet() != null && altHouse.getDistance() > closest.getDistance()){ if (house.getStreet().equalsIgnoreCase(altHouse.getRoad().getStreet())){ if (bestMatchingName == null || bestMatchingName.getDistance() > altHouse.getDistance()){ bestMatchingName = altHouse; } } else { if (bestMatchingName != null && altHouse.getDistance() > bestMatchingName.getDistance()) continue; } } closest.addAlternativeRoad(altHouse.getRoad()); } } if (bestMatchingName != null){ if (house.getStreet().equals(bestMatchingName.getRoad().getStreet()) == false){ log.warn("accepting match in spite of different capitalisation" , house.getStreet(),house.getSign(), bestMatchingName.getRoad().getRoadDef(), "house:",house.toBrowseURL()); bestMatchingName.setStreet(bestMatchingName.getRoad().getStreet()); closest.setStreet(bestMatchingName.getStreet()); } } if (closest == bestMatchingName || bestMatchingName == null || bestMatchingName.getDistance() > MAX_DISTANCE_TO_ROAD) return closest; double ratio = closest.getDistance() / bestMatchingName.getDistance(); if (ratio < 0.25) return closest; HousenumberMatch best = closest; if (ratio > 0.75){ // prefer the road with the matching name for (MapRoad r : closest.getAlternativeRoads()){ if (house.getStreet().equalsIgnoreCase(r.getStreet())) bestMatchingName.addAlternativeRoad(r); } best = bestMatchingName; best.calcRoadSide(); } else { if (log.isDebugEnabled()){ log.debug("further checks needed for address", closest.getStreet(), closest.getSign(), closest.toBrowseURL(), formatLen(closest.getDistance()), formatLen(bestMatchingName.getDistance())); } } return best; } } }