/* * 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.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.net.NumberStyle; import uk.me.parabola.imgfmt.app.net.Numbers; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.filters.LineSplitterFilter; 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.osmstyle.housenumber.HousenumberGenerator.HousenumberMatchByPosComparator; /** * Helper class to allow easy corrections like splitting. * * If we want to split an interval because it overlaps * with another one, we have different options. * 1) if the interval covers multiple points of the road, we may change a point to a number node * 2) or if the road segment is long enough we may add a point to split it, * long enough means that the we can find a point that is so close to the original * line that it is not too distorting, * 3) or we may duplicate the node at one end (or both) and move some of the numbers to that new * zero-length-interval. * * When we find no more overlaps, we can start to reduce the distance * of the calculated position (the result of the address search in * Garmin products) and the known best position on the road. * This is a bit tricky: Garmin software places * a) a single house in the middle of the * segment covered by the interval * b) two or more houses are placed so that the first * is at the very beginning, the last is at the very end, * the rest is between them with equal distances. * The problem: We can have houses on both sides of the segment, * so the optimal length for the left side may not be the * best for the right side. * We try to find a good compromise between good search result * and the number of additional intervals. * * @author GerdP * */ public class ExtNumbers { private static final Logger log = Logger.getLogger(ExtNumbers.class); private final HousenumberRoad housenumberRoad; private static final int MAX_LOCATE_ERROR = 40; private static final List<HousenumberMatch> NO_HOUSES = Collections.emptyList(); public ExtNumbers prev,next; class RoadSide { List<HousenumberMatch> houses = Collections.emptyList(); boolean multipleZipCodes; boolean multipleCities; boolean notInOrder; } private RoadSide leftSide,rightSide; private Numbers numbers = null; private int startInRoad, endInRoad; private int nodeIndex; private boolean needsSplit; private HousenumberMatch worstHouse; // indicates a number that is found in the interval, but should not private int badNum; private boolean hasGaps; // true if interval covers more numbers than known // constants representing reasons for splitting public static final int SR_FIX_ERROR = 0; public static final int SR_OPT_LEN = 1; public static final int SR_SPLIT_ROAD_END = 2; public ExtNumbers(HousenumberRoad housenumberRoad) { super(); this.housenumberRoad = housenumberRoad; reset(); } private void setNeedsSplit(boolean b) { needsSplit = true; } public boolean needsSplit() { return needsSplit; } private boolean notInOrder(boolean left){ RoadSide rs = (left) ? leftSide : rightSide; return (rs != null) ? rs.notInOrder : false; } private List<HousenumberMatch> getHouses(boolean left){ RoadSide rs = (left) ? leftSide : rightSide; return (rs != null) ? rs.houses : NO_HOUSES; } private MapRoad getRoad(){ return housenumberRoad.getRoad(); } private void reset() { numbers = null; needsSplit = false; hasGaps = false; } public void setNodeIndex(int nodeIndex) { this.nodeIndex = nodeIndex; if (numbers != null) numbers.setIndex(nodeIndex); } public Numbers getNumbers() { if (numbers == null){ numbers = new Numbers(); numbers.setIndex(nodeIndex); fillNumbers(Numbers.LEFT); fillNumbers(Numbers.RIGHT); if (!numbers.isEmpty()){ verify(getHouses(Numbers.LEFT)); // TODO : remove verify(getHouses(Numbers.RIGHT)); // TODO : remove } } return numbers; } /** * Store given house numbers and meta info * @param housenumbers a list of house numbers, sorted by appearance on the road * @param startSegment index of road point where this segment starts * @param endSegment index of road point where this segment ends * @param left {@code true} the left side of the street; {@code false} the right side of the street * @return the number of elements which were used (from the beginning) */ public int setNumbers(List<HousenumberMatch> housenumbers, int startSegment, int endSegment, boolean left) { int assignedNumbers = 0; if (housenumbers.isEmpty() == false) { RoadSide rs = new RoadSide(); if (left) leftSide = rs; else rightSide = rs; // get the sublist of house numbers int maxN = -1; int numHouses = housenumbers.size(); for (int i = 0; i< numHouses; i++) { HousenumberMatch house = housenumbers.get(i); if (house.isIgnored()) continue; if (house.getSegment() >= endSegment) { break; } maxN = i; } if (maxN >= 0) { assignedNumbers = maxN + 1; rs.houses = new ArrayList<>(housenumbers.subList(0, assignedNumbers)); startInRoad = startSegment; endInRoad = endSegment; assert startSegment < endSegment; if (getRoad().getPoints().get(startInRoad).isNumberNode() == false || getRoad().getPoints().get(endInRoad).isNumberNode() == false){ log.error("internal error: start or end is not a number node", this); } } } return assignedNumbers; } /** * Apply the given house numbers to the numbers object. * @param left {@code true} the left side of the street; {@code false} the right side of the street */ private void fillNumbers(boolean left) { NumberStyle style = NumberStyle.NONE; List<HousenumberMatch> houses = getHouses(left); if (houses.isEmpty() == false) { Set<CityInfo> cityInfos = new HashSet<>(); Set<ZipCodeInfo> zipCodes = new HashSet<>(); // get the sublist of house numbers boolean even = false; boolean odd = false; boolean inOrder = true; boolean inc = false; boolean dec = false; HousenumberMatch highest, lowest; lowest = highest = houses.get(0); Int2IntOpenHashMap distinctNumbers = new Int2IntOpenHashMap(); int numHouses = houses.size(); HousenumberMatch pred = null; for (int i = 0; i< numHouses; i++) { HousenumberMatch house = houses.get(i); if (house.getCityInfo() != null && house.getCityInfo().isEmpty() == false) cityInfos.add(house.getCityInfo()); if (house.getZipCode() != null && house.getZipCode().getZipCode() != null) zipCodes.add(house.getZipCode()); int num = house.getHousenumber(); if (!hasGaps) distinctNumbers.put(num, 1); if (num > highest.getHousenumber()) highest = house; if (num < lowest.getHousenumber()) lowest = house; if (num % 2 == 0) { even = true; } else { odd = true; } if (pred != null){ int diff = num - pred.getHousenumber(); if(diff > 0) inc = true; else if (diff < 0) dec = true; } pred = house; } if (even && odd) { style = NumberStyle.BOTH; } else if (even) { style = NumberStyle.EVEN; } else { style = NumberStyle.ODD; } int highestNum = highest.getHousenumber(); int lowestNum = lowest.getHousenumber(); int start = houses.get(0).getHousenumber(); int end = houses.get(numHouses-1).getHousenumber(); boolean increasing = false; // from low to high if (dec & inc) inOrder = false; if (start == end && highestNum - lowestNum != 0){ if (prev != null){ int lastEnd = prev.getNumbers().getEnd(left ); if (lastEnd <= lowestNum) increasing = true; } else if (next != null){ int nextStart = next.getNumbers().getStart(left); if (highestNum < nextStart) increasing = true; } else { increasing = true; } } else if (start != highestNum && start != lowestNum || end != highestNum && end != lowestNum) { inOrder = false; if (start <= end) increasing = true; } else if (start < end){ increasing = true; } if (increasing){ start = lowestNum; end = highestNum; } else { start = highestNum; end = lowestNum; } if (!hasGaps){ int step = (style == NumberStyle.BOTH) ? 1 : 2; for (int n = lowestNum+step; n < highestNum; n += step){ if (distinctNumbers.containsKey(n)) continue; hasGaps = true; break; } } RoadSide rs = (left) ? leftSide : rightSide; numbers.setNumbers(left, style, start, end); rs.multipleCities = (cityInfos.size() > 1); rs.multipleZipCodes = (zipCodes.size() > 1); if (cityInfos.size() == 1){ CityInfo ci = cityInfos.iterator().next(); if (ci.isEmpty() == false){ if (ci.equals(housenumberRoad.getRoadCityInfo()) == false) numbers.setCityInfo(left, ci); } } if (zipCodes.size() == 1){ ZipCodeInfo zipCodeInfo = zipCodes.iterator().next(); if (zipCodeInfo.getZipCode() != null){ if (zipCodeInfo.equals(housenumberRoad.getRoadZipCode()) == false){ // we found a zip code and the road doesn't yet have one, use it for the whole road if (housenumberRoad.getRoadZipCode() == null){ housenumberRoad.setZipCodeInfo(zipCodeInfo); } else numbers.setZipCode(left, zipCodeInfo); } } } rs.notInOrder = !inOrder; } } /** * Return the intervals in the format used for the writer routines * @return */ public List<Numbers> getNumberList() { // do we have numbers? boolean foundNumbers = false; for (ExtNumbers curr = this; curr != null; curr = curr.next){ if (curr.hasNumbers()){ foundNumbers = true; break; } } if (!foundNumbers) return null; List<Numbers> list = new ArrayList<>(); boolean headerWasReported = false; for (ExtNumbers curr = this; curr != null; curr = curr.next){ if (curr.hasNumbers() == false) continue; list.add(curr.getNumbers()); if (log.isInfoEnabled()) { if (headerWasReported == false){ MapRoad road = curr.getRoad(); if (road.getStreet() == null && road.getName() == null) log.info("final numbers for", road, curr.housenumberRoad.getName(), "in", road.getCity()); else log.info("final numbers for", road, "in", road.getCity()); headerWasReported = true; } Numbers cn = curr.getNumbers(); log.info("Left: ",cn.getLeftNumberStyle(),cn.getIndex(),"Start:",cn.getLeftStart(),"End:",cn.getLeftEnd(), "numbers "+curr.getHouses(Numbers.LEFT)); log.info("Right:",cn.getRightNumberStyle(),cn.getIndex(),"Start:",cn.getRightStart(),"End:",cn.getRightEnd(), "numbers "+curr.getHouses(Numbers.RIGHT)); } } return list; } public ExtNumbers checkSingleChainSegments(String streetName, boolean removeGaps) { ExtNumbers curr = this; ExtNumbers head = this; if (housenumberRoad.isRandom() || removeGaps){ for (curr = head; curr != null; curr = curr.next){ while (curr.hasGaps && (removeGaps || curr.notInOrder(true) || curr.notInOrder(false))){ curr.worstHouse = null; curr.badNum = -1; ExtNumbers test = curr.tryChange(SR_FIX_ERROR); if (test != curr){ if (curr.prev == null) head = test; curr = test; } else { log.warn("can't split numbers interaval for road", curr.getNumbers(), curr); break; } } } } for (curr = head; curr != null; curr = curr.next){ while (curr.isPlausible() == false){ // this happens in the following cases: // 1. correct OSM data, multiple houses build a block. Standing on the road // you probably see a small service road which leads to the houses. // It is okay to use each of them. // 2. correct OSM data, one or more house should be connected to a // different road with the same name, we want to ignore them // 3. wrong OSM data, one or more numbers are wrong, we want to ignore them // 4. other cases, e.g. numbers 1,3,5 followed by 10,14,12. This should be fixed // by splitting the segment first, as the OSM data might be correct. if (log.isInfoEnabled()) log.info("detected unplausible interval in",streetName, curr.getNumbers(),"in road", getRoad()); if (log.isDebugEnabled()){ if (curr.notInOrder(Numbers.LEFT)) log.debug("left numbers not in order:", getRoad(), curr.getHouses(Numbers.LEFT)); if (curr.notInOrder(Numbers.RIGHT)) log.debug("right numbers not in order:", getRoad(), curr.getHouses(Numbers.RIGHT)); } curr.setNeedsSplit(true); curr.findGoodSplitPos(); ExtNumbers test = curr.tryChange(SR_FIX_ERROR); if (test != curr){ housenumberRoad.setChanged(true); if (curr.prev == null) head = test; curr = test; } else { log.warn("can't fix unplausible numbers interaval for road",curr.getNumbers(),curr); break; } } } return head; } private void verify(List<HousenumberMatch> houses) { for (HousenumberMatch house : houses){ if (house.isIgnored()) continue; if (house.getSegment() < startInRoad || house.getSegment() >= endInRoad){ log.error("internal error, house has wrong segment, road",getRoad(),"house",house,house.toBrowseURL()); } if (Double.isNaN(house.getDistance()) || house.getDistance() > HousenumberGenerator.MAX_DISTANCE_TO_ROAD + 10){ if (house.getGroup() == null) log.error("internal error, distance to road too large, road",getRoad(),"house",house,house.toBrowseURL()); } } } /** * Split an interval. This means that we either change an existing point * to a number node or we add a number node. A new node may be added between * two other points or as a duplicate of one of them. * Depending on the reason for the split we use different methods to distribute * the existing numbers to the new intervals. * @param reason indicates the reason for the split * @return this if split was not done or a new {@link ExtNumbers} instance which is * the start of a chain, the last instance in this chain points to the same * {@link ExtNumbers} instance as the {@code next} field in {@code this} . * */ public ExtNumbers tryChange(int reason){ ExtNumbers en = this; if (reason == SR_FIX_ERROR){ if (notInOrder(Numbers.LEFT) == false && notInOrder(Numbers.RIGHT) == false){ if (badNum < 0 && worstHouse != null) badNum = worstHouse.getHousenumber(); if (badNum > 0){ en = splitInterval(); } else { log.info("have to split",this); } } } //TODO: in some cases it might be better to move a house to the prev or next interval instead of splitting if (en == this) en = tryAddNumberNode(reason); boolean changedInterval = false; if (en != this){ if (en.hasNumbers() && en.next != null && en.next.hasNumbers()){ changedInterval = true; } else { ExtNumbers test = en.hasNumbers() ? en : en.next; if (test.getNumbers().isSimilar(this.getNumbers()) == false) changedInterval = true; } if (changedInterval) housenumberRoad.setChanged(true); else { if (reason == SR_FIX_ERROR){ if (en.hasNumbers()){ en.worstHouse = worstHouse; return en.tryChange(reason); } else { en.next.worstHouse = worstHouse; en.next = en.next.tryAddNumberNode(reason); } } } } return en; } /** * Split an interval to remove overlaps * 1) detect the optimal split position * 2) calculate the new intervals * @return */ private ExtNumbers splitInterval(){ if (log.isDebugEnabled()) log.debug("trying to split",this,"so that",badNum,"is not contained"); boolean doSplit = false; Numbers origNumbers = getNumbers(); if (origNumbers.countMatches(badNum) == 0){ if (log.isDebugEnabled()) log.debug("badNum",badNum,"is not contained in",this); return this; } // create an test interval to find out which side contains the bad number Numbers testNumbers = new Numbers(); testNumbers.setNumbers(Numbers.LEFT, origNumbers.getLeftNumberStyle(), origNumbers.getLeftStart(), origNumbers.getLeftEnd()); boolean left = (testNumbers.countMatches(badNum) > 0); List<HousenumberMatch> before = new ArrayList<>(); List<HousenumberMatch> after = new ArrayList<>(); List<HousenumberMatch> toSplit = getHouses(left); boolean inc = (origNumbers.getEnd(left) > origNumbers.getStart(left)); BitSet segmentsBefore = new BitSet(); BitSet segmentsAfter = new BitSet(); for (HousenumberMatch house : toSplit){ List<HousenumberMatch> target; if (house.getHousenumber() < badNum){ target = inc ? before : after; } else if (house.getHousenumber() > badNum){ target = inc ? after : before; } else { int s = origNumbers.getStart(left); target = (s == badNum) ? before : after; } target.add(house); if (target == before){ segmentsBefore.set(house.getSegment()); } else { segmentsAfter.set(house.getSegment()); } } if (before.isEmpty() || after.isEmpty()) return this; if (log.isDebugEnabled()) log.debug("todo: find best method to separate",before,"and",after); HousenumberMatch house1 = before.get(before.size() - 1); HousenumberMatch house2 = after.get(0); List<HousenumberMatch> testOrder = new ArrayList<>(); testOrder.add(house1); testOrder.add(house2); Collections.sort(testOrder, new HousenumberMatchByPosComparator()); if (testOrder.get(0) != house1){ log.info("order indicates random case or missing road!",this); housenumberRoad.setRandom(true); } int splitSegment = -1; if (house1.getSegment() != house2.getSegment()){ // simple case: change point if (log.isDebugEnabled()) log.debug("simple case: change point to number node between",house1,house2); // what point is best?, use beginning of 2nd for now splitSegment = house2.getSegment(); doSplit = true; } else { int seg = house1.getSegment(); Coord c1 = getRoad().getPoints().get(seg); Coord c2 = getRoad().getPoints().get(seg + 1); double segmentLength = c1.distance(c2); Coord toAdd = null; boolean addOK = true; double wantedFraction = (house1.getSegmentFrac() + house2.getSegmentFrac()) / 2; // handle cases where perpendicular is not on the road if (wantedFraction <= 0){ wantedFraction = 0; toAdd = new Coord(c1); } else if (wantedFraction >= 1){ wantedFraction = 1; toAdd = new Coord(c2); } double usedFraction = wantedFraction; if (toAdd == null) { Coord wanted = c1.makeBetweenPoint(c2, wantedFraction); log.debug("possible solution: split segment with length",formatLen(segmentLength),"near",formatLen(wantedFraction * segmentLength)); toAdd = rasterLineNearPoint(c1, c2, wanted, true); if (toAdd != null){ if (toAdd.equals(c1)){ toAdd = new Coord(c1); usedFraction = 0.0; } else if (toAdd.equals(c2)){ toAdd = new Coord(c2); usedFraction = 0; } else { addOK = checkLineDistortion(c1, c2, toAdd); if (addOK) usedFraction = HousenumberGenerator.getFrac(c1, c2, toAdd); else toAdd = null; } } } if (toAdd == null){ double len1 = wantedFraction * segmentLength; double len2 = (1 - wantedFraction) * segmentLength; if (Math.min(len1, len2) < MAX_LOCATE_ERROR){ if (len1 < len2){ toAdd = new Coord(c1); usedFraction = 0.0; } else { toAdd = new Coord(c2); usedFraction = 1.0; } } } if (toAdd == null){ log.error("internal error, cannot split",this); } if (toAdd != null){ if (log.isDebugEnabled()){ log.debug("solution: split segment with length",formatLen(segmentLength),"at",formatLen(usedFraction * segmentLength)); double distToLine = toAdd.getDisplayedCoord().distToLineSegment(c1.getDisplayedCoord(), c2.getDisplayedCoord()); log.info("adding number node at",toAdd.toDegreeString(),"to split, dist to line is",formatLen(distToLine)); } doSplit = true; splitSegment = seg+1; addAsNumberNode(splitSegment, toAdd); this.endInRoad++; for (HousenumberMatch house : before){ if (house.getSegment() >= seg){ HousenumberGenerator.findClosestRoadSegment(house, getRoad(), seg, splitSegment); } } for (HousenumberMatch house : after){ if (house.getSegment() < splitSegment) HousenumberGenerator.findClosestRoadSegment(house, getRoad(), splitSegment, splitSegment + 1); else house.setSegment(house.getSegment()+1); } recalcHousePositions(getHouses(!left)); // the other side } } if (doSplit){ ExtNumbers en1 = split(splitSegment); ExtNumbers en2 = en1.next; if (en1.getHouses(Numbers.LEFT).size() + en2.getHouses(Numbers.LEFT).size() != getHouses(Numbers.LEFT).size() || en1.getHouses(Numbers.RIGHT).size() + en2.getHouses(Numbers.RIGHT).size() != getHouses(Numbers.RIGHT).size()){ log.error("internal error, lost houses"); } log.info("number node added in street",getRoad(),getNumbers(),"==>",en1.getNumbers(),"+",en2.getNumbers()); return en1; } return this; } private boolean checkLineDistortion(Coord c1, Coord c2, Coord toAdd){ double distToLine = toAdd.getDisplayedCoord().distToLineSegment(c1.getDisplayedCoord(), c2.getDisplayedCoord()); if (distToLine > 0.2){ double angle = Utils.getDisplayedAngle(c1, toAdd, c2); if (Math.abs(angle) > 3){ return false; } } return true; } /** * Try to add a number node. * We may change an existing point to a number node or add a new * number node. A new node might be between the existing ones * or a duplicate of one of them. * @return */ private ExtNumbers tryAddNumberNode(int reason) { String action; if (endInRoad - startInRoad > 1) action = "change"; else { if (getRoad().getPoints().size() + 1 > LineSplitterFilter.MAX_POINTS_IN_LINE){ log.warn("can't change intervals, road has already",LineSplitterFilter.MAX_POINTS_IN_LINE,"points"); return this; // can't add a node } Coord c1 = getRoad().getPoints().get(startInRoad); Coord c2 = getRoad().getPoints().get(startInRoad+1); if (c1.equals(c2)){ return dupNode(0, SR_FIX_ERROR); } double segmentLength = c1.distance(c2); int countAfterEnd = 0, countBeforeStart = 0; double minFraction0To1 = 2; double maxFraction0To1 = -1; for (int side = 0; side < 2; side++){ boolean left = side == 0; List<HousenumberMatch> houses = getHouses(left); for (HousenumberMatch house : houses){ if (house.getSegmentFrac() < 0) ++countBeforeStart; else if (house.getSegmentFrac() > 1) ++countAfterEnd; else { if (minFraction0To1 > house.getSegmentFrac()) minFraction0To1 = house.getSegmentFrac(); if (maxFraction0To1 < house.getSegmentFrac()) maxFraction0To1 = house.getSegmentFrac(); } } } // special cases: perpendicular not on the road if (countBeforeStart > 0){ return dupNode(0, SR_SPLIT_ROAD_END); } if (countAfterEnd > 0){ return dupNode(1, SR_SPLIT_ROAD_END); } // try to find a good split point depending on the split reason double wantedFraction, midFraction; wantedFraction = midFraction = (minFraction0To1 + maxFraction0To1) / 2; Coord toAdd = null; double len1 = segmentLength * minFraction0To1; // dist to first double len2 = segmentLength * maxFraction0To1; double len3 = (1-maxFraction0To1) * segmentLength; double expectedError = c1.getDisplayedCoord().distance(new Coord(c1.getLatitude()+1,c1.getLongitude())); double maxDistBefore = expectedError; double maxDistAfter = expectedError; if (reason == SR_FIX_ERROR && worstHouse != null){ wantedFraction = worstHouse.getSegmentFrac(); if (wantedFraction < minFraction0To1 || wantedFraction > maxFraction0To1){ log.error("internal error, worst house not found",this,worstHouse); } } boolean allowSplitBetween = true; boolean forceEmpty = false; if (reason == SR_OPT_LEN){ if (log.isDebugEnabled()){ if (maxFraction0To1 != minFraction0To1){ log.debug("trying to find good split point, houses are between",formatLen(len1),"and",formatLen(len2),"in segment with",formatLen(segmentLength)); } else log.debug("trying to find good split point, houses are at",formatLen(len1),"in segment with",formatLen(segmentLength)); } if (len2 - len1 < 10 && getHouses(Numbers.LEFT).size() <= 1 && getHouses(Numbers.RIGHT).size() <= 1){ // one house or two opposite houses // we try to split so that the house(s) are near the middle of one part wantedFraction = wantedFraction * 2 - (wantedFraction > 0.5 ? 1 : 0); allowSplitBetween = false; } else { if (len1 > MAX_LOCATE_ERROR / 2){ // create empty segment at start wantedFraction = minFraction0To1 * 0.999; forceEmpty = true; } if (len3 > MAX_LOCATE_ERROR / 2 && len3 > len1){ // create empty segment at end wantedFraction = maxFraction0To1 * 1.001; forceEmpty = true; } } } double partLen = wantedFraction * segmentLength ; double shorterLen = Math.min(partLen , segmentLength - partLen); if (shorterLen < 10){ if (reason == SR_FIX_ERROR && minFraction0To1 == maxFraction0To1) return dupNode(midFraction, SR_FIX_ERROR); double splitFrac = len1 < len3 ? minFraction0To1 : maxFraction0To1; return dupNode(splitFrac, SR_OPT_LEN); } double usedFraction = 0; double bestDist = Double.MAX_VALUE; if (wantedFraction < minFraction0To1){ maxDistAfter = 0; } if (wantedFraction > maxFraction0To1){ maxDistBefore = 0; } for (;;){ Coord wanted = c1.makeBetweenPoint(c2, wantedFraction); Map<Double, List<Coord>> candidates = rasterLineNearPoint2(c1, c2, wanted, maxDistBefore, maxDistAfter); boolean foundGood = false; for (Entry<Double, List<Coord>> entry : candidates.entrySet()){ if (foundGood) break; bestDist = entry.getKey(); for (Coord candidate: entry.getValue()){ toAdd = candidate; usedFraction = HousenumberGenerator.getFrac(c1, c2, toAdd); if (usedFraction <= 0 || usedFraction >= 1) toAdd = null; else if (usedFraction > minFraction0To1 && wantedFraction < minFraction0To1 || usedFraction < maxFraction0To1 && wantedFraction > maxFraction0To1){ toAdd = null; } else if (allowSplitBetween == false && usedFraction > minFraction0To1 && usedFraction < maxFraction0To1){ toAdd = null; } else { if (bestDist > 0.2){ double angle = Utils.getDisplayedAngle(c1, toAdd, c2); if (Math.abs(angle) > 3){ toAdd = null; continue; } } foundGood = true; break; } } } if (foundGood){ break; } toAdd = null; boolean tryAgain = false; if (maxDistBefore > 0 && maxDistBefore < segmentLength * wantedFraction) { maxDistBefore *= 2; tryAgain = true; } if (maxDistAfter > 0 && maxDistAfter < segmentLength * (1 - wantedFraction)) { maxDistAfter *= 2; tryAgain = true; } if (!tryAgain) break; } boolean addOK = true; if (toAdd == null) addOK = false; else { toAdd.incHighwayCount(); if (log.isDebugEnabled()){ log.debug("spliting road segment",startInRoad,"at",formatLen(usedFraction * segmentLength)); } } if (!addOK){ if (reason == SR_FIX_ERROR && minFraction0To1 == maxFraction0To1) return dupNode(midFraction, SR_FIX_ERROR); if (Math.min(len1, len3) < MAX_LOCATE_ERROR ){ double splitFrac = -1; if (reason == SR_OPT_LEN){ if (wantedFraction <= minFraction0To1) splitFrac = minFraction0To1; else if (wantedFraction >= maxFraction0To1) splitFrac = maxFraction0To1; if (splitFrac <= 0.5 && len1 >= MAX_LOCATE_ERROR || splitFrac > 0.5 && len3 >= MAX_LOCATE_ERROR){ splitFrac = -1; } } if (splitFrac < 0) splitFrac = (minFraction0To1 != maxFraction0To1) ? midFraction : minFraction0To1; return dupNode(splitFrac, SR_OPT_LEN); } if(reason == SR_FIX_ERROR) log.warn("can't fix error in interval",this); else if (log.isDebugEnabled()) log.debug("can't improve search result",this); return this; } if (log.isInfoEnabled()) log.info("adding number node at",toAdd.toDegreeString(),"to split, dist to line is",formatLen(bestDist)); action = "add"; this.endInRoad = addAsNumberNode(startInRoad + 1, toAdd); int forcedSegment = - 1; if (forceEmpty){ if (wantedFraction < minFraction0To1) forcedSegment = startInRoad + 1; else if (wantedFraction > maxFraction0To1) forcedSegment = startInRoad; } if (forcedSegment >= 0){ setSegment(forcedSegment, getHouses(Numbers.LEFT)); setSegment(forcedSegment, getHouses(Numbers.RIGHT)); } else { this.recalcHousePositions(getHouses(Numbers.LEFT)); this.recalcHousePositions(getHouses(Numbers.RIGHT)); } } int splitSegment = (startInRoad + endInRoad) / 2; if (worstHouse != null){ if (worstHouse.getSegment() == startInRoad) splitSegment = startInRoad + 1; else if (worstHouse.getSegment() == endInRoad - 1) splitSegment = worstHouse.getSegment(); } else if (endInRoad - startInRoad > 2){ int firstSegWithHouses = endInRoad; int lastSegWithHouses = -1; for (int side = 0; side < 2; side++){ boolean left = side == 0; List<HousenumberMatch> houses = getHouses(left); for (HousenumberMatch house : houses){ int s = house.getSegment(); if (s < firstSegWithHouses) firstSegWithHouses = s; if (s > lastSegWithHouses) lastSegWithHouses = s; } } splitSegment = (firstSegWithHouses + lastSegWithHouses) / 2; if (splitSegment == startInRoad) splitSegment++; } ExtNumbers en1 = split(splitSegment); ExtNumbers en2 = en1.next; if (reason == SR_OPT_LEN){ // TODO: fill gaps, e.g. if split results in O,1,9 -> O,1,1 + O,9,9 ? } if ("add".equals(action)) log.info("number node added in street",getRoad(),getNumbers(),"==>",en1.getNumbers(),"+",en2.getNumbers()); else log.info("point changed to number node in street",getRoad(),getNumbers(),"==>",en1.getNumbers(),"+",en2.getNumbers()); return en1; } /** * Duplicate a node in the road. This creates a zero-length segment. * We can add house numbers to this segment and the position in the * address search will be the same for all of them. * @param fraction a value below 0.5 means duplicate the start node, others * duplicate the end node * @return the new chain with two segments */ private ExtNumbers dupNode(double fraction, int reason) { log.info("duplicating number node in road",getRoad(),getNumbers(),getHouses(Numbers.LEFT),getHouses(Numbers.RIGHT)); boolean atStart = (fraction <= 0.5); // add a copy of an existing node int index = (atStart) ? startInRoad : endInRoad; int splitSegment = (atStart) ? startInRoad + 1: endInRoad; Coord closePoint = getRoad().getPoints().get(index); Coord toAdd = new Coord(closePoint); toAdd.setOnBoundary(closePoint.getOnBoundary()); toAdd.incHighwayCount(); // we have to make sure that the road starts and ends with a CoordNode! this.endInRoad = addAsNumberNode(splitSegment, toAdd); // distribute the houses to the new intervals List<ArrayList<HousenumberMatch>> leftTargets = Arrays.asList(new ArrayList<HousenumberMatch>(),new ArrayList<HousenumberMatch>()); List<ArrayList<HousenumberMatch>> rightTargets = Arrays.asList(new ArrayList<HousenumberMatch>(),new ArrayList<HousenumberMatch>()); int target; if (reason == SR_SPLIT_ROAD_END || reason == SR_OPT_LEN){ for (int side = 0; side < 2; side++){ boolean left = side == 0; List<ArrayList<HousenumberMatch>> targets = left ? leftTargets : rightTargets; for (HousenumberMatch house : getHouses(left)){ if (house.getSegmentFrac() < fraction) target = 0; else if (house.getSegmentFrac() > fraction) target = 1; else target = (atStart) ? 0 : 1; targets.get(target).add(house); } } } else if (getHouses(Numbers.LEFT).size() > 1 || getHouses(Numbers.RIGHT).size() > 1){ int start,end; for (int side = 0; side < 2; side++){ boolean left = side == 0; if (getHouses(left).isEmpty()) continue; start = getNumbers().getStart(left); end = getNumbers().getEnd(left); List<ArrayList<HousenumberMatch>> targets = left ? leftTargets : rightTargets; if (start != end){ int midNum = (start + end) / 2; for (HousenumberMatch house : getHouses(left)){ if (house.getHousenumber() < midNum) target = 0; else if (house.getHousenumber() > midNum) target = 1; else target = (atStart) ? 0 : 1; targets.get(target).add(house); } } else if (multipleZipOrCity(left) == false){ if (atStart) targets.get(1).addAll(getHouses(left)); else targets.get(0).addAll(getHouses(left)); } else { int mid = getHouses(left).size() / 2; targets.get(0).addAll(getHouses(left).subList(0, mid)); targets.get(1).addAll(getHouses(left).subList(mid,getHouses(left).size())); } } } else { log.error("internal error, don't know how to split", this); } assert splitSegment != startInRoad && splitSegment != endInRoad; // make sure that the numbers are assigned to the wanted segment setSegment(startInRoad, leftTargets.get(0)); setSegment(startInRoad, rightTargets.get(0)); setSegment(splitSegment, leftTargets.get(1)); setSegment(splitSegment, rightTargets.get(1)); // don't use split() here, the numbers in this may not be properly // sorted and we don't want to sort them ExtNumbers en1 = divide(); ExtNumbers en2 = en1.next; en1.setNumbers(leftTargets.get(0), startInRoad, splitSegment, true); en1.setNumbers(rightTargets.get(0), startInRoad, splitSegment, false); en2.setNumbers(leftTargets.get(1), splitSegment, endInRoad, true); en2.setNumbers(rightTargets.get(1), splitSegment, endInRoad, false); log.info("zero length interval added in street",getRoad(),getNumbers(),"==>",en1.getNumbers(),"+",en2.getNumbers()); if (atStart && !en1.hasNumbers() || !atStart && !en2.hasNumbers()){ log.error("internal error, zero length interval has no numbers in road",getRoad()); } return en1; } private void setSegment(int segment, List<HousenumberMatch> houses) { for (HousenumberMatch house : houses){ HousenumberGenerator.findClosestRoadSegment(house, getRoad(), segment,segment+1); if (house.getRoad() == null || house.getSegment() != segment){ // should not happen log.error("internal error, house too far from forced segment in road",getRoad(),house,house.toBrowseURL()); house.setIgnored(true); } } } /** * This should be called if a point was added to the road segment * covered by this interval. We have to recalculate the segment numbers * and fraction values. */ private void recalcHousePositions(List<HousenumberMatch> houses){ for (HousenumberMatch house : houses){ HousenumberGenerator.findClosestRoadSegment(house, getRoad(), startInRoad, endInRoad); } if (houses.size() > 1) Collections.sort(houses, new HousenumberMatchByPosComparator()); } /** * Create two empty intervals which will replace this. * @return */ private ExtNumbers divide(){ ExtNumbers en1 = new ExtNumbers(housenumberRoad); ExtNumbers en2 = new ExtNumbers(housenumberRoad); // maintain the linked list en1.prev = this.prev; if (prev != null) prev.next = en1; en1.next = en2; en2.prev = en1; en2.next = this.next; if (this.next != null) next.prev = en2; en1.setNodeIndex(nodeIndex); en2.setNodeIndex(nodeIndex+1); ExtNumbers work = en2.next; while (work != null){ work.setNodeIndex(work.nodeIndex + 1); work = work.next; } return en1; } /** * Add node to the road. * Maintain the positions of houses in the following intervals. * @param toAdd * @param pos * @return new start of next interval */ private int addAsNumberNode(int pos, Coord toAdd){ toAdd.setNumberNode(true); toAdd.setAddedNumberNode(true); getRoad().getPoints().add(pos, toAdd); ExtNumbers work = next; while (work != null){ work.increaseNodeIndexes(startInRoad); work = work.next; } return endInRoad + 1; } /** * This has to be called when a point is added to the road. * @param startPos */ private void increaseNodeIndexes(int startPos){ if (hasNumbers() == false) return; if (startInRoad > startPos){ startInRoad++; endInRoad++; } for (int side = 0; side < 2; side++){ boolean left = side == 0; for (HousenumberMatch house : getHouses(left)){ int s = house.getSegment(); if (s > startPos) house.setSegment(s+1); else assert false : "internal error " + getRoad() + " " + getHouses(Numbers.LEFT) + " " + getHouses(Numbers.RIGHT); } } } private void findGoodSplitPos(){ badNum = -1; worstHouse = null; boolean multipleZipOrCity = false; for (int side = 0; side < 2; side++){ boolean left = side == 0; List<HousenumberMatch> houses = getHouses(left); if (houses.size() <= 1) continue; if (multipleZipOrCity(left)) multipleZipOrCity = true; for (HousenumberMatch house: houses){ int hn = house.getHousenumber(); if (countOccurence(houses, hn) > 1) continue; ExtNumbers modIvl = simulateRemovalOfHouseNumber(hn, left); if (modIvl.isPlausible()){ badNum = hn; if (log.isDebugEnabled()) log.debug("splitpos details: single remove of",badNum,"results in plausible interval"); return; } } } if (multipleZipOrCity) return; // unlikely // log.debug("did not yet find good split position"); Numbers ivl = getNumbers(); int[] firstBad = {-1,-1}; int[] lastBad = {-1,-1}; for (int side = 0; side < 2; side++){ boolean left = (side == 0); int step = 2; if (ivl.getNumberStyle(left) == NumberStyle.BOTH) step = 1; int s = ivl.getStart(left); int e = ivl.getEnd(left); int s2 = ivl.getStart(!left); int e2 = ivl.getEnd(!left ); NumberStyle style2 = ivl.getNumberStyle(!left); for (int hn = Math.min(s, e); hn <= Math.max(s, e); hn += step){ if (style2 == NumberStyle.EVEN && hn % 2 == 1 || style2 == NumberStyle.ODD && hn % 2 == 0 ){ if (firstBad[side] < 0) firstBad[side] = hn; lastBad[side] = hn; continue; } if (hn < Math.min(s2, e2) || hn > Math.max(s2, e2)){ if (firstBad[side] < 0) firstBad[side] = hn; lastBad[side] = hn; } } } if (firstBad[0] == lastBad[0]){ badNum = firstBad[0]; if (badNum >= 0) return; } if (firstBad[1] == lastBad[1]){ badNum = firstBad[1]; if (badNum >= 0) return; } badNum = Math.max(firstBad[0], lastBad[0]); if (badNum == -1) badNum = Math.min(firstBad[1], lastBad[1]); if (log.isDebugEnabled()) log.debug("splitpos details",Arrays.toString(firstBad), Arrays.toString(lastBad),"gives badNum",badNum); } private boolean multipleZipOrCity(boolean left) { RoadSide rs = left ? leftSide : rightSide; return rs == null ? false : rs.multipleCities || rs.multipleZipCodes; } public ExtNumbers checkChainPlausibility(String streetName, List<HousenumberMatch> potentialNumbersThisRoad) { // we try to repair up to 10 times ExtNumbers head = this; for (int loop = 0; loop < 10; loop++){ boolean anyChanges = false; for (ExtNumbers en1 = head; en1 != null; en1 = en1.next){ if (anyChanges) break; if (en1.hasNumbers() == false) continue; for (ExtNumbers en2 = en1.next; en2 != null; en2 = en2.next){ if (anyChanges) break; if (en2.hasNumbers() == false) continue; int res = checkIntervals(streetName, en1, en2); switch (res) { case OK_NO_CHANGES: case NOT_OK_KEEP: break; case OK_AFTER_CHANGES: anyChanges = true; break; case NOT_OK_TRY_SPLIT: if (en1.needsSplit){ ExtNumbers test = en1.tryChange(SR_FIX_ERROR); if (test != en1){ housenumberRoad.setChanged(true); anyChanges = true; if (test.prev == null){ head = test; } } } if (en2.needsSplit){ ExtNumbers test = en2.tryChange(SR_FIX_ERROR); if (test != en2){ anyChanges = true; housenumberRoad.setChanged(true); } } break; case NOT_OK_STOP: return head; default: break; } } } if (!anyChanges) break; } return head; } public static final int OK_NO_CHANGES = 0; public static final int OK_AFTER_CHANGES = 1; public static final int NOT_OK_TRY_SPLIT = 2; public static final int NOT_OK_KEEP = 3; public static final int NOT_OK_STOP = 4; /** * Check if two intervals are overlapping (all combinations of left + right) * @param streetName * @param en1 * @param en2 * @return true if something was changed */ public static int checkIntervals(String streetName, ExtNumbers en1, ExtNumbers en2) { if (en1.getRoad() != en2.getRoad()){ Coord cs1 = en1.getRoad().getPoints().get(en1.startInRoad); Coord ce1 = en1.getRoad().getPoints().get(en1.endInRoad); Coord ce2 = en2.getRoad().getPoints().get(en2.endInRoad); if (ce2 == cs1 || ce2 == ce1){ ExtNumbers help = en1; en1 = en2; en2 = help; } } boolean allOK = true; Numbers ivl1 = en1.getNumbers(); Numbers ivl2 = en2.getNumbers(); for (int i = 0; i < 2; i++){ boolean left1 = i == 0; NumberStyle style1 = ivl1.getNumberStyle(left1); if (style1 == NumberStyle.NONE) continue; int s1 = ivl1.getStart(left1); int e1 = ivl1.getEnd(left1); for (int j = 0; j < 2; j++){ boolean left2 = (j == 0); NumberStyle style2 = ivl2.getNumberStyle(left2); if (style2 == NumberStyle.NONE) continue; int s2 = ivl2.getStart(left2); int e2 = ivl2.getEnd(left2); boolean ok = true; if (style1 == style2 || style1 == NumberStyle.BOTH || style2 == NumberStyle.BOTH) ok = checkIntervalBoundaries(s1, e1, s2, e2, left1 == left2 && en1.getRoad() == en2.getRoad()); if (ok) continue; if (en1.getRoad() != en2.getRoad() && en1.hasGaps == false && en2.hasGaps == false){ allOK = false; continue; } if (s1 == e1){ if (en1.getHouses(left1).get(0).isFarDuplicate()){ allOK = false; continue; } } if (s2 == e2){ if (en2.getHouses(left2).get(0).isFarDuplicate()){ allOK = false; continue; } } List<HousenumberMatch> houses1 = en1.getHouses(left1); List<HousenumberMatch> houses2 = en2.getHouses(left2); if (log.isInfoEnabled()){ log.info("detected unplausible combination of intervals in",streetName, s1 + ".." + e1, "and", s2 + ".." + e2, "houses:", (left1 ? "left:" : "right"), houses1, (left2 ? "left:" : "right"), houses2, (en1.getRoad() == en2.getRoad() ? "in road " + en1.getRoad() : "road id(s):" + en1.getRoad().getRoadDef().getId() + ", " + en2.getRoad().getRoadDef().getId())); } double smallestDelta = Double.POSITIVE_INFINITY; HousenumberMatch bestMoveOrig = null; HousenumberMatch bestMoveMod = null; ExtNumbers bestRemove = null; List<HousenumberMatch> possibleRemoves1 = new ArrayList<>(); List<HousenumberMatch> possibleRemoves2 = new ArrayList<>(); if (en1.housenumberRoad.isRandom() == false && en2.housenumberRoad.isRandom() == false){ // check if we can move a house from en1 to en2 for (HousenumberMatch house : houses1){ if (house.getGroup() != null) continue; int n = house.getHousenumber(); if (countOccurence(houses1, n) > 1) continue; if (n == s1 || n == e1) { Numbers modNumbers = en1.simulateRemovalOfHouseNumber(n, left1).getNumbers(); int s1Mod = modNumbers.getStart(left1); int e1Mod = modNumbers.getEnd(left1); NumberStyle modStyle = modNumbers.getNumberStyle(left1); boolean ok2 = true; if (modStyle == style2 || modStyle == NumberStyle.BOTH || style2 == NumberStyle.BOTH) ok2 = checkIntervalBoundaries(s1Mod, e1Mod, s2, e2, left1 == left2 && en1.getRoad() == en2.getRoad()); if (ok2){ // the intervals don't overlap if house is removed from en1 if (houses1.size() > 1) possibleRemoves1.add(house); // check if it fits into en2 HousenumberMatch test = checkMoveTo(house, en2, left2); if (test.getRoad() != null){ double deltaDist = test.getDistance() - house.getDistance(); if (deltaDist < smallestDelta){ bestMoveMod = test; bestMoveOrig = house; smallestDelta = deltaDist; bestRemove = en1; } } } } } for (HousenumberMatch house : houses2){ if (house.getGroup() != null) continue; int n = house.getHousenumber(); if (countOccurence(houses2, n) > 1) continue; if (n == s2 || n == e2) { Numbers modNumbers = en2.simulateRemovalOfHouseNumber(n, left2).getNumbers(); int s2Mod = modNumbers.getStart(left2); int e2Mod = modNumbers.getEnd(left2); NumberStyle modStyle = modNumbers.getNumberStyle(left2); boolean ok2 = true; if (modStyle == style1 || modStyle == NumberStyle.BOTH || style1 == NumberStyle.BOTH) ok2 = checkIntervalBoundaries(s1, e1, s2Mod, e2Mod, left1 == left2 && en1.getRoad() == en2.getRoad()); if (ok2){ // the intervals don't overlap if house is removed from en2 if (houses2.size() > 1) possibleRemoves2.add(house); // check if it fits into en1 HousenumberMatch test = checkMoveTo(house, en1, left1); if (test.getRoad() != null){ double deltaDist = test.getDistance() - house.getDistance(); if (deltaDist < smallestDelta){ bestMoveMod = test; bestMoveOrig = house; smallestDelta = deltaDist; bestRemove = en2; } } } } } if (bestMoveMod != null){ if (bestMoveOrig.isDuplicate()){ log.warn("duplicate number causes problems",streetName,bestMoveOrig.getSign(),bestMoveOrig.toBrowseURL() ); } List<HousenumberMatch> fromHouses, toHouses; ExtNumbers from,to; if (bestRemove == en1){ from = en1; to = en2; fromHouses = houses1; toHouses = houses2; bestMoveOrig.setLeft(left2); } else { from = en2; to = en1; fromHouses = houses2; toHouses = houses1; bestMoveOrig.setLeft(left1); } if (bestMoveOrig.getMoved() >= 3){ bestMoveMod = null; bestMoveOrig = null; bestRemove.housenumberRoad.setRandom(true); } else { if (log.isInfoEnabled()){ if (to.getRoad() == from.getRoad()) log.info("moving",streetName,bestMoveOrig.getSign(),bestMoveOrig.getElement().toBrowseURL(),"from",fromHouses,"to",toHouses,"in road",to.getRoad()); else log.info("moving",streetName,bestMoveOrig.getSign(),bestMoveOrig.getElement().toBrowseURL(),"from",fromHouses,"in road",from.getRoad(),"to",toHouses,"in road",to.getRoad()); } bestMoveOrig.incMoved(); bestMoveOrig.setRoad(to.getRoad()); bestMoveOrig.setHousenumberRoad(to.housenumberRoad); bestMoveOrig.setSegment(bestMoveMod.getSegment()); bestMoveOrig.setDistance(bestMoveMod.getDistance()); bestMoveOrig.setSegmentFrac(bestMoveMod.getSegmentFrac()); from.housenumberRoad.getHouses().remove(bestMoveOrig); fromHouses.remove(bestMoveOrig); toHouses.add(bestMoveOrig); Collections.sort(toHouses, new HousenumberMatchByPosComparator()); en1.reset(); en2.reset(); en1.setNumbers(houses1, en1.startInRoad, en1.endInRoad, left1); en2.setNumbers(houses2, en2.startInRoad, en2.endInRoad, left2); return OK_AFTER_CHANGES; } } } ExtNumbers toSplit = null; int splitNum = -1; int delta1 = Math.abs(e1-s1); int delta2 = Math.abs(e2-s2); if (delta1 > 0 && delta2 > 0){ if (en1.hasGaps != en2.hasGaps){ if (en1.hasGaps){ if (possibleRemoves1.isEmpty() == false) splitNum = possibleRemoves1.get(0).getHousenumber(); toSplit = en1; } else { if (possibleRemoves2.isEmpty() == false) splitNum = possibleRemoves2.get(0).getHousenumber(); toSplit = en2; } } else if (possibleRemoves1.size() == 1){ splitNum = possibleRemoves1.get(0).getHousenumber(); toSplit = en1; } else if (possibleRemoves2.size() == 1){ splitNum = possibleRemoves2.get(0).getHousenumber(); toSplit = en2; } else if (possibleRemoves1.size() > 0){ splitNum = possibleRemoves1.get(0).getHousenumber(); toSplit = en1; } else if (possibleRemoves2.size() > 0){ splitNum = possibleRemoves2.get(0).getHousenumber(); toSplit = en2; } else { // intervals are overlapping, a single remove doesn't help if (ivl1.isContained(s2, left1) && ivl1.isContained(e2, left1)){ // en2 is completely in en1 toSplit = en1; splitNum = s2; } else if (ivl2.isContained(s1, left2) && ivl2.isContained(e1, left2)){ // en1 is completely in en2 toSplit = en2; splitNum = s1; } else { if (ivl1.isContained(s2, left1)){ toSplit = en1; splitNum = s2; } else if (ivl1.isContained(e2, left1)){ toSplit = en1; splitNum = e2; } else if (ivl2.isContained(s1, left2)){ toSplit = en2; splitNum = s1; } else if (ivl2.isContained(e1, left2)){ toSplit = en2; splitNum = e1; } else if (style1 == NumberStyle.BOTH){ toSplit = en1; } else if (style2 == NumberStyle.BOTH){ toSplit = en2; } else { toSplit = (delta1 >= delta2) ? en1 : en2; } } } } else if (delta1 == 0 && delta2 > 0 && countOccurence(houses2, s1) == 0){ toSplit = en2; splitNum = s1; } else if (delta2 == 0 && delta1 > 0 && countOccurence(houses1, s2) == 0){ toSplit = en1; splitNum = s2; } if (toSplit != null){ toSplit.worstHouse = null; toSplit.badNum = splitNum; toSplit.setNeedsSplit(true); return NOT_OK_TRY_SPLIT; } allOK = false; } } if (allOK) return OK_NO_CHANGES; return NOT_OK_KEEP; } private static HousenumberMatch checkMoveTo(HousenumberMatch house, ExtNumbers other, boolean otherLeft){ HousenumberMatch test = new HousenumberMatch(house); Numbers otherIvl = other.getNumbers(); int oStart = otherIvl.getStart(otherLeft); int oEnd = otherIvl.getEnd(otherLeft); // check if it fits into en2 if (house.getHousenumber() <= Math.min(oStart, oEnd) ||house.getHousenumber() >= Math.max(oStart, oEnd)) return test; boolean even = house.getHousenumber() % 2 == 0; NumberStyle oStyle = otherIvl.getNumberStyle(otherLeft); if (oStyle == NumberStyle.EVEN && !even || oStyle == NumberStyle.ODD && even) return test; HousenumberGenerator.findClosestRoadSegment(test, other.getRoad(), other.startInRoad, other.endInRoad); if (test.getDistance() <= HousenumberGenerator.MAX_DISTANCE_TO_ROAD){ Coord c1 = other.getRoad().getPoints().get(test.getSegment()); Coord c2 = other.getRoad().getPoints().get(test.getSegment() + 1); if (c1.highPrecEquals(c2) || otherLeft == HousenumberGenerator.isLeft(c1, c2, house.getLocation())){ test.setLeft(otherLeft); return test; } } test.setRoad(null); return test; } /** * Check the start and end values of two consecutive number intervals * for plausibility. * @param s1 1st interval start * @param e1 1st interval end * @param s2 2nd interval start * @param e2 2nd interval end * @return */ private static boolean checkIntervalBoundaries(int s1, int e1, int s2,int e2, boolean sameSide){ boolean ok = false; // many cases, maybe someone finds simpler code? if (sameSide){ // allow equal numbers at boundaries if (s1 == e1) { if (e1 == s2) ok = true; // 6 6 6 4 , 6 6 6 6 , 6 6 6 8 else if (e1 < s2 && e1 < e2) ok = true; // 4 4 8 6, 4 4 6 8 1st equal, not in higher 2nd else if (e1 > s2 && e1 > e2) ok = true; // 6 6 4 2, 6 6 2 4 1st equal, not in lower 2nd } else if (s1 < e1){ if (e1 <= s2 && s2 <= e2) ok = true; // 6 8 8 8, 6 8 8 10, 6 8 10 10, 6 8 10 12 up else if (s2 > e2 && e1 < e2) ok = true; // 6 8 12 10 up down, no overlap, 2nd is higher else if (s1 > s2 && s1 > e2) ok = true; // 6 8 4 4, 6 8 4 2, 6 8 2 4 up down, 2nd is lower } else { // s1 > e1 if (e1 >= s2 && s2 >= e2) ok = true; // 8 6 6 6, 8 6 6 4, 8 6 4 4, 8 6 4 2 down else if (e1 > s2 && e1 > e2) ok = true; // 8 6 2 4 down up, no overlap,2nd is lower else if (s1 < s2 && s1 < e2) ok = true; // 8 6 10 10, 8 6 10 12, 8 6 12 10, 1st down, no overlap,2nd is higher } } else { // left and right: don't allow equal numbers in different intervals if (s1 == e1) { if (s2 == e2 && s1 != s2) ok = true; // 6 6 2 2, 6 6 8 8 else if (s2 < e2 && (s1 < s2 || s1 > e2)) ok = true; // 6 6 2 4 , 6 6 8 10 else if (s2 > e2 && (s1 > s2 || s1 < e2)) ok = true; // 6 6 4 2, 6 6 10 8 } else if (s1 < e1){ if (e1 < s2 && s2 <= e2) ok = true; // 6 8 10 10, 6 8 10 12 up else if (s2 > e2 && e1 < e2) ok = true; // 6 8 12 10 up down, no overlap, 2nd is higher else if (s1 > s2 && s1 > e2) ok = true; // 6 8 4 4, 6 8 4 2, 6 8 2 4 up down, 2nd is lower } else { // s1 > e1 if (e1 > s2 && s2 >= e2) ok = true; // 8 6 4 4, 8 6 4 2 down else if (e1 > s2 && e1 > e2) ok = true; // 8 6 2 4 down up, no overlap,2nd is lower else if (s1 < s2 && s1 < e2) ok = true; // 8 6 10 10, 8 6 10 12, 8 6 12 10, 1st down, no overlap,2nd is higher } } if (!ok){ // log.error("interval check not ok: ", s1,e1,s2,e2,"same side:",sameSide); } return ok; } private ExtNumbers simulateRemovalOfHouseNumber(int hn, boolean left){ ExtNumbers help = new ExtNumbers(housenumberRoad); help.prev = prev; help.next = next; List<HousenumberMatch> modifiedHouses = new ArrayList<>(getHouses(left)); Iterator<HousenumberMatch> iter = modifiedHouses.iterator(); while (iter.hasNext()){ HousenumberMatch house = iter.next(); if (house.getHousenumber() == hn) iter.remove(); } help.setNumbers(modifiedHouses, startInRoad, endInRoad, left); help.setNumbers(getHouses(!left), startInRoad, endInRoad, !left); return help; } public boolean hasNumbers(){ return getNumbers().isEmpty() == false; } /** * Try to add node(s) to decrease the distance of the calculated * position of an address. * @return */ public ExtNumbers splitLargeGaps(){ if (hasNumbers() == false) return this; // calculate the length of each road segment and // the overall length covered by this interval int numSegments = endInRoad - startInRoad; double[] segmentLenghts = new double[numSegments]; double fullLength = 0; for (int i = startInRoad; i < endInRoad; i++){ Coord c1 = getRoad().getPoints().get(i); Coord c2 = getRoad().getPoints().get(i+1); double len = c1.distance(c2); segmentLenghts[i-startInRoad] = len; fullLength += len; } if (fullLength < MAX_LOCATE_ERROR){ if (log.isDebugEnabled()) log.debug("segment",this.getNumbers(), "with length",formatLen(fullLength),"is considered OK"); return this; } TreeMap<Integer, Double> searchPositions = new TreeMap<>(); boolean ok = calcSearchPositions(fullLength, searchPositions); if (!ok) return this; double worstDelta = 0; worstHouse = null; for (int side = 0; side < 2; side++){ boolean left = side == 0; List<HousenumberMatch> houses = getHouses(left); for (HousenumberMatch house : houses){ double distToStart = 0; for (int k = startInRoad; k < house.getSegment(); k++) distToStart += segmentLenghts[k-startInRoad]; if (house.getSegmentFrac() > 0){ try { distToStart += Math.min(1, house.getSegmentFrac()) * segmentLenghts[house.getSegment() - startInRoad]; } catch (Exception e) { log.error(e); } } Double searchDist = searchPositions.get(house.getHousenumber()); if (searchDist == null){ log.warn("can't compute address search result of",house); } else { double delta = distToStart - searchDist; house.setSearchDist(delta); if (Math.abs(delta) > worstDelta){ worstDelta = Math.abs(delta); worstHouse = house; } } } } if (worstDelta > MAX_LOCATE_ERROR){ if (log.isInfoEnabled()) log.info("trying to optimize address search for house number in road",getRoad(),worstHouse,"error before opt is",formatLen(worstDelta)); return tryChange(SR_OPT_LEN); } if (log.isDebugEnabled()) log.debug("segment",this.getNumbers(), "with length",formatLen(fullLength),"is OK, worst address search for house number in road",getRoad(),worstHouse,"error is",formatLen(worstDelta)); return this; } /** * Try to simulate a Garmin address search for each number covered by the interval. * @param fullLength * @param searchPositions filled by this routine * @return false if calculation failed */ private boolean calcSearchPositions(double fullLength, TreeMap<Integer, Double> searchPositions){ Numbers ivl = getNumbers(); for (int side = 0; side < 2; side++){ boolean left = side == 0; NumberStyle style = ivl.getNumberStyle(left); if (style != NumberStyle.NONE){ int start = ivl.getStart(left); int end = ivl.getEnd(left); int step = style == NumberStyle.BOTH ? 1 : 2; if (step != 1 && start % 2 != end % 2){ log.error("internal error, bad interval in optimization",this); return false; } if (start == end){ searchPositions.put(start, fullLength / 2); } else { int parts = Math.abs(end - start) / step; double partLen = fullLength / parts; if (start > end) step = -step; int hn = start; double dist = 0; while (true) { searchPositions.put(hn, dist); if (hn == end) break; dist += partLen; hn += step; } if (parts > 1) assert Math.abs(fullLength - dist) < 0.1; } } } return true; } /** * Use Bresenham algorithm to get the Garmin points which are close to the line * described by c1 and c2 and the point p. * @param c1 * @param c2 * @param p * @return point with smallest perpendicular distance to line */ public static Coord rasterLineNearPoint(Coord c1, Coord c2, Coord p, boolean includeEndPoints){ int x0 = c1.getLongitude(); int y0 = c1.getLatitude(); int x1 = c2.getLongitude(); int y1 = c2.getLatitude(); Coord c1Dspl = c1.getDisplayedCoord(); Coord c2Dspl = c2.getDisplayedCoord(); int x = x0, y = y0; int dx = Math.abs(x1-x), sx = x<x1 ? 1 : -1; int dy = -Math.abs(y1-y), sy = y<y1 ? 1 : -1; int err = dx+dy, e2; /* error value e_xy */ double minDistLine = Double.MAX_VALUE; double minDistTarget = Double.MAX_VALUE; int bestX = Integer.MAX_VALUE, bestY = Integer.MAX_VALUE; for(;;){ /* loop */ if (!includeEndPoints && x==x1 && y==y1) break; if (Math.abs(y - p.getLatitude()) <= 1 || Math.abs(x - p.getLongitude()) <= 1){ Coord t = new Coord(y, x); double distToTarget = t.distance(p); if (includeEndPoints || x != x0 || y != y0){ if (distToTarget < 10){ double distLine = t.distToLineSegment(c1Dspl, c2Dspl); if (distLine < minDistLine || distLine == minDistLine && distToTarget < minDistTarget || distLine < 0.2 && distToTarget < minDistTarget){ bestX = x; bestY = y; minDistLine = distLine; minDistTarget = distToTarget; } } } } if (x==x1 && y==y1) break; e2 = 2*err; if (e2 > dy) { err += dy; x += sx; } /* e_xy+e_x > 0 */ if (e2 < dx) { err += dx; y += sy; } /* e_xy+e_y < 0 */ } if (minDistLine == Double.MAX_VALUE) return null; Coord best = new Coord(bestY, bestX); return best; } /** * Use Bresenham algorithm to get the Garmin points which are close to the line * described by c1 and c2 and the point p. * @param c1 * @param c2 * @param p * @param maxBefore tolerated distance before p * @param maxAfter tolerated distance after p * @return sorted map with closest points */ public static TreeMap<Double,List<Coord>> rasterLineNearPoint2(Coord c1, Coord c2, Coord p, double maxBefore, double maxAfter){ int x0 = c1.getLongitude(); int y0 = c1.getLatitude(); int x1 = c2.getLongitude(); int y1 = c2.getLatitude(); Coord c1Dspl = c1.getDisplayedCoord(); Coord c2Dspl = c2.getDisplayedCoord(); int x = x0, y = y0; int dx = Math.abs(x1-x), sx = x<x1 ? 1 : -1; int dy = -Math.abs(y1-y), sy = y<y1 ? 1 : -1; int err = dx+dy, e2; /* error value e_xy */ TreeMap<Double,List<Coord>> sortedByDistToLine = new TreeMap<>(); boolean beforeTarget = true; double lastDist = Double.NaN; for(;;){ /* loop */ if (Math.abs(y - p.getLatitude()) <= 1 || Math.abs(x - p.getLongitude()) <= 1){ Coord t = new Coord(y, x); double distToTarget = t.distance(p); if (beforeTarget){ if (Double.isNaN(lastDist) == false && lastDist < distToTarget) beforeTarget = false; } if (beforeTarget && distToTarget < maxBefore || !beforeTarget && distToTarget < maxAfter){ Double distLine = t.distToLineSegment(c1Dspl, c2Dspl); List<Coord> list = sortedByDistToLine.get(distLine); if (list == null){ list = new ArrayList<>(); sortedByDistToLine.put(distLine, list); } list.add(t); } lastDist = distToTarget; } if (x==x1 && y==y1) break; e2 = 2*err; if (e2 > dy) { err += dy; x += sx; } /* e_xy+e_x > 0 */ if (e2 < dx) { err += dx; y += sy; } /* e_xy+e_y < 0 */ } return sortedByDistToLine; } /** * Use Bresemham algorithm to get the Garmin points which are close to the line * described by c1 and c2 and the point p. * @param c1 * @param c2 * @param p * @return the list of points */ public static List<Coord> rasterLineNearPoint3(Coord c1, Coord c2, double maxDistToLine){ int x0 = c1.getLongitude(); int y0 = c1.getLatitude(); int x1 = c2.getLongitude(); int y1 = c2.getLatitude(); Coord c1Dspl = c1.getDisplayedCoord(); Coord c2Dspl = c2.getDisplayedCoord(); int x = x0, y = y0; int dx = Math.abs(x1-x), sx = x<x1 ? 1 : -1; int dy = -Math.abs(y1-y), sy = y<y1 ? 1 : -1; int err = dx+dy, e2; /* error value e_xy */ List<Coord> rendered = new ArrayList<>(); for(;;){ /* loop */ Coord t = new Coord(y, x); double distLine = t.distToLineSegment(c1Dspl, c2Dspl); if (distLine <= maxDistToLine) rendered.add(t); if (x==x1 && y==y1) break; e2 = 2*err; if (e2 > dy) { err += dy; x += sx; } /* e_xy+e_x > 0 */ if (e2 < dx) { err += dx; y += sy; } /* e_xy+e_y < 0 */ } return rendered; } private static int countOccurence(List<HousenumberMatch> houses, int num){ int count = 0; for (HousenumberMatch house : houses){ if (house.getHousenumber() == num) count++; } return count; } /** * @param length * @return string with length, e.g. "0.23 m" or "116.12 m" */ private static String formatLen(double length){ return HousenumberGenerator.formatLen(length); } /** * Try to detect the case that many house numbers have no specific order. * */ public void detectRandom() { int countFilledIvls = 0; int countFilledSides = 0; int countNotInOrder = 0; for (ExtNumbers curr = this; curr != null; curr = curr.next){ if (curr.hasNumbers() == false) continue; countFilledIvls++; if (curr.notInOrder(Numbers.LEFT)) countNotInOrder++; if (curr.notInOrder(Numbers.RIGHT)) countNotInOrder++; if (curr.getHouses(Numbers.LEFT).size() > 1) ++countFilledSides; if (curr.getHouses(Numbers.RIGHT).size() > 1) ++countFilledSides; } if (countNotInOrder > 0){ if (countNotInOrder > countFilledIvls || countNotInOrder > 2 || countFilledSides == countNotInOrder) housenumberRoad.setRandom(true); } } private boolean isPlausible(){ if (getNumbers().isPlausible() == false) return false; if (multipleZipOrCity(true) || multipleZipOrCity(false)) return false; return true; } /** * Split this segment into two. * @param splitSegment */ private ExtNumbers split(int splitSegment){ getRoad().getPoints().get(splitSegment).setNumberNode(true); ExtNumbers first = divide(); // distribute the houses. The caller has to make sure that // they are assigned to the wanted segment and sorted. for (int side = 0; side < 2; side++){ boolean left = side == 0; List<HousenumberMatch> houses = getHouses(left); if (houses.isEmpty()) continue; int used = first.setNumbers(houses, startInRoad, splitSegment, left); first.next.setNumbers(houses.subList(used, houses.size()), splitSegment, endInRoad, left); } return first; } public String toString(){ return getRoad().toString() + getHouses(Numbers.LEFT).toString() + getHouses(Numbers.RIGHT).toString(); } }