All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.roklenarcic.tree.KDTreeSpherical Maven / Gradle / Ivy

The newest version!
package com.roklenarcic.tree;

import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

/**
 * 3-D tree with coordinates of the double type.
 *
 * It is instantiated with a list of points, with the coordinates longitude and latitude. The values are
 * limited to range [-180, 180] and [-90, 90].
 *
 *
 * @author Rok Lenarcic
 *
 * @param 
 *            Value to be stored in points.
 */
public class KDTreeSpherical {

    private static final double DEGREES_IN_RADIAN = 57.29577951308233;

    @SuppressWarnings("unchecked")
    private final Comparator>[] comparators = (Comparator>[]) new Comparator[] { Point.createComparator(0), Point.createComparator(1),
            Point.createComparator(2) };

    private double maxDistance;
    private final Point root;

    /**
     * Build a tree from list of points, where points are given by the objects of the Point class.
     *
     * @param points
     *            list of points with coordinates and values
     * @param maxDistance
     *            max distance from query point to search, must be in [0, 180] range
     */
    public KDTreeSpherical(List> points, double maxDistance) {
        // Convert maxDistance along the sphere into chord length
        // chord = 2 * sin (1/2 * angle) where angle is the maxDistance since we have a unit
        // sphere.
        if (maxDistance > 180 || maxDistance < 0) {
            throw new IllegalArgumentException("Max distance must be between 0 and 180.");
        }
        maxDistance = 2 * Math.sin(0.5 * maxDistance / DEGREES_IN_RADIAN);
        this.maxDistance = maxDistance * maxDistance;
        root = buildTree(points, 0);
    }

    /**
     * Find the nearest point to the coordinates given, that is within the maximum distance, inclusive. The
     * smaller the maximum distance, the faster the query.
     *
     * @param longitude
     *            longitude coordinate of the query point
     * @param latitude
     *            latitude coordinate of the query point
     * @return the point in the tree closest to the coordinates given within max distance
     */
    public Point findNearest(double longitude, double latitude) {
        NearestPoint nearest = new NearestPoint();
        if (root != null) {
            nearest.distance = this.maxDistance;
            // Calculate those cartesian coordinates
            double azimuth = (longitude + 180) / DEGREES_IN_RADIAN;
            double inclination = (-latitude + 90) / DEGREES_IN_RADIAN;
            double sinAzimuth = Math.sin(azimuth);
            double cosAzimuth = Math.cos(azimuth);
            double sinInclination = Math.sin(inclination);
            double cosInclination = Math.cos(inclination);
            root.findNearest(sinInclination * cosAzimuth, sinInclination * sinAzimuth, cosInclination, nearest);
        }
        return nearest.p;
    }

    /**
     * Find a number of nearest points to the coordinates given that are within the maximum distance given,
     * inclusive. The smaller the maximum distance, the faster the query. The points returned are sorted from
     * the closest to the farthest.
     *
     * @param longitude
     *            longitude coordinate of the query point
     * @param latitude
     *            latitude coordinate of the query point
     * @param numberOfNearest
     *            number of points to return
     * @return an iterable of points in the tree closest to the coordinates given, in order of ascending
     *         distance
     */
    public Iterable> findNearest(double longitude, double latitude, int numberOfNearest) {
        if (root != null) {
            LinkedList nearestPoints = LinkedList.constructChain(numberOfNearest, this.maxDistance);
            // Calculate those cartesian coordinates
            double azimuth = (longitude + 180) / DEGREES_IN_RADIAN;
            double inclination = (-latitude + 90) / DEGREES_IN_RADIAN;
            double sinAzimuth = Math.sin(azimuth);
            double cosAzimuth = Math.cos(azimuth);
            double sinInclination = Math.sin(inclination);
            double cosInclination = Math.cos(inclination);
            nearestPoints = root.findNearest(sinInclination * cosAzimuth, sinInclination * sinAzimuth, cosInclination, nearestPoints).dropEmptyPrefix();
            if (nearestPoints != null) {
                return nearestPoints.reverse();
            } else {
                return Collections.emptyList();
            }
        } else {
            return Collections.emptyList();
        }
    }

    private Point buildTree(List> points, int axis) {
        if (points.size() == 0) {
            return null;
        } else {
            // Sort by axis.
            Collections.sort(points, comparators[axis % 3]);
            int pivotIdx = points.size() >> 1;
            if ((points.size() & 1) == 0) { // If odd size
                // Shift pivot to the left every second level so for lists of size 4
                // the pivot is idx 1 and 2 every other level.
                pivotIdx -= axis & 1;
            }

            Point p = points.get(pivotIdx);
            p.rotate(axis % 3);
            // Build subtree. Bigger branch also contains points that has equal axis value to the pivot.
            p.smaller = buildTree(points.subList(0, pivotIdx), axis + 1);
            p.bigger = buildTree(points.subList(pivotIdx + 1, points.size()), axis + 1);
            return p;
        }
    }

    /**
     * Point in the 3-D space on a sphere with a user specified value attached to it.
     *
     * @author Rok Lenarcic
     *
     * @param 
     *            type of value
     */
    public static class Point {

        private static Comparator> createComparator(final int axis) {
            return new Comparator>() {
                public int compare(Point o1, Point o2) {
                    double d = axis == 0 ? o1.axisValue - o2.axisValue : (axis == 1 ? o1.otherValue - o2.otherValue : o1.otherValue2 - o2.otherValue2);
                    if (d > 0) {
                        return 1;
                    } else if (d < 0) {
                        return -1;
                    } else {
                        return 0;
                    }
                }
            };
        }

        // Axis value other value contain x and y, but in such order
        // that the value that is used as axis for this point is in axisValue variable.
        private double axisValue;
        private final double longitude, latitude;
        private double otherValue;

        private double otherValue2;

        private Point smaller, bigger;

        private final T value;

        /**
         * New point with the specified coordinates and the value. The constructor maps the point to 3-D space
         * (non-trivial cost).
         *
         * @param longitude
         * @param latitude
         * @param value
         */
        public Point(double longitude, double latitude, T value) {
            this.longitude = longitude;
            this.latitude = latitude;
            if (longitude > 180 || longitude < -180 || latitude > 90 || latitude < -90) {
                throw new IllegalArgumentException("Point " + this + " has longitude outside [-180, 180] or latitude outside [-90, 90].");
            }
            // Save the coordinates
            // Now translate them:
            // Latitude 90 is 0 inclination, -90 is 180
            // Longitude -180 is 0 azimuth (different than earth, but we don't care)
            // then into the radians.
            double azimuth = (longitude + 180) / DEGREES_IN_RADIAN;
            double inclination = (-latitude + 90) / DEGREES_IN_RADIAN;
            double sinAzimuth = Math.sin(azimuth);
            double cosAzimuth = Math.cos(azimuth);
            double sinInclination = Math.sin(inclination);
            double cosInclination = Math.cos(inclination);
            this.axisValue = sinInclination * cosAzimuth;
            this.otherValue = sinInclination * sinAzimuth;
            this.otherValue2 = cosInclination;
            this.value = value;
        }

        /**
         *
         * @return the latitude of the point
         */
        public double getLatitude() {
            return latitude;
        }

        /**
         *
         * @return the longitude of the point
         */
        public double getLongitude() {
            return longitude;
        }

        /**
         *
         * @return the value of the point
         */
        public T getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "Longitude=" + longitude + ", Latitude=" + latitude;
        }

        private LinkedList findNearest(double queryAxis, double queryOther, double queryOther2, LinkedList currentBest) {
            // Negative number means this point is on the left to the query point.
            double diffAxis = queryAxis - axisValue;
            Point closerChild, fartherChild;
            if (diffAxis >= 0) {
                closerChild = bigger;
                fartherChild = smaller;
            } else {
                closerChild = smaller;
                fartherChild = bigger;
            }
            // First check the closer side
            if (closerChild != null) {
                currentBest = closerChild.findNearest(queryOther, queryOther2, queryAxis, currentBest);
            }
            // Now let's see it the other side is still relevant. Since that search
            // might have narrowed the circle.

            // Calculate distance to axis.
            double distanceToHyperplane = diffAxis * diffAxis;
            // See if line intersects circle
            if (distanceToHyperplane <= currentBest.distance) {
                // If it does then this point might be the best one.
                double diffOther = queryOther - otherValue;
                double diffOther2 = queryOther2 - otherValue2;
                double d = distanceToHyperplane + diffOther * diffOther + diffOther2 * diffOther2;
                if (d <= currentBest.distance) {
                    // Start with the farthest point in the list
                    // This point is farther than the this point
                    LinkedList farther = currentBest;
                    LinkedList newHead = currentBest;
                    // Scroll down the list to find the last node that is farther than this node
                    while (farther.tail != null && d <= farther.tail.distance) {
                        farther = farther.tail;
                        newHead = currentBest.tail;
                    }
                    currentBest.head = this;
                    currentBest.distance = d;
                    // Here's a bit of a trickeroo. We've got 2 scenarios:
                    // - The farthest (first) point in the list is the one being replaced. In that case
                    // it's really easy, we're done already.
                    // - In other cases we need assign first point (currentBest) into the chain as tail of
                    // "farther" then we need to update currentBest as tail of currentBest to keep the
                    // currentBest as the start of the chain.
                    //
                    // The trick both cases can be solved by the same code.
                    LinkedList tail = farther.tail;
                    farther.tail = currentBest;
                    currentBest.tail = tail;
                    currentBest = newHead;
                }
                // Search the other side.
                if (fartherChild != null) {
                    currentBest = fartherChild.findNearest(queryOther, queryOther2, queryAxis, currentBest);
                }
            }
            return currentBest;
        }

        private void findNearest(double queryAxis, double queryOther, double queryOther2, NearestPoint currentBest) {
            // Negative number means this point is on the left to the query point.
            double diffAxis = queryAxis - axisValue;
            Point closerChild, fartherChild;
            if (diffAxis >= 0) {
                closerChild = bigger;
                fartherChild = smaller;
            } else {
                closerChild = smaller;
                fartherChild = bigger;
            }
            // First check the closer side
            if (closerChild != null) {
                closerChild.findNearest(queryOther, queryOther2, queryAxis, currentBest);
            }
            // Now let's see it the other side is still relevant. Since that search
            // might have narrowed the circle.

            // Calculate distance to axis.
            double distanceToHyperplane = diffAxis * diffAxis;
            // See if line intersects circle
            if (distanceToHyperplane <= currentBest.distance) {
                // If it does then this point might be the best one.
                double diffOther = queryOther - otherValue;
                double diffOther2 = queryOther2 - otherValue2;
                double d = distanceToHyperplane + diffOther * diffOther + diffOther2 * diffOther2;
                if (d <= currentBest.distance) {
                    currentBest.p = this;
                    currentBest.distance = d;
                }
                // Search the other side.
                if (fartherChild != null) {
                    fartherChild.findNearest(queryOther, queryOther2, queryAxis, currentBest);
                }
            }
        }

        private void rotate(int axis) {
            if (axis == 1) {
                double temp = otherValue;
                otherValue = otherValue2;
                otherValue2 = axisValue;
                axisValue = temp;
            } else if (axis == 2) {
                // Rotate by 2 slots
                double temp = otherValue2;
                otherValue2 = otherValue;
                otherValue = axisValue;
                axisValue = temp;
            }
        }

    }

    private static class LinkedList implements Iterable> {

        private static  LinkedList constructChain(int length, double distance) {
            LinkedList ret = new LinkedList(distance);
            for (int i = 1; i < length; i++) {
                LinkedList nextNode = new LinkedList(distance);
                nextNode.tail = ret;
                ret = nextNode;
            }
            return ret;
        }

        private double distance;
        private Point head;
        private LinkedList tail;

        private LinkedList(double distance) {
            this.distance = distance;
        }

        public LinkedList dropEmptyPrefix() {
            LinkedList c = LinkedList.this;
            while (c != null && c.head == null) {
                c = c.tail;
            }
            return c;
        }

        public Iterator> iterator() {
            return new Iterator>() {

                private LinkedList cursor = LinkedList.this;

                public boolean hasNext() {
                    return cursor != null;
                }

                public Point next() {
                    Point p = cursor.head;
                    cursor = cursor.tail;
                    return p;
                }

                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }

        public LinkedList reverse() {
            LinkedList c = LinkedList.this;
            LinkedList prev = null;
            while (c != null) {
                LinkedList tmp = c;
                c = c.tail;
                tmp.tail = prev;
                prev = tmp;
            }
            return prev;
        }
    }

    private static class NearestPoint {
        private double distance;
        private Point p;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy