/**
* This file is part of OSM2ShareNav
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* Copyright (C) 2007 Harald Mueller
* Copyright (C) 2007, 2008 Kai Krueger
*/
package net.sharenav.osmToShareNav;
import java.lang.Long;
import java.util.HashMap;
import java.util.List;
import net.sharenav.osmToShareNav.model.Entity;
import net.sharenav.osmToShareNav.model.Node;
import net.sharenav.osmToShareNav.model.Way;
import net.sharenav.osmToShareNav.model.POIdescription;
import net.sharenav.osmToShareNav.model.WayDescription;
import edu.wlu.cs.levy.CG.KDTree;
import edu.wlu.cs.levy.CG.KeyDuplicateException;
import edu.wlu.cs.levy.CG.KeySizeException;
public class CalcNearBy {
private int kdSize = 0; // Hack around the fact that KD-tree doesn't tell us its size
private static int kdWaysSize = 0;
private static KDTree nearByWays;
private int exactCount = 0;
private int heuristicCount = 0;
public CalcNearBy(OsmParser parser) {
KDTree nearByElements = getNearByElements(parser);
if (Configuration.getConfiguration().useHouseNumbers) {
nearByWays = getNearByWays(parser);
}
if (kdSize > 0) {
calcCityNearBy(parser, nearByElements);
calcWayIsIn(parser, nearByElements);
}
if (kdWaysSize > 0) {
CalcWaysForHouseNumberAreas(parser, nearByWays);
calcWaysForHouseNumbers(parser, nearByWays);
}
}
// FIXME: this is a pretty crude and error-prone way to do this, rewrite to be better
// should use a better algorithm for findind the proper way
// ShareNav has a place where the nearest way to a destination is found,
// perhaps that can be used.
// sk750 April 2011: What you want to do with finding the closest way for a house number in Osm2ShareNav
// might be a mixture of closest point on a line in ShareNav
// and the traffic signal route node marking in Osm2ShareNav -
// though I this misses marking some route nodes
// because it doesn't look over tile boundaries for performance reasons.
//
// Plan:
// 1) get a bunch of nearby (by midpoint) named ways (maybe two dozen or so),
// 2) if addr:street matches to exactly one of the ways, mark that way as associated
// 3) order the ways by distance from the node to the closest point of the way
// 4) mark the nearest way as associated
// 5) maybe mark also one or two other nearest ways as associated, depending on .properties config
// (false nearby positives might be considered a lesser evil than not finding a housenumber)
// (the storage model currently doesn't allow for several way, so that would
// involve a change to storage or cloning of the node)
// Update: plan carried out except for marking muptiple ways and sorting addr:street matches
public long calcWayForHouseNumber(Entity n, HashMap<Long, Way> wayHashMap) {
POIdescription poiDesc = null;
WayDescription wayDesc = null;
if (n instanceof Way) {
wayDesc = Configuration.getConfiguration().getWayDesc(((Way) n).getType(null));
} else {
poiDesc = Configuration.getConfiguration().getpoiDesc(((Node) n).getType(null));
}
//System.out.println("poidesc: " + poiDesc + " waydesc: " + wayDesc);
String streetNameAttr = "addr:street";
if (poiDesc != null && poiDesc.houseNumberMatchTag != null) {
streetNameAttr = poiDesc.houseNumberMatchTag;
//System.out.println("Streetnameattr: " + streetNameAttr);
}
if (wayDesc != null && wayDesc.houseNumberMatchTag != null) {
streetNameAttr = wayDesc.houseNumberMatchTag;
//System.out.println("Streetnameattr: " + streetNameAttr);
}
String streetName = n.getAttribute(streetNameAttr);
//System.out.println("Streetnameattr: " + streetNameAttr);
Node nearestWay = null;
try {
Node thisNode = null;
if (n instanceof Node) {
thisNode = (Node) n;
} else {
Way w = (Way) n;
thisNode = w.getMidPoint();
}
nearestWay = (Node) nearByWays.nearest(MyMath.latlon2XYZ(thisNode));
long maxDistanceTested = MyMath.dist(thisNode, nearestWay);
int retrieveN = 25;
int retrieveNforName = 100;
if (retrieveN > kdWaysSize) {
retrieveN = kdWaysSize;
}
if (retrieveNforName > kdWaysSize) {
retrieveNforName = kdWaysSize;
}
nearestWay = null;
long dist = 0;
Object [] nearWays = null;
// first look for matching street name in nearby streets
dist = 0;
nearWays = nearByWays.nearest(MyMath.latlon2XYZ(thisNode), retrieveNforName);
for (Object o : nearWays) {
Node other = (Node) o;
dist = MyMath.dist(thisNode, other);
String otherName = other.getAttribute("__wayname");
if (otherName != null && streetName != null) {
//System.out.println ("trying to match addr:street, comparing " + streetName + " to " + otherName);
// FIXME to be pedantically correct, could do this after finding closest points in the
// candidate ways. Probably not worth the cost, as apparently the only thing this would
// change in some cases ist that is_in info for housenumber streetname would be different
// in some cases
if (streetName.equalsIgnoreCase(otherName)) {
nearestWay = other;
exactCount++;
break;
}
}
}
//if (nearestWay != null) {
// System.out.println ("found addr:street match for node " + n + " : street: " + nearestWay);
//}
maxDistanceTested = dist;
if (nearestWay == null) {
//System.out.println ("Start heuristic way search for " + thisNode);
nearestWay = (Node) nearByWays.nearest(MyMath.latlon2XYZ(thisNode));
maxDistanceTested = MyMath.dist(thisNode, nearestWay);
nearestWay = null;
retrieveN = 25;
dist = maxDistanceTested;
if (retrieveN > kdWaysSize) {
retrieveN = kdWaysSize;
}
//System.out.println ("First way " + nearestWay + " at dist " + dist);
nearWays = nearByWays.nearest(MyMath.latlon2XYZ(thisNode), retrieveN);
// then look for other named ways
for (Object o : nearWays) {
Node other = (Node) o;
if (nearestWay == null) {
nearestWay = other;
}
//dist = MyMath.dist(thisNode, other);
//As the list returned by the kd-tree is sorted by distance,
//we can stop at the first found plus some (to match for street name)
// FIXME add calculation for real distance to street
long wayId = other.id;
Way w = wayHashMap.get(wayId);
long distToWay = distanceToWay(thisNode, w, dist);
if (distToWay < dist) {
dist = distToWay;
nearestWay = other;
//System.out.println ("Found closer way " + nearestWay + " at dist " + dist);
}
}
if (nearestWay != null) {
heuristicCount++;
maxDistanceTested = dist;
//found a suitable Way
//System.out.println ("decided a heuristic match for node " + n
// + " streetName: " + nearestWay);
}
}
} catch (KeySizeException e) {
// Something must have gone horribly wrong here,
// This should never happen.
e.printStackTrace();
return 0;
}
if (nearestWay != null) {
return nearestWay.id;
}
return (long) 0;
}
/**
* @param parser
* @param nearByElements
*/
private void calcWayIsIn(OsmParser parser, KDTree nearByElements) {
long startTime = System.currentTimeMillis();
for (Way w : parser.getWays()) {
if (w.getName() != null) {
calcEntityIsIn(parser, nearByElements, (Entity) w);
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("info: calcEntityIsIn for ways took " + time / 1000 + " seconds");
}
startTime = System.currentTimeMillis();
for (Node n : parser.getNodes()) {
if (n.getName() != null) {
calcEntityIsIn(parser, nearByElements, (Entity) n);
}
}
time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("info: calcEntityIsIn for nodes took " + time / 1000 + " seconds");
}
}
private void calcEntityIsIn(OsmParser parser, KDTree nearByElements, Entity e) {
// index way, poi or area with is_in
Node thisNode = null;
if (e instanceof Node) {
thisNode = (Node) e;
}
if (e instanceof Way) {
Way w = (Way) e;
thisNode = w.getMidPoint();
}
if (thisNode == null) {
return;
}
Node nearestPlace = null;
try {
nearestPlace = (Node) nearByElements.nearest(MyMath.latlon2XYZ(thisNode));
if (nearestPlace.getType(null) <= 5 && !(MyMath.dist(thisNode, nearestPlace) < Constants.MAX_DIST_CITY[nearestPlace.getType(null)])) {
long maxDistanceTested = MyMath.dist(thisNode, nearestPlace);
int retrieveN = 5;
if (retrieveN > kdSize) {
retrieveN = kdSize;
}
nearestPlace = null;
while (maxDistanceTested < Constants.MAX_DIST_CITY[Constants.NODE_PLACE_CITY]) {
Object [] nearPlaces = nearByElements.nearest(MyMath.latlon2XYZ(thisNode), retrieveN);
long dist = 0;
for (Object o : nearPlaces) {
Node other = (Node) o;
dist = MyMath.dist(thisNode, other);
//As the list returned by the kd-tree is sorted by distance,
//we can stop at the first found
if (other.getType(null) <= 5 && dist < Constants.MAX_DIST_CITY[other.getType(null)]) {
nearestPlace = other;
break;
}
}
if (nearestPlace != null) {
//found a suitable Place, leaving loop
break;
}
if (retrieveN == kdSize) {
/**
* We have checked all available places and nothing was
* suitable, so abort with nearestPlace == null;
*/
break;
}
maxDistanceTested = dist;
retrieveN = retrieveN * 5;
if (retrieveN > kdSize) {
retrieveN = kdSize;
}
}
}
} catch (KeySizeException exc) {
// Something must have gone horribly wrong here,
// This should never happen.
exc.printStackTrace();
return;
}
if (nearestPlace != null) {
if (!e.containsKey("is_in")) {
e.setAttribute("is_in", nearestPlace.getName());
}
// don't overwrite info from calcCityNearBy()
if (e.nearBy == null) {
e.nearBy = nearestPlace;
}
}
}
/**
* @param parser
* @param nearByElements
*/
private void calcWaysForHouseNumbers(OsmParser parser, KDTree nearByElements) {
long startTime = System.currentTimeMillis();
int count = 0;
int ignoreCount = 0;
HashMap<Long, Way> wayHashMap = parser.getWayHashMap();
for (Node n : parser.getNodes()) {
if (n.hasHouseNumberTag()) {
long way = calcWayForHouseNumber((Entity) n, wayHashMap);
//System.out.println ("Got id " + way + " for housenumber node " + n);
if (way != 0 && !n.containsKey("__wayid")) {
count++;
n.setAttribute("__wayid", Long.toString(way));
Way w = wayHashMap.get(way);
if (w != null) {
w.houseNumberAdd(n);
}
} else {
if (way == 0) {
System.out.println("Warning: ignoring map housenumber data: node: "
+ n + " result from calcWayForHouseNumber: " + way);
ignoreCount++;
}
}
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("info: node housenumbers: accepted " + count + " non-relation housenumber-to-street connections in " + time / 1000 + " seconds");
System.out.println("info: node housenumbers: ignored " + ignoreCount + " non-relation housenumber-to-street connections");
System.out.println("info: node+area housenumbers: " + exactCount + " exact matches");
System.out.println("info: node+area housenumbers: " + heuristicCount + " heuristic matches (housenumbers without addr:street or addr:street not found)");
}
}
private void CalcWaysForHouseNumberAreas(OsmParser parser, KDTree nearByElements) {
long startTime = System.currentTimeMillis();
int count = 0;
HashMap<Long, Way> wayHashMap = parser.getWayHashMap();
for (Way w : parser.getWays()) {
if (w.hasHouseNumberTag()) {
count++;
Node n = w.getMidPoint();
long way = calcWayForHouseNumber((Entity) n, wayHashMap);
if (way != (long) 0) {
// if there's a relation, __wayid has already been set at relation handling
if (!w.containsKey("__wayid")) {
w.setAttribute("__wayid", Long.toString(way));
}
// FIXME if we want to have nodes for housenumbers in ShareNav
// (if we want to list housenumber nodes in way data structure),
// we'll probably need to create fake IDs and add the nodes to parser. Takes more space and gets more complicated.
// w.houseNumberAdd(n);
//System.out.println("Adding housenumber helper tag __wayid " + way + " to way " + w + " midpoint: " + n );
}
//n.wayToPOItransfer(w);
//parser.addNode(n);
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("info: area housenumbers: accepted " + count + " non-relation housenumber-to-street connections in " + time / 1000 + " seconds");
}
}
private void calcCityNearBy(OsmParser parser, KDTree nearByElements) {
long startTime = System.currentTimeMillis();
//double [] latlonKey = new double[2];
for (Node n : parser.getNodes()) {
String place = n.getPlace();
if (place != null) {
Node nearestPlace = null;
int nneighbours = 10;
long nearesDist = Long.MAX_VALUE;
while ((nearestPlace == null)) {
nearesDist = Long.MAX_VALUE;
Object[] nearNodes = null;
if (kdSize < nneighbours) {
nneighbours = kdSize;
}
try {
nearNodes = nearByElements.nearest(
MyMath.latlon2XYZ(n), nneighbours);
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
} catch (KeySizeException e) {
e.printStackTrace();
return;
} catch (ClassCastException cce) {
System.out.println(nearNodes);
return;
}
for (Object otherO : nearNodes) {
Node other = (Node) otherO;
if ((n.getType(null) > other.getType(null)) && (other.getType(null) > 0)) {
long dist = MyMath.dist(n, other);
if (dist < nearesDist) {
nearesDist = dist;
nearestPlace = other;
}
}
}
if (nneighbours == kdSize) {
break;
}
nneighbours *= 5;
}
if (nearestPlace != null) {
n.nearBy = nearestPlace;
//n.nearByDist = nearesDist;
// System.out.println(n + " near " + n.nearBy);
}
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("info: city nearbys created in " + time / 1000 + " seconds");
}
}
private KDTree getNearByElements(OsmParser parser) {
long startTime = System.currentTimeMillis();
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("Creating nearBy candidates");
}
KDTree kd = new KDTree(3);
//double [] latlonKey = new double[2];
for (Node n : parser.getNodes()) {
if (n.isPlace()) {
//latlonKey[0] = n.lat;
//latlonKey[1] = n.lon;
if (n.getName() == null || n.getName().trim().length() == 0) {
System.out.println("STRANGE: place without name, skipping: " + n);
System.out.println(" Please fix in OSM: " + n.toUrl());
continue;
}
try {
kd.insert(MyMath.latlon2XYZ(n), n);
kdSize++;
} catch (KeySizeException e) {
e.printStackTrace();
} catch (KeyDuplicateException e) {
System.out.println("KeyDuplication at " + n);
System.out.println(" Please fix in OSM: " + n.toUrl());
}
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("Found " + kdSize + " placenames in " + time / 1000 + " seconds");
}
return kd;
}
private KDTree getNearByWays(OsmParser parser) {
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("Creating nearBy way candidates");
}
long startTime = System.currentTimeMillis();
KDTree kd = new KDTree(3);
//double [] latlonKey = new double[2];
for (Way w : parser.getWays()) {
if (w.isHighway() /*&& w.getIsIn() == null */) {
if (w.getName() == null || w.getName().trim().length() == 0) {
continue;
}
Node n = w.getMidPoint();
if (n == null) {
continue;
}
try {
// FIXME would be better to make a real data
// type for way proximity & name calculation instead of abusing nodes and node tags
// replace node's id with way id so
// we get the right id to add as tag
// causes problems
Node n2 = new Node();
n2.id = w.id;
// transfer coords to node
n2.lat = n.lat;
n2.lon = n.lon;
// System.out.println("way name: " + w.getName());
//n.setAttribute("name", w.getName());
// is this needed? probably not
//n2.cloneTags(w);
//System.out.println("way " + w.getName() + " converted into node: " + n2 + " for way matching");
if (!n2.containsKey("__wayname")) {
n2.setAttribute("__wayname", w.getName());
}
// FIXME: should find out about and eliminate duplicate warnings
kd.insert(MyMath.latlon2XYZ(n2), n2);
kdWaysSize++;
} catch (KeySizeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (KeyDuplicateException e) {
// can be safely ignored
//System.out.println("Warning: KeyDuplication bug at housenumber handling " + n.toUrl());
}
}
}
long time = (System.currentTimeMillis() - startTime);
if (Configuration.getConfiguration().verbose >= 0) {
System.out.println("Found " + kdWaysSize + " waynames in " + time / 1000 + " seconds");
}
return kd;
}
// return distance from node to way
private static long distanceToWay(Node node, Way w, long dist) {
List<Node> nl = w.getNodes();
for (int i = 0; i < nl.size() - 1 ; i++ ) {
Node closestPoint = closestPointOnLine(nl.get(i), nl.get(i+1), node);
long distNew = MyMath.dist(node, closestPoint);
if (distNew < dist) {
dist = distNew;
}
}
return dist;
}
private static Node closestPointOnLine(Node node1, Node node2, Node offNode) {
// avoid division by zero if node1 and node2 are at the same coordinates
if (node1.lat == node2.lat && node1.lon == node2.lon) {
return new Node(node1);
}
float uX = (float)Math.toRadians(node2.lat) - (float)Math.toRadians(node1.lat);
float uY = (float)Math.toRadians(node2.lon) - (float)Math.toRadians(node1.lon);
float u = ( ((float)Math.toRadians(offNode.lat) - (float)Math.toRadians(node1.lat)) * uX
+ ((float)Math.toRadians(offNode.lon)- (float)Math.toRadians(node1.lon)) * uY) / (uX * uX + uY * uY);
if (u > 1.0) {
return new Node(node2);
} else if (u <= 0.0) {
return new Node(node1);
} else {
return new Node( (float)Math.toDegrees((node2.lat * u + node1.lat * (1.0 - u ))), (float) Math.toDegrees((node2.lon * u + node1.lon * (1.0-u))), 1);
}
}
}