/* * Copyright (C) 2015 Gerd Petermann * * 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 java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.CityInfo; import uk.me.parabola.mkgmap.general.MapRoad; import uk.me.parabola.mkgmap.general.ZipCodeInfo; import uk.me.parabola.mkgmap.reader.osm.Node; import uk.me.parabola.mkgmap.reader.osm.TagDict; import uk.me.parabola.mkgmap.reader.osm.Way; /** * Represents a (part of an) addr:interpolation way. * It contains the points between two house number elements * and the information how numbers are interpolated along * the way that is described by these points. * * We try to use the information to find * a) the right road for the houses * b) the right road segment * c) the position of the interpolated houses * * We have to be aware of several special cases so we use some * flags to say for which of the above points the information * can be used. * * @author Gerd Petermann * */ public class HousenumberIvl { private static final Logger log = Logger.getLogger(HousenumberIvl.class); /** Gives the maximum distance between house number element and the matching road * when the number is part of an addr:interpolation way */ public static final double MAX_INTERPOLATION_DISTANCE_TO_ROAD = 75.0; private final String streetName; private final Way interpolationWay; private MapRoad roadForInterpolatedHouses; private final Node n1,n2; private List<Coord> points; private int step, start, end, steps; private HousenumberMatch[] knownHouses = {null, null}; private boolean hasMultipleRoads; private boolean foundCluster; private int interpolated; // counter to detect wrong double use private boolean ignoreForInterpolation; private boolean equalEnds; private static final short streetTagKey = TagDict.getInstance().xlate("mkgmap:street"); private static final short housenumberTagKey = TagDict.getInstance().xlate("mkgmap:housenumber"); private static final short addrInterpolationTagKey = TagDict.getInstance().xlate("addr:interpolation"); public HousenumberIvl(String streetName, Way interpolationWay, Node n1, Node n2) { this.streetName = streetName; this.interpolationWay = interpolationWay; this.n1 = n1; this.n2 = n2; } public void setPoints(List<Coord> points) { this.points = new ArrayList<Coord>(points); } public void setStep(int step) { this.step = step; } public int getStep() { return step; } public void setStart(int start) { this.start = start; } public int getStart() { return start; } public void setEnd(int end) { this.end = end; } public int getEnd() { return end; } public void calcSteps() { if (start < end){ steps = (end - start) / step - 1; } else { steps = (start - end) / step - 1; } } public Node getNode1() { return n1; } public Node getNode2() { return n2; } // public boolean needsSplit(){ // return needsSplit; // } public void addHousenumberMatch(HousenumberMatch house) { if (house.getElement() == n1) knownHouses[0] = house; else if (house.getElement() == n2) knownHouses[1] = house; else { log.error("cannot add",house,"to",this); } } public boolean checkRoads(){ boolean res = checkRoads2(); if (!res || equalEnds){ // the interval is not ok --> ignore the numbers as well ignoreNodes(); } return res; } private boolean checkRoads2(){ for (int i = 0; i < 2; i++){ if (knownHouses[i] == null ){ log.error("internal error: housenumber matches not properly set", this); return false; } if (knownHouses[i].getRoad() == null || knownHouses[i].getDistance() > 100 ){ log.warn("cannot find any reasonable road for both nodes, ignoring them",streetName,this); return false; } } if (knownHouses[0].getRoad().getRoadDef().getId() == knownHouses[1].getRoad().getRoadDef().getId()){ if (knownHouses[0].getRoad() != knownHouses[1].getRoad()){ // special case: interval goes along clipped road, data is probably OK hasMultipleRoads = true; return true; } for (MapRoad r : knownHouses[0].getAlternativeRoads()){ if (r.getRoadDef().getId() == knownHouses[0].getRoad().getRoadDef().getId()){ // special case: interval may go along clipped road, data is probably OK hasMultipleRoads = true; return true; } } } MapRoad bestRoad = null; // make sure that the closest road is one with a matching name for (int i = 0; i < 2; i++){ while (streetName.equals(knownHouses[i].getRoad().getStreet()) == false && knownHouses[i].hasAlternativeRoad()){ HousenumberMatch testx = new HousenumberMatch(knownHouses[i]); MapRoad r = knownHouses[i].getAlternativeRoads().remove(0); if (streetName.equals(r.getStreet())){ HousenumberGenerator.findClosestRoadSegment(testx, r); if (testx.getDistance() < MAX_INTERPOLATION_DISTANCE_TO_ROAD){ copyRoadData(testx, knownHouses[i]); } } } } List<MapRoad> toTest = new ArrayList<>(); toTest.add(knownHouses[0].getRoad()); toTest.add(knownHouses[1].getRoad()); for (MapRoad r : knownHouses[0].getAlternativeRoads()){ if (knownHouses[1].getAlternativeRoads().contains(r)) toTest.add(r); } HousenumberMatch[] test = new HousenumberMatch[2]; HousenumberMatch[] closest = new HousenumberMatch[2]; boolean foundSingleRoad = false; for (MapRoad r : toTest){ if (streetName.equals(r.getStreet()) == false) continue; foundSingleRoad = true; for (int i = 0; i < 2; i++){ test[i] = knownHouses[i]; if (test[i].getRoad() != r){ test[i] = new HousenumberMatch(knownHouses[i]); HousenumberGenerator.findClosestRoadSegment(test[i], r); test[i].calcRoadSide(); } if (test[i].getRoad() == null || test[i].getDistance() > MAX_INTERPOLATION_DISTANCE_TO_ROAD ){ foundSingleRoad = false; break; } } if (foundSingleRoad){ if (test[0].isLeft() != test[1].isLeft()){ foundSingleRoad = false; continue; } int s0 = test[0].getSegment(); int s1 = test[1].getSegment(); // check if the road and the addr:interpolation way are nearly parallel lines double angle1 = Utils.getAngle(test[0].getClosestPointOnRoad(), points.get(0),points.get(1)); if (Math.abs(angle1) < 30){ foundSingleRoad = false; HousenumberMatch testx = new HousenumberMatch(test[0]); for (int s = Math.min(s0,s1); s <= Math.max(s0, s1); s++){ if (s != test[0].getSegment()){ HousenumberGenerator.findClosestRoadSegment(testx, r, s,s+1); angle1 = Utils.getAngle(testx.getClosestPointOnRoad(), points.get(0),points.get(1)); if (Math.abs(angle1) >= 30 && testx.getDistance() < 2*test[0].getDistance()){ test[0] = testx; foundSingleRoad = true; break; } } } } double angle2 = Utils.getAngle(points.get(points.size()-2),points.get(points.size()-1),test[1].getClosestPointOnRoad()); if (Math.abs(angle2) < 30){ foundSingleRoad = false; HousenumberMatch testx = new HousenumberMatch(test[1]); for (int s = Math.min(s0,s1); s <= Math.max(s0, s1); s++){ if (s != test[1].getSegment()){ HousenumberGenerator.findClosestRoadSegment(testx, r, s,s+1); angle2 = Utils.getAngle(points.get(points.size()-2),points.get(points.size()-1),testx.getClosestPointOnRoad()); if (Math.abs(angle2) >= 30 && testx.getDistance() < 2*test[1].getDistance()){ test[1] = testx; foundSingleRoad = true; break; } } } } } if (foundSingleRoad){ if (r.isNamedByHousenumberProcessing() == false) break; // the closest road was originally unnamed , try to find one that is named in OSM if (bestRoad == null){ bestRoad = r; closest[0] = test[0]; closest[1] = test[1]; } } } if (!foundSingleRoad && bestRoad != null){ // not matching road name in original OSM data, use the closest foundSingleRoad = true; test[0] = closest[0]; test[1] = closest[1]; } if (!foundSingleRoad){ if (streetName.equals(knownHouses[0].getRoad().getStreet()) == false || streetName.equals(knownHouses[1].getRoad().getStreet()) == false){ log.warn("cannot find reasonable road for both nodes",streetName,this); return false; } hasMultipleRoads = true; return true; } // we found the road that should be used for interpolation roadForInterpolatedHouses = test[0].getRoad(); // we found a single plausible road, make sure that both nodes are using it for (int i = 0; i < 2; i++){ if (knownHouses[i].getRoad() != test[i].getRoad() || knownHouses[i].getSegment() != test[i].getSegment()){ copyRoadData(test[i], knownHouses[i]); knownHouses[i].forgetAlternativeRoads(); } if (knownHouses[i].getSegmentFrac() < 0 || knownHouses[i].getSegmentFrac() > 1){ hasMultipleRoads = true; } } if (knownHouses[0].isLeft() != knownHouses[1].isLeft()){ log.warn("addr:interpolation way crosses road",streetName,this); return false; } return true; } private void copyRoadData(HousenumberMatch source, HousenumberMatch dest) { if (log.isInfoEnabled()){ if (source.getRoad() != dest.getRoad()) log.info("moving",streetName,dest.getSign(),dest.toBrowseURL(),"from road",dest.getRoad(),"to road",source.getRoad()); else log.info("moving",streetName,dest.getSign(),dest.toBrowseURL(),"from segment",dest.getSegment(),"to ",source.getSegment(),"in road",source.getRoad()); } dest.setRoad(source.getRoad()); dest.setSegment(source.getSegment()); dest.setSegmentFrac(source.getSegmentFrac()); dest.setDistance(source.getDistance()); dest.calcRoadSide(); } public List<HousenumberMatch> getInterpolatedHouses(){ List<HousenumberMatch> houses = new ArrayList<>(); if (ignoreForInterpolation|| start == end || steps <= 0) return houses; List<Coord> interpolatedPoints = getInterpolatedPoints(); int usedStep = (start < end) ? step : -step; int hn = start; boolean distanceWarningIssued = false; CityInfo ci = knownHouses[0].getCityInfo(); ZipCodeInfo zip = knownHouses[0].getZipCode(); if (ci != null && ci.equals(knownHouses[1].getCityInfo()) == false) log.warn("addr:interpolation way connects houses in different cities",streetName,this,"using city",ci,"for all interpolated adresses"); if (zip != null && zip.equals(knownHouses[1].getZipCode()) == false) log.warn("addr:interpolation way connects houses with differnt zip codes",streetName,this,"using zip code",zip,"for all interpolated adresses"); for (Coord co : interpolatedPoints){ hn += usedStep; Node generated = new Node(interpolationWay.getId(), co); generated.setFakeId(); generated.addTag(streetTagKey, streetName); String number = String.valueOf(hn); generated.addTag(housenumberTagKey, number); // TODO: maybe add check that city info and zip code of both houses is equal ? // what if not ? HousenumberElem houseElem = new HousenumberElem(generated, ci); houseElem.setHousenumber(hn); houseElem.setZipCode(zip); houseElem.setStreet(streetName); houseElem.setSign(number); HousenumberMatch house = new HousenumberMatch(houseElem); if (roadForInterpolatedHouses != null){ HousenumberGenerator.findClosestRoadSegment(house, roadForInterpolatedHouses); if (house.getRoad() == null || house.getDistance() > MAX_INTERPOLATION_DISTANCE_TO_ROAD ){ if (distanceWarningIssued == false){ log.warn("interpolated house is not close to expected road",this,house); distanceWarningIssued = true; } continue; } house.calcRoadSide(); } house.setInterpolated(true); houses.add(house); } if (log.isDebugEnabled()){ String addrInterpolationMethod = interpolationWay.getTag(addrInterpolationTagKey); if (hasMultipleRoads == false) log.debug(this,"generated",addrInterpolationMethod,"interpolated number(s) for",knownHouses[0].getRoad()); else log.debug(this,"generated",addrInterpolationMethod,"interpolated number(s) for",streetName); } return houses; } /** * Calculate the wanted number of coords on a way so that they have * similar distances to each other (and to the first and last point * of the way). * @param points list of points that build the way * @param num the wanted number * @return a list with the number of points or the empty list in * case of errors */ public List<Coord> getInterpolatedPoints(){ if (interpolated > 0){ log.debug("interpolating numbers again for", this ); } interpolated++; if (steps < 1 || points.size() < 2) return Collections.emptyList(); List<Coord> interpolated = new ArrayList<>(steps); double wayLen = 0; for (int i = 0; i+1 < points.size(); i++){ wayLen += points.get(i).distance(points.get(i+1)); } double ivlLen = wayLen / (steps+1); if (ivlLen < 0.1){ if (log.isInfoEnabled()) log.info("addr:interpolation",interpolationWay.toBrowseURL(),"segment ignored, would generate",steps,"houses with distance of",ivlLen,"m"); return interpolated; } int pos = 0; double rest = 0; while (pos+1 < points.size()){ Coord c1 = points.get(pos); Coord c2 = points.get(pos+1); pos++; double neededPartOfSegment = 0; double segmentLen = c1.distance(c2); for(;;){ neededPartOfSegment += ivlLen - rest; if (neededPartOfSegment <= segmentLen){ double fraction = neededPartOfSegment / segmentLen; Coord c = c1.makeBetweenPoint(c2, fraction); interpolated.add(c); if (interpolated.size() >= steps){ // GpxCreator.createGpx("e:/ld/road", knownHouses[0].getRoad().getPoints()); // GpxCreator.createGpx("e:/ld/test", interpolated, Arrays.asList(points.get(0),points.get(points.size()-1))); return interpolated; } rest = 0; } else { rest = segmentLen - neededPartOfSegment + ivlLen; break; } } } log.warn("addr:interpolation",interpolationWay.toBrowseURL(),"interpolation for segment with nodes",n1.getId(),n2.getId(),"failed"); return interpolated; } public String toString() { return interpolationWay.toBrowseURL() + " " + start + ".." + end + ", step=" + step; } public String getDesc() { return streetName + "_" + start + ".." + end + "_" + step; } // public boolean setNodeRefs(HashMap<Element, HousenumberMatch> houses) { // knownHouses[0] = houses.get(n1); // knownHouses[1] = houses.get(n2); // if (knownHouses[0] == null || knownHouses[1] == null) // return false; // knownHouses[0].incIntervalInfoRefs(); // knownHouses[1].incIntervalInfoRefs(); // return true; // } // public void ignoreNodes() { for (int i = 0; i < 2; i++){ if (knownHouses[i] != null){ knownHouses[i].decIntervalInfoRefs(); if (knownHouses[i].getIntervalInfoRefs() == 0) knownHouses[i].setIgnored(true); } } } public long getId() { return interpolationWay.getId(); } public boolean ignoreForInterpolation() { return ignoreForInterpolation; } public void setIgnoreForInterpolation(boolean ignoreForInterpolation) { this.ignoreForInterpolation = ignoreForInterpolation; } public boolean isBad() { return false; } public boolean inCluster(List<HousenumberMatch> housesNearCluster) { int count = 0; for (HousenumberMatch house : housesNearCluster){ if (knownHouses[0] == house || knownHouses[1] == house){ ++count; } if (count == 2) break; } if (count > 0){ foundCluster = true; return true; } return false; } public boolean foundCluster() { return foundCluster; } public void setEqualEnds() { this.equalEnds = true; } public boolean setNodeRefs(Map<Long, Integer> interpolationNodes, List<HousenumberElem> houseElems) { for (int i = 0; i < 2; i++){ long id = (i == 0) ? n1.getId(): n2.getId(); Integer elemPos = interpolationNodes.get(id); if (elemPos == null || elemPos >= houseElems.size()) return false; HousenumberElem he = houseElems.get(elemPos); if (he instanceof HousenumberMatch == false) return false; if (he.getElement().getId() != id) return false; knownHouses[i] = (HousenumberMatch) he; knownHouses[i].incIntervalInfoRefs(); } return true; } public HousenumberMatch[] getHouseNodes (){ return knownHouses; } /** * Check if an address node can be used to calculate new intervals * @param houseToAdd a single address from OSM data * @return null in case of error or an array with two new {@link HousenumberIvl} instances */ public HousenumberIvl[] trySplitAt(HousenumberMatch houseToAdd) { if (houseToAdd.isInterpolated()) return null; if (houseToAdd.getRoad() != knownHouses[0].getRoad() && houseToAdd.getRoad() != knownHouses[1].getRoad()) return null; HousenumberMatch s = knownHouses[0]; HousenumberMatch e = knownHouses[1]; if (s.getSegment() > e.getSegment() || s.getSegment() == e.getSegment() && s.getSegmentFrac() > e.getSegmentFrac()){ s = knownHouses[1]; e = knownHouses[0]; } if (houseToAdd.getSegment() < s.getSegment() || houseToAdd.getSegment() > e.getSegment()) return null; if (houseToAdd.getSegment() == s.getSegment() && houseToAdd.getSegmentFrac() < s.getSegmentFrac()) return null; if (houseToAdd.getSegment() == e.getSegment() && houseToAdd.getSegmentFrac() > e.getSegmentFrac()) return null; for (int i = 0; i+1 < points.size(); i++){ Coord c1 = points.get(i); Coord c2 = points.get(i + 1); double frac = HousenumberGenerator.getFrac(c1, c2, houseToAdd.getLocation()); if (frac < 0 || frac > 1) continue; HousenumberIvl[] ivls = new HousenumberIvl[2]; HousenumberMatch hnm = null; if (houseToAdd.element instanceof Node){ hnm = houseToAdd; } else { // create a Node instance Node toAdd = new Node(houseToAdd.getElement().getId(), houseToAdd.getLocation()); toAdd.setFakeId(); toAdd.copyTags(houseToAdd.element); HousenumberElem hnElem = new HousenumberElem(toAdd, houseToAdd.getCityInfo()); hnm = new HousenumberMatch(hnElem); hnm.setZipCode(houseToAdd.getZipCode()); HousenumberGenerator.findClosestRoadSegment(hnm, houseToAdd.getRoad(), houseToAdd.getSegment(), houseToAdd.getSegment()); } List<Coord> points1 = new ArrayList<>(); List<Coord> points2 = new ArrayList<>(); points1.addAll(points.subList(0, i+1)); points1.add(houseToAdd.getLocation()); points2.add(houseToAdd.getLocation()); points2.addAll(points.subList(i+1, points.size())); ivls[0] = new HousenumberIvl(streetName, interpolationWay, n1, (Node)hnm.element); ivls[0].setStart(knownHouses[0].getHousenumber()); ivls[0].setEnd(houseToAdd.getHousenumber()); ivls[0].setStep(step); ivls[0].calcSteps(); ivls[0].setPoints(points1); ivls[0].addHousenumberMatch(knownHouses[0]); ivls[0].addHousenumberMatch(hnm); ivls[1] = new HousenumberIvl(streetName, interpolationWay, (Node) hnm.element, n2); ivls[1].setStart(houseToAdd.getHousenumber()); ivls[1].setEnd(knownHouses[1].getHousenumber()); ivls[1].setStep(step); ivls[1].calcSteps(); ivls[1].setPoints(points2); ivls[1].addHousenumberMatch(knownHouses[1]); ivls[1].addHousenumberMatch(hnm); return ivls; } return null; } }