/* * 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 it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.filters.LineSplitterFilter; import uk.me.parabola.mkgmap.general.MapRoad; /** * Combine two or more HousenumberMatch instances that * can be found at the same point on the road * (with a tolerance of a few meters). * We will create a single Numbers instance for them. * * @author Gerd Petermann * */ public class HousenumberGroup { private static final Logger log = Logger.getLogger(HousenumberGroup.class); final HousenumberRoad hnr; final List<HousenumberMatch> houses = new ArrayList<>(); Int2IntOpenHashMap usedNumbers; int minNum, maxNum; int minSeg , maxSeg; double minFrac, maxFrac; HousenumberMatch closestHouseToRoad ; HousenumberMatch farthestHouseToRoad; int odd,even; Coord linkNode; boolean findSegmentWasCalled; public HousenumberGroup(HousenumberRoad hnr, List<HousenumberMatch> housesToUse) { this.hnr = hnr; reset(); for (HousenumberMatch house : housesToUse){ addHouse(house); } } private void addHouse(HousenumberMatch house){ int num = house.getHousenumber(); if (num % 2 == 0) ++even; else ++odd; int count = usedNumbers.get(num); usedNumbers.put(num, count + 1); if (houses.isEmpty()){ minNum = maxNum = house.getHousenumber(); minSeg = maxSeg = house.getSegment(); minFrac = maxFrac = house.getSegmentFrac(); closestHouseToRoad = farthestHouseToRoad = house; } else { if (house.getSegment() < minSeg){ minSeg = house.getSegment(); minFrac = house.getSegmentFrac(); } else if (house.getSegment() > maxSeg){ maxSeg = house.getSegment(); maxFrac = house.getSegmentFrac(); } else if (house.getSegment() == minSeg ){ minFrac = Math.min(minFrac, house.getSegmentFrac()); } else if (house.getSegment() == maxSeg ){ maxFrac = Math.max(maxFrac, house.getSegmentFrac()); } minNum = Math.min(minNum, num); maxNum = Math.max(maxNum, num); if (house.getDistance() < closestHouseToRoad.getDistance()) closestHouseToRoad = house; if (house.getDistance() > farthestHouseToRoad.getDistance()) farthestHouseToRoad = house; } houses.add(house); } private static final double MIN_DISTANCE_TO_EXISTING_POINT = 7.5; /** * find place for the group, change to or add number nodes * @param nodesForLinks * @return true if one or two nodes were added */ public boolean findSegment(String streetName, List<HousenumberGroup> groups){ if (minSeg < 0 || maxSeg < 0){ log.error("internal error: group is not valid:",this); return false; } findSegmentWasCalled = true; linkNode = null; List<Coord> points = getRoad().getPoints(); Coord pointToUse = null; int seg = closestHouseToRoad.getSegment(); Coord c1 = points.get(seg); Coord c2 = points.get(seg + 1); if (c1.highPrecEquals(c2)){ boolean useExisting = true; // already a zero length segment for (HousenumberGroup hg : groups){ if (hg == this) continue; if (hg.linkNode == c1){ if (hg.closestHouseToRoad.isLeft() != this.closestHouseToRoad.isLeft()){ // attach this group to the same segment on the other road side linkNode = c1; return false; } else { log.warn("two groups on same side of road share same point, group 1:",hg,"group 2:",this,"in road",getRoad()); useExisting = false; break; } } } if (useExisting){ c1.setNumberNode(true); c2.setNumberNode(true); linkNode = c1; return false; } } int timesToAdd = 1; double frac = closestHouseToRoad.getSegmentFrac(); if (frac < 0) frac = 0; if (frac > 1) frac = 1; double segLen = c1.distance(c2); pointToUse = c1.makeBetweenPoint(c2, frac); double len1 = segLen * frac; double len2 = (1 - Math.min(1, frac)) * segLen; if (Math.min(len1, len2) < MIN_DISTANCE_TO_EXISTING_POINT){ pointToUse = (len1 <= len2) ? c1 : c2; } else { Coord optPoint = ExtNumbers.rasterLineNearPoint(c1, c2, pointToUse, true); double opt1Dist = c1.distance(optPoint); double opt2Dist = c2.distance(optPoint); pointToUse = optPoint; if (Math.min(opt1Dist, opt2Dist) <= MIN_DISTANCE_TO_EXISTING_POINT){ pointToUse = (opt1Dist < opt2Dist) ? c1 : c2; } else { if (log.isInfoEnabled()){ double distLine = pointToUse.distToLineSegment(c1.getDisplayedCoord(), c2.getDisplayedCoord()); log.info("adding number nodes at",pointToUse.toDegreeString(),"to optimize address search", "dist to line is", HousenumberGenerator.formatLen(distLine)); } timesToAdd = 2; } } if (points.size() + timesToAdd > LineSplitterFilter.MAX_POINTS_IN_LINE) return false; pointToUse.setNumberNode(true); if (timesToAdd == 2){ // add two new points between c1 and c2 points.add(seg + 1, pointToUse); pointToUse.setAddedNumberNode(true); pointToUse = new Coord (pointToUse); pointToUse.setNumberNode(true); points.add(seg + 1, pointToUse); pointToUse.setAddedNumberNode(true); linkNode = pointToUse; } else { // copy it pointToUse = new Coord(pointToUse); pointToUse.setNumberNode(true); // add copy before c2 points.add(seg + 1, pointToUse); pointToUse.setAddedNumberNode(true); if (pointToUse.highPrecEquals(c1)){ linkNode = c1; } else { // link to the copy of c2 which is before c2 linkNode = pointToUse; } } return true; } public boolean verify(){ if (findSegmentWasCalled) return true; if (minSeg < 0 || maxSeg < 0) return false; int step = 1; if (odd == 0 || even == 0) step = 2; boolean ok = false; if (usedNumbers.size() == (maxNum - minNum) / step + 1) ok = true; // final check: double deltaDist = Math.abs(closestHouseToRoad.getDistance() - farthestHouseToRoad.getDistance()); if (houses.size() > 2 && deltaDist < houses.size() * 3 ){ // more than two houses: make sure that they are really not parallel to the road // for each house we calculate 3m so that a group is kept if it forms an angle of 45° or more // with the road, presuming that the road is rather straight ok = false; } for (HousenumberMatch house : houses){ // forget the group, it will not improve search house.setGroup(ok ? this : null); } return ok; } public MapRoad getRoad(){ return hnr.getRoad(); } private final static double CLOSE_HOUSES_DIST = 10; public static boolean housesFormAGroup(HousenumberMatch house1, HousenumberMatch house2) { if (house1.isIgnored() || house2.isIgnored()) return false; if (house1.getRoad() != house2.getRoad()){ log.error("internal error, group check with houses on different roads?",house1.getElement().getId(),house2.getElement().getId()); return false; } // assert house1.getRoad() == house2.getRoad(); if (house1.getSegment() > house2.getSegment()){ HousenumberMatch help = house1; house1 = house2; house2 = help; } double distBetweenHouses = house1.getLocation().distance(house2.getLocation()); if (distBetweenHouses == 0) return true; double minDistToRoad = Math.min(house1.getDistance(), house2.getDistance()); double maxDistToRoad = Math.max(house1.getDistance(), house2.getDistance()); double distOnRoad = house2.getDistOnRoad(house1); if (house1.getSegment() != house2.getSegment()){ if (minDistToRoad > 40 && distBetweenHouses < CLOSE_HOUSES_DIST) return true; // not the same segment, the distance on road may be misleading when segments have a small angle // and the connection point is a bit more away Coord c1 = house1.getLocation(); Coord c2 = house2.getLocation(); Coord closest1 = house1.getClosestPointOnRoad(); Coord closest2 = house2.getClosestPointOnRoad(); double frac1 = HousenumberGenerator.getFrac(closest1, closest2, c1); double frac2 = HousenumberGenerator.getFrac(closest1, closest2, c2); double segLen = closest1.distance(closest2); if (frac1 < 0) frac1 = 0; if (frac2 < 0) frac2 = 0; if (frac1 > 1) frac1 = 1; if (frac2 > 1) frac2 = 1; double distOnRoadSimple = (Math.max(frac1, frac2) - Math.min(frac1, frac2)) * segLen; if (distOnRoadSimple != distOnRoad){ // log.debug("distOnRoad recalculation:", house1.getRoad(),house1,house2,distOnRoad,"--->",distOnRoadSimple); distOnRoad = distOnRoadSimple; } } if (distOnRoad <= 0){ return true; } // two houses form a group when the distance on road is short // how short? The closer the houses are to the road, the shorter double toleranceDistOnRoad = 5 + maxDistToRoad/ 10; if (distOnRoad > toleranceDistOnRoad){ return false; } double deltaDistToRoad = maxDistToRoad - minDistToRoad; double ratio2 = deltaDistToRoad / distBetweenHouses; // a ratio2 near or higher 1 means that the two houses and the closest point on the // road are on a straight line if (ratio2 > 0.9) return true; if (ratio2 < 0.666) return false; return true; } public boolean tryAddHouse(HousenumberMatch house) { if (house.isInterpolated() || house.getRoad() == null || house.isIgnored()) return false; int num = house.getHousenumber(); int step = 1; if (odd == 0 || even == 0) step = 2; if (num - maxNum != step) return false; HousenumberMatch last = houses.get(houses.size()-1); if (last.getGroup() != null){ if (last.getGroup() == house.getGroup()){ addHouse(house); return true; } else return false; } if (last.getDistance() + 3 < house.getDistance() && last.isDirectlyConnected(house)){ addHouse(house); return true; } if (housesFormAGroup(house, last) == false){ return false; } if (houses.size() > 1){ HousenumberMatch first = houses.get(0); if (housesFormAGroup(house, first) == false){ HousenumberMatch preLast = houses.get(houses.size()-2); double angle = Utils.getAngle(house.getLocation(), last.getLocation(), preLast.getLocation()); if (Math.abs(angle) > 30) return false; } } addHouse(house); return true; } public boolean recalcPositions(){ List<HousenumberMatch> saveHouses = new ArrayList<>(houses); reset(); for (HousenumberMatch house : saveHouses) addHouse(house); if (!verify()){ for (HousenumberMatch house : houses){ HousenumberGenerator.findClosestRoadSegment(house, getRoad()); } return false; } return true; } private void reset() { usedNumbers = new Int2IntOpenHashMap(); minNum = Integer.MAX_VALUE; maxNum = -1; minSeg = Integer.MAX_VALUE; maxSeg = -1; minFrac = maxFrac = Double.NaN; closestHouseToRoad = null; farthestHouseToRoad = null; odd = even = 0; houses.clear(); } public String toString(){ return houses.toString(); } }