/*
* Copyright (C) 2014 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.util;
import java.util.LinkedHashSet;
import java.util.Set;
import uk.me.parabola.imgfmt.app.Coord;
/**
* A kd-tree (2D) implementation to solve the nearest neighbor problem.
* The tree is not explicitly balanced.
*
* @author Gerd Petermann
*
*/
public class KdTree <T extends Locatable> {
private static final boolean ROOT_NODE_USES_LONGITUDE = false;
private class KdNode {
T point;
KdNode left;
KdNode right;
KdNode(T p) {
point = p;
}
}
// the tree root
private KdNode root;
// number of saved objects
private int size;
// helpers
private T nextPoint ;
private double minDist;
private double maxDist;
private Set<T> set;
/**
* create an empty tree
*/
public KdTree() {
root = null;
}
public long size()
{
return size;
}
/**
* Start the add action with the root
* @param toAdd
*/
public void add(T toAdd) {
size++;
root = add(toAdd, root, ROOT_NODE_USES_LONGITUDE);
}
/**
* Compares the given axis of both points.
* @param longitude <code>true</code>: compare longitude; <code>false</code> compare latitude
* @param c1 a point
* @param c2 another point
* @return <code>true</code> the axis value of c1 is smaller than c2;
* <code>false</code> the axis value of c1 is equal or larger than c2
*/
private boolean isSmaller(boolean longitude, Coord c1, Coord c2) {
if (longitude) {
return c1.getLongitude() < c2.getLongitude();
} else {
return c1.getLatitude() < c2.getLatitude();
}
}
/**
* Recursive routine to find the right place for inserting
* into the tree.
* @param toAdd the point
* @param tree the subtree root node where to add (maybe <code>null</code>)
* @param useLongitude <code>true</code> the tree node uses longitude for comparison;
* <code>false</code> the tree node uses latitude for comparison
* @return the subtree root node after insertion
*/
private KdNode add( T toAdd, KdNode tree, boolean useLongitude){
if( tree == null ) {
tree = new KdNode( toAdd );
} else {
if(isSmaller(useLongitude, toAdd.getLocation(), tree.point.getLocation())) {
tree.left = add(toAdd, tree.left, !useLongitude);
} else {
tree.right = add(toAdd, tree.right, !useLongitude);
}
}
return tree;
}
/**
* Searches for the point that has smallest distance to the given point.
* @param p the point to search for
* @return the point with shortest distance to <var>p</var>
*/
public T findNextPoint(Locatable p) {
// reset
minDist = Double.MAX_VALUE;
maxDist = -1;
set = null;
nextPoint = null;
// false => first node is a latitude level
return findNextPoint(p, root, ROOT_NODE_USES_LONGITUDE);
}
/**
* Searches for the point that has smallest distance to the given point.
* @param p the point to search for
* @return the point with shortest distance to <var>p</var>
*/
public Set<T> findNextPoint(Locatable p, double maxDist) {
// reset
minDist = Double.MAX_VALUE;
this.maxDist = Math.pow(maxDist * 360 / Coord.U, 2); // convert maxDist in meter to distanceInDegreesSquared
nextPoint = null;
this.set = new LinkedHashSet<>();
// false => first node is a latitude level
findNextPoint(p, root, ROOT_NODE_USES_LONGITUDE);
return set;
}
private T findNextPoint(Locatable p, KdNode tree, boolean useLongitude) {
boolean continueWithLeft = false;
if (tree == null)
return nextPoint;
if (tree.left == null && tree.right == null){
double dist = tree.point.getLocation().distanceInDegreesSquared(p.getLocation());
if (dist <= maxDist && set != null){
set.add(tree.point);
}
if (dist < minDist){
nextPoint = tree.point;
if (dist < maxDist)
minDist = maxDist;
else
minDist = dist;
}
return nextPoint;
}
else {
if (isSmaller(useLongitude, p.getLocation(), tree.point.getLocation())){
continueWithLeft = false;
nextPoint = findNextPoint(p, tree.left, !useLongitude);
}
else {
continueWithLeft = true;
nextPoint = findNextPoint(p, tree.right, !useLongitude);
}
}
double dist = tree.point.getLocation().distanceInDegreesSquared(p.getLocation());
if (dist <= maxDist && set != null)
set.add(tree.point);
if (dist < minDist){
nextPoint = tree.point;
minDist = dist;
if (dist < maxDist)
minDist = maxDist;
else
minDist = dist;
}
// do we have to search the other part of the tree?
Coord test;
if (useLongitude)
test = Coord.makeHighPrecCoord(p.getLocation().getHighPrecLat(), tree.point.getLocation().getHighPrecLon());
else
test = Coord.makeHighPrecCoord(tree.point.getLocation().getHighPrecLat(), p.getLocation().getHighPrecLon());
if (test.distanceInDegreesSquared(p.getLocation()) < minDist){
if (continueWithLeft)
nextPoint = findNextPoint(p, tree.left, !useLongitude);
else
nextPoint = findNextPoint(p, tree.right, !useLongitude);
}
return nextPoint;
}
}