/* * 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; } }