/*
* 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.Int2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.CoordNode;
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.osmstyle.housenumber.HousenumberGenerator.HousenumberMatchByNumComparator;
import uk.me.parabola.mkgmap.osmstyle.housenumber.HousenumberGenerator.HousenumberMatchByPosComparator;
/**
* Helper class to combine house numbers with MapRoad instances
* @author Gerd Petermann
*
*/
public class HousenumberRoad {
private static final Logger log = Logger.getLogger(HousenumberRoad.class);
private String streetName;
private final MapRoad road;
private CityInfo roadCityInfo;
private ZipCodeInfo roadZipCode;
private ExtNumbers extNumbersHead;
private final List<HousenumberMatch> houseNumbers;
private boolean changed;
private boolean isRandom;
private boolean removeGaps;
private LinkedHashSet<String> furtherNames;
public HousenumberRoad(MapRoad r, CityInfo ci, List<HousenumberMatch> potentialNumbersThisRoad) {
this.streetName = r.getStreet();
this.road = r;
this.roadCityInfo = ci;
this.houseNumbers = new ArrayList<>(potentialNumbersThisRoad);
for (HousenumberMatch house : houseNumbers){
house.setHousenumberRoad(this);
}
}
public void addPlaceName(String name) {
if (furtherNames == null){
furtherNames = new LinkedHashSet<>();
}
furtherNames.add(name);
}
public String getName (){
return streetName;
}
public void buildIntervals() {
Collections.sort(houseNumbers, new HousenumberMatchByNumComparator());
if (log.isInfoEnabled())
log.info("Initial housenumbers for",road,"in",road.getCity(),houseNumbers);
filterRealDuplicates();
filterGroups();
if (houseNumbers.isEmpty())
return;
List<HousenumberMatch> leftNumbers = new ArrayList<HousenumberMatch>();
List<HousenumberMatch> rightNumbers = new ArrayList<HousenumberMatch>();
for (HousenumberMatch house : houseNumbers) {
if (house.getRoad() == null || house.isIgnored()){
continue;
}
if (house.getHousenumberRoad() != this || house.getHousenumberRoad().getRoad() != house.getRoad()){
log.error("internal error, road links are not correct",house.toBrowseURL());
}
if (house.isLeft()) {
leftNumbers.add(house);
} else {
rightNumbers.add(house);
}
}
detectGroups(leftNumbers, rightNumbers);
Collections.sort(leftNumbers, new HousenumberMatchByPosComparator());
Collections.sort(rightNumbers, new HousenumberMatchByPosComparator());
int currNodePos = 0;
int nodeIndex = 0;
int prevNumberNodeIndex = 0;
int prevNodePos = 0;
extNumbersHead = null;
ExtNumbers currNumbers = null;
for (Coord p : road.getPoints()) {
if (currNodePos == 0) {
if (road.skipAddToNOD() == false)
assert p instanceof CoordNode;
}
// An ordinary point in the road.
if (p.isNumberNode() == false) {
currNodePos++;
continue;
}
// The first time round, this is guaranteed to be a CoordNode
if (currNodePos == 0) {
nodeIndex++;
currNodePos++;
continue;
}
// Now we have a CoordNode and it is not the first one.
ExtNumbers numbers = new ExtNumbers(this);
numbers.setNodeIndex(prevNumberNodeIndex);
int leftUsed = numbers.setNumbers(leftNumbers, prevNodePos, currNodePos, true);
int rightUsed = numbers.setNumbers(rightNumbers, prevNodePos, currNodePos, false);
prevNodePos = currNodePos;
// maintain chain
numbers.prev = currNumbers;
if (currNumbers != null)
currNumbers.next = numbers;
else {
extNumbersHead = numbers;
}
currNumbers = numbers;
leftNumbers.subList(0, leftUsed).clear();
rightNumbers.subList(0, rightUsed).clear();
prevNumberNodeIndex = nodeIndex;
nodeIndex++;
currNodePos++;
}
}
/**
* Try to detect groups of houses with continues numbers
* which should be attached to a zero-length segment.
* Very useful when a service road connects eg.
* numbers 7..15 to the named road, but also for just two numbers.
* @param depth
* @param leftNumbers
* @param rightNumbers
*/
private void detectGroups(List<HousenumberMatch> leftNumbers, List<HousenumberMatch> rightNumbers) {
List<HousenumberGroup> groups = new ArrayList<>();
for (int side = 0; side < 2; side++){
boolean left = side == 0;
List<HousenumberMatch> houses = left ? leftNumbers : rightNumbers;
HousenumberGroup group = null;
for (int j = 1; j < houses.size(); j++){
HousenumberMatch house = houses.get(j);
if (group == null){
if (house.isInterpolated())
continue;
HousenumberMatch predHouse = houses.get(j-1);
int deltaNum = predHouse.getHousenumber() - house.getHousenumber();
if (Math.abs(deltaNum) > 2)
continue;
if (HousenumberGroup.housesFormAGroup(predHouse, house))
group = new HousenumberGroup(this, houses.subList(j-1, j+1));
} else {
if (group.tryAddHouse(house) == false){
if(group.verify())
groups.add(group);
group = null;
}
}
}
if (group != null && group.verify()){
groups.add(group);
}
}
if (groups.isEmpty())
return;
boolean nodesAdded = false;
for (HousenumberGroup group : groups){
int oldNumPoints = getRoad().getPoints().size();
if (nodesAdded){
if (group.recalcPositions() == false)
continue;
}
if (group.findSegment(streetName, groups)){
nodesAdded = true;
if (log.isDebugEnabled())
log.debug("added",getRoad().getPoints().size() - oldNumPoints,"number node(s) at",group.linkNode.toDegreeString(),"for group",group,"in road",getRoad());
oldNumPoints = getRoad().getPoints().size();
int minSeg = group.minSeg;
for (HousenumberMatch house : this.houseNumbers){
if (house.getSegment() >= minSeg)
HousenumberGenerator.findClosestRoadSegment(house, getRoad());
}
group.recalcPositions();
} else {
if(group.linkNode != null){
if (log.isDebugEnabled())
log.debug("used existing zero-length-segment at",group.linkNode.toDegreeString(),"for group",group,"in road",getRoad());
}
}
}
return;
}
/**
*/
public void checkIntervals(){
if (extNumbersHead == null)
return;
boolean anyChanges = false;
extNumbersHead.detectRandom();
for (int loop = 0; loop < 10; loop++){
if (loop > 4){
// TODO: 3,4,5 ?
setRandom(true);
}
setChanged(false);
extNumbersHead = extNumbersHead.checkSingleChainSegments(streetName, removeGaps);
extNumbersHead = extNumbersHead.checkChainPlausibility(streetName, houseNumbers);
if (isChanged())
anyChanges = true;
else
break;
}
setChanged(anyChanges);
}
/**
* Identify duplicate numbers and ignore those which are close together
* and those which are probably wrong.
*/
private void filterRealDuplicates() {
List<HousenumberMatch> toIgnore = new ArrayList<>();
final int TO_SEARCH = 6;
int oddLeft = 0, oddRight = 0, evenLeft = 0, evenRight = 0;
for (HousenumberMatch house: houseNumbers){
if (house.isIgnored())
continue;
if (house.isLeft()){
if (house.getHousenumber() % 2 == 0)
evenLeft++;
else
oddLeft++;
} else {
if (house.getHousenumber() % 2 == 0)
evenRight++;
else
oddRight++;
}
}
HousenumberMatch usedForCalc = null;
for (int i = 1; i < houseNumbers.size(); i++){
HousenumberMatch house1 = houseNumbers.get(i - 1);
HousenumberMatch house2 = houseNumbers.get(i);
if (house1.getSign().equals(house2.getSign()) == false){
usedForCalc = null;
} else {
if (house1.isEqualAddress(house2) == false)
continue;
// found a duplicate address (e.g. 2 and 2 or 1b and 1b in same road,city etc.)
double distBetweenHouses = house2.getLocation().distance(house1.getLocation());
double distToUsed = (usedForCalc == null) ? distBetweenHouses : house2.getLocation().distance(usedForCalc.getLocation());
if (usedForCalc == null)
usedForCalc = (house1.getDistance() < house2.getDistance()) ? house1 : house2;
else {
house1 = usedForCalc;
}
boolean sameSide = (house2.isLeft() == house1.isLeft());
if (log.isDebugEnabled())
log.debug("analysing duplicate address",streetName,house1.getSign(),"for road with id",getRoad().getRoadDef().getId());
if (sameSide && (distBetweenHouses < 100 || distToUsed < 100)){
HousenumberMatch obsolete = house1 == usedForCalc ? house2 : house1;
if (log.isDebugEnabled())
log.debug("house",obsolete,obsolete.toBrowseURL(),"is close to other element and on the same road side, is ignored");
toIgnore.add(obsolete);
continue;
}
if (!sameSide){
if (log.isDebugEnabled())
log.debug("oddLeft, oddRight, evenLeft, evenRight:",oddLeft, oddRight, evenLeft, evenRight);
HousenumberMatch wrongSide = null;
if (house2.getHousenumber() % 2 == 0){
if (evenLeft == 1 && (oddLeft > 1 || evenRight > 0 && oddRight == 0)){
wrongSide = house2.isLeft() ? house2: house1;
}
if (evenRight == 1 && (oddRight > 1 || evenLeft > 0 && oddLeft == 0)){
wrongSide = !house2.isLeft() ? house2: house1;
}
} else {
if (oddLeft == 1 && (evenLeft > 1 || oddRight > 0 && evenRight == 0)){
wrongSide = house2.isLeft() ? house2: house1;
}
if (oddRight == 1 && (evenRight > 1 || oddLeft > 0 && evenLeft == 0)){
wrongSide = !house2.isLeft() ? house2: house1;
}
}
if (wrongSide != null){
if (log.isDebugEnabled())
log.debug("house",streetName,wrongSide.getSign(),"from",wrongSide.toBrowseURL(),"seems to be wrong, is ignored");
toIgnore.add(wrongSide);
continue;
}
}
double[] sumDist = new double[2];
double[] sumDistSameSide = new double[2];
int[] confirmed = new int[2];
int[] falsified = new int[2];
int[] found = new int[2];
List<HousenumberMatch> dups = Arrays.asList(house2, house1);
for (int k = 0; k < dups.size(); k++){
HousenumberMatch other, curr;
if (k == 0){
curr = dups.get(0);
other = dups.get(1);
} else {
curr = dups.get(1);
other = dups.get(0);
}
int pos = houseNumbers.indexOf(curr);
int left = pos - 1;
int right = pos + 1;
HousenumberMatch nearHouse;
int stillToFind = TO_SEARCH;
while (stillToFind > 0){
int oldDone = stillToFind;
if (left >= 0){
nearHouse = houseNumbers.get(left);
if (nearHouse != other){
double dist = curr.getLocation().distance(nearHouse.getLocation());
sumDist[k] += dist;
if (nearHouse.isLeft() == curr.isLeft()){
sumDistSameSide[k] += dist;
}
if (curr.getHousenumber() == nearHouse.getHousenumber()){
if (dist < 20)
confirmed[k]++;
} else {
if (dist < 10 )
falsified[k]++;
}
}
--left;
stillToFind--;
if (stillToFind == 0)
break;
}
if (right < houseNumbers.size()){
nearHouse = houseNumbers.get(right);
if (nearHouse != other){
double dist = curr.getLocation().distance(nearHouse.getLocation());
sumDist[k] += dist;
if (nearHouse.isLeft() == curr.isLeft()){
sumDistSameSide[k] += dist;
}
if (curr.getHousenumber() == nearHouse.getHousenumber()){
if (dist < 40)
confirmed[k]++;
} else {
if (dist < 10 )
falsified[k]++;
}
}
stillToFind--;
right++;
}
if (oldDone == stillToFind)
break;
}
found[k] = TO_SEARCH - 1 - stillToFind;
}
if (log.isDebugEnabled()){
log.debug("dup check 1:", streetName, house1, house1.toBrowseURL());
log.debug("dup check 2:", streetName, house2, house2.toBrowseURL());
log.debug("confirmed",Arrays.toString(confirmed),"falsified",Arrays.toString(falsified),"sum-dist",Arrays.toString(sumDist),"sum-dist-same-side",Arrays.toString(sumDistSameSide));
}
HousenumberMatch bad = null;
if (confirmed[1] > 0 && confirmed[0] == 0 && falsified[1] == 0)
bad = dups.get(0);
else if (confirmed[0] > 0 && confirmed[1] == 0 && falsified[0] == 0)
bad = dups.get(1);
else if (found[0] > 3 && sumDist[0] > sumDist[1] && sumDistSameSide[0] > sumDistSameSide[1])
bad = dups.get(0);
else if (found[1] > 3 && sumDist[1] > sumDist[0] && sumDistSameSide[1] > sumDistSameSide[0])
bad = dups.get(1);
if (bad != null){
toIgnore.add(bad);
} else {
if (log.isDebugEnabled())
log.debug("duplicate house number, don't know which one to use, ignoring both");
toIgnore.add(house1);
toIgnore.add(house2);
house2.setIgnored(true);
house1.setIgnored(true);
}
}
}
for (HousenumberMatch house : toIgnore){
if (log.isInfoEnabled())
log.info("duplicate housenumber",streetName,house.getSign(),"is ignored for road with id",house.getRoad().getRoadDef().getId(),",house:",house.toBrowseURL());
houseNumbers.remove(house);
}
}
/**
* Identify groups of buildings with numbers like 1a,1b,1c.
* The list in housenumbers is sorted so that 2 appears before 2a and
* 2b appears before 2c.
* XXX This is quite aggressive, maybe we have to add more logic here.
*/
private void filterGroups() {
if (houseNumbers.size() <= 1)
return;
HousenumberMatch prev = houseNumbers.get(0);
HousenumberMatch used = null;
for (int i = 1; i < houseNumbers.size(); i++){
HousenumberMatch house = houseNumbers.get(i);
if (house.getHousenumber() != prev.getHousenumber())
used = null;
else {
if (used == null)
used = prev;
if (prev.getSign().equals(house.getSign()) && prev.isEqualAddress(house) == false){
// we want to keep these duplicates
} else {
house.setIgnored(true);
if (log.isInfoEnabled())
log.info("using",streetName,used.getSign(), "in favor of",house.getSign(),"as target for address search");
}
}
prev = house;
}
}
public void checkWrongRoadAssignmments(HousenumberRoad other) {
if (this.extNumbersHead == null || other.extNumbersHead == null)
return;
for (int loop = 0; loop < 10; loop++){
boolean changed = false;
ExtNumbers head1 = this.extNumbersHead;
for (ExtNumbers en1 = head1; en1 != null; en1 = en1.next){
if (changed)
break;
if (en1.hasNumbers() == false)
continue;
ExtNumbers head2 = other.extNumbersHead;
for (ExtNumbers en2 = head2; en2 != null; en2 = en2.next){
if (changed)
break;
if (en2.hasNumbers() == false)
continue;
int res = ExtNumbers.checkIntervals(streetName, en1, en2);
switch (res) {
case ExtNumbers.OK_NO_CHANGES:
case ExtNumbers.NOT_OK_KEEP:
break;
case ExtNumbers.OK_AFTER_CHANGES:
changed = true;
this.setChanged(true);
other.setChanged(true);
break;
case ExtNumbers.NOT_OK_TRY_SPLIT:
if (en1.needsSplit()){
ExtNumbers test = en1.tryChange(ExtNumbers.SR_FIX_ERROR);
if (test != en1){
changed = true;
if (test.prev == null){
this.extNumbersHead = test;
}
}
}
if (en2.needsSplit()){
ExtNumbers test = en2.tryChange(ExtNumbers.SR_FIX_ERROR);
if (test != en2){
changed = true;
if (test.prev == null){
other.extNumbersHead = test;
}
}
}
break;
case ExtNumbers.NOT_OK_STOP:
return;
default:
log.error("can't fix",en1,en2);
}
}
}
if (!changed)
break;
}
}
public void setNumbers() {
if (extNumbersHead == null)
return;
if (houseNumbers.isEmpty())
return;
// make sure that the name we used for the cluster is also attached to the road
if (streetName == null){
log.error("found no name for road with housenumbers, implement a move to the next named road ?",road);
return;
}
String[] labels = road.getLabels();
boolean found = false;
for (String label : labels){
if (label == null)
break;
if (streetName.equals(label))
found = true;
}
if (!found){
if (labels[0] == null){
// add empty label so that the address search name doesn't appear in the map
// when the original road did not have any label
labels[0] = "";
}
for (int i = 1; i < labels.length; i++){
if (labels[i] == null){
labels[i] = streetName;
log.info("added label",streetName,"for",road,"Labels are now:",Arrays.toString(labels));
found = true;
break;
}
}
}
if (!found){
int last = labels.length-1;
String droppedLabel = labels[last];
labels[last] = streetName;
if (droppedLabel != null){
if (log.isInfoEnabled())
log.info("dropped label",droppedLabel,"for",road,"in preference to correct address search. Labels are now:",Arrays.toString(labels));
}
}
if (furtherNames != null){
boolean changed = false;
for (String furtherName : furtherNames){
if (road.addLabel(furtherName))
changed = true;
}
if (changed){
log.info("added further labels for",road,"Labels are now:",Arrays.toString(labels));
}
}
if (road.getZip() == null && roadZipCode != null){
road.setZip(roadZipCode.getZipCode());
}
road.setNumbers(extNumbersHead.getNumberList());
}
public MapRoad getRoad(){
return road;
}
public CityInfo getRoadCityInfo() {
return roadCityInfo;
}
public ZipCodeInfo getRoadZipCode() {
return roadZipCode;
}
public boolean isChanged() {
return changed;
}
public void setChanged(boolean changed) {
this.changed = changed;
}
public boolean isRandom() {
return isRandom;
}
public void setRandom(boolean isRandom) {
if (this.isRandom == false)
if (log.isDebugEnabled())
log.debug("detected random case",this);
this.isRandom = isRandom;
}
public void setRemoveGaps(boolean b) {
removeGaps = true;
}
public boolean getRemoveGaps() {
return removeGaps;
}
/**
*
*/
public void improveSearchResults() {
ExtNumbers curr = extNumbersHead;
while (curr != null) {
ExtNumbers en = curr.splitLargeGaps();
if (en != curr) {
if (en.hasNumbers() && en.next != null && en.next.hasNumbers())
setChanged(true);
else {
ExtNumbers test = en.hasNumbers() ? en : en.next;
if (test.getNumbers().isSimilar(curr.getNumbers()) == false)
setChanged(true);
}
if (curr.prev == null)
extNumbersHead = en;
curr = en;
continue;
}
curr = curr.next;
}
}
public String toString(){
return getRoad().toString() + " " + houseNumbers;
}
/**
* Check if street name is set, if not, try to find one.
* Identify those houses which are assigned to this road because it was the closest,
* but can't be correct because street name doesn't match.
*
* @param road2HousenumberRoadMap maps {@link MapRoad} instances to corresponding
* {@link HousenumberRoad} instances
* @param nodeId2RoadLists maps node ids to the {@link MapRoad} that use the corresponding nodes.
* @return
*/
public List<HousenumberMatch> checkStreetName(Map<MapRoad, HousenumberRoad> road2HousenumberRoadMap, Int2ObjectOpenHashMap<HashSet<MapRoad>> nodeId2RoadLists) {
List<HousenumberMatch> noWrongHouses = Collections.emptyList();
List<HousenumberMatch> wrongHouses = Collections.emptyList();
double minDist = Double.MAX_VALUE;
double maxDist = 0;
if (houseNumbers.isEmpty() == false){
HashMap<String, Integer>possibleStreetNamesFromHouses = new HashMap<>();
HashMap<String, Integer>possiblePlaceNamesFromHouses = new HashMap<>();
for (HousenumberMatch house : houseNumbers){
if (house.getDistance() > maxDist)
maxDist = house.getDistance();
if (house.getDistance() < minDist)
minDist = house.getDistance();
String potentialName = house.getStreet();
if (potentialName != null){
Integer oldCount = possibleStreetNamesFromHouses.put(potentialName, 1);
if (oldCount != null)
possibleStreetNamesFromHouses.put(potentialName, oldCount + 1);
}
String placeName = house.getPlace();
if (placeName != null){
Integer oldCount = possiblePlaceNamesFromHouses.put(placeName, 1);
if (oldCount != null)
possiblePlaceNamesFromHouses.put(placeName, oldCount + 1);
}
}
HashSet<String> connectedRoadNames = new HashSet<>();
for (Coord co : road.getPoints()){
if (co.getId() == 0)
continue;
HashSet<MapRoad> connectedRoads = nodeId2RoadLists.get(co.getId());
for (MapRoad r : connectedRoads){
if (r.getStreet() != null)
connectedRoadNames.add(r.getStreet());
}
}
if (streetName != null){
if (possibleStreetNamesFromHouses.isEmpty()){
// ok, houses have no street name
return noWrongHouses;
}
if (possibleStreetNamesFromHouses.size() == 1){
if (possibleStreetNamesFromHouses.containsKey(streetName)){
// ok, houses have same name as street
return noWrongHouses;
}
}
}
if (possibleStreetNamesFromHouses.isEmpty()){
// neither road not houses tell us a street name
if (furtherNames != null && furtherNames.size() > 0){
Iterator<String> iter = furtherNames.iterator();
streetName = iter.next();
iter.remove();
if (furtherNames.isEmpty())
furtherNames = null;
}
return noWrongHouses;
}
if (streetName == null){
if (possibleStreetNamesFromHouses.size() == 1){
String potentialName = possibleStreetNamesFromHouses.keySet().iterator().next();
boolean nameOK = false;
if (connectedRoadNames.contains(potentialName))
nameOK = true;
else if (houseNumbers.size() > 1){
nameOK = true;
} else if (maxDist <= 10){
nameOK = true;
}
if (nameOK){
streetName = potentialName;
return noWrongHouses; // all good, return empty list
}
} else {
List<String> matchingNames = new ArrayList<>();
for (Entry<String, Integer> entry : possibleStreetNamesFromHouses.entrySet()){
String name = entry.getKey();
if (connectedRoadNames.contains(name)){
matchingNames.add(name);
}
}
if (matchingNames.size() == 1){
streetName = matchingNames.get(0);
}
}
}
// if we get here we have no usable street name
wrongHouses = new ArrayList<>();
Iterator<HousenumberMatch> iter = houseNumbers.iterator();
while (iter.hasNext()){
HousenumberMatch house = iter.next();
if (streetName != null){
if (house.getStreet() == null || streetName.equalsIgnoreCase(house.getStreet()))
continue;
} else if (house.getPlace() != null)
continue;
double bestDist = Double.MAX_VALUE;
HousenumberMatch best = null;
for (MapRoad altRoad : house.getAlternativeRoads()){
if (house.getStreet() != null){
if (house.getStreet().equals(altRoad.getStreet())){
HousenumberMatch test = new HousenumberMatch(house);
HousenumberGenerator.findClosestRoadSegment(test, altRoad);
if (test.getDistance() < bestDist){
best = test;
bestDist = test.getDistance();
}
}
}
}
iter.remove();
if (best != null){
best.calcRoadSide();
wrongHouses.add(best);
} else {
log.warn("found no plausible road for address",house.getStreet(),house,house.toBrowseURL());
}
}
}
return wrongHouses;
}
public void addHouse(HousenumberMatch house) {
if (extNumbersHead != null){
log.error("internal error: trying to add house to road that was already processed",this.getRoad(),house);
}
house.setHousenumberRoad(this);
houseNumbers.add(house);
}
public List<HousenumberMatch> getHouses() {
return houseNumbers;
}
public void setZipCodeInfo(ZipCodeInfo zipInfo) {
roadZipCode = zipInfo;
}
}