package com.github.davidmoten.rtree;

import static org.junit.Assert.assertEquals;

import java.util.List;

import org.junit.Test;

import com.github.davidmoten.grumpy.core.Position;
import com.github.davidmoten.rtree.geometry.Geometries;
import com.github.davidmoten.rtree.geometry.Point;
import com.github.davidmoten.rtree.geometry.Rectangle;

import rx.Observable;
import rx.functions.Func1;

public class LatLongExampleTest {

    private static final Point sydney = Geometries.point(151.2094, -33.86);
    private static final Point canberra = Geometries.point(149.1244, -35.3075);
    private static final Point brisbane = Geometries.point(153.0278, -27.4679);
    private static final Point bungendore = Geometries.point(149.4500, -35.2500);

    @Test
    public void testLatLongExample() {

        // This is to demonstrate how to use rtree to to do distance searches
        // with Lat Long points

        // Let's find all cities within 300km of Canberra

        RTree<String, Point> tree = RTree.star().create();
        tree = tree.add("Sydney", sydney);
        tree = tree.add("Brisbane", brisbane);

        // Now search for all locations within 300km of Canberra
        final double distanceKm = 300;
        List<Entry<String, Point>> list = search(tree, canberra, distanceKm)
                // get the result
                .toList().toBlocking().single();

        // should have returned Sydney only
        assertEquals(1, list.size());
        assertEquals("Sydney", list.get(0).value());
    }

    public static <T> Observable<Entry<T, Point>> search(RTree<T, Point> tree, Point lonLat,
            final double distanceKm) {
        // First we need to calculate an enclosing lat long rectangle for this
        // distance then we refine on the exact distance
        final Position from = Position.create(lonLat.y(), lonLat.x());
        Rectangle bounds = createBounds(from, distanceKm);

        return tree
                // do the first search using the bounds
                .search(bounds)
                // refine using the exact distance
                .filter(new Func1<Entry<T, Point>, Boolean>() {
                    @Override
                    public Boolean call(Entry<T, Point> entry) {
                        Point p = entry.geometry();
                        Position position = Position.create(p.y(), p.x());
                        return from.getDistanceToKm(position) < distanceKm;
                    }
                });
    }

    @Test
    public void testSearchLatLongCircles() {
        RTree<GeoCircleValue<String>, Rectangle> tree = RTree.star().create();
        // create circles around these major towns
        GeoCircleValue<String> sydneyCircle = createGeoCircleValue(sydney, 100, "Sydney");
        GeoCircleValue<String> canberraCircle = createGeoCircleValue(canberra, 50, "Canberra");
        GeoCircleValue<String> brisbaneCircle = createGeoCircleValue(brisbane, 200, "Brisbane");

        // add the circles to the RTree using the bounding box of the circle as
        // the geometry
        tree = add(tree, sydneyCircle);
        tree = add(tree, canberraCircle);
        tree = add(tree, brisbaneCircle);

        // now find the circles that contain bungendore (which is 30km from
        // Canberra)
        final Point location = bungendore;
        String result = tree.search(location)
                // filter on the exact distance from the centre of the GeoCircle
                .filter(new Func1<Entry<GeoCircleValue<String>, Rectangle>, Boolean>() {
                    Position from = Position.create(location.y(), location.x());

                    @Override
                    public Boolean call(Entry<GeoCircleValue<String>, Rectangle> entry) {
                        Position centre = Position.create(entry.value().lat, entry.value().lon);
                        return from.getDistanceToKm(centre) < entry.value().radiusKm;
                    }
                })
                // do the search (only expect one value)
                .toBlocking().single()
                // get the name of the GoeCircleValue returned
                .value().value;
        assertEquals("Canberra", result);
    }

    private static Rectangle createBounds(final Position from, final double distanceKm) {
        // this calculates a pretty accurate bounding box. Depending on the
        // performance you require you wouldn't have to be this accurate because
        // accuracy is enforced later
        Position north = from.predict(distanceKm, 0);
        Position south = from.predict(distanceKm, 180);
        Position east = from.predict(distanceKm, 90);
        Position west = from.predict(distanceKm, 270);

        return Geometries.rectangle(west.getLon(), south.getLat(), east.getLon(), north.getLat());
    }

    private static <T> GeoCircleValue<T> createGeoCircleValue(Point point, double radiusKm,
            T value) {
        return new GeoCircleValue<T>((float) point.y(), (float) point.x(), radiusKm, value);
    }

    private static <T> RTree<GeoCircleValue<T>, Rectangle> add(
            RTree<GeoCircleValue<T>, Rectangle> tree, GeoCircleValue<T> c) {
        return tree.add(c, createBounds(Position.create(c.lat, c.lon), c.radiusKm));
    }

    private static class GeoCircleValue<T> {

        GeoCircleValue(float lat, float lon, double radiusKm, T value) {
            this.lat = lat;
            this.lon = lon;
            this.radiusKm = radiusKm;
            this.value = value;
        }

        float lat;
        float lon;
        double radiusKm;
        T value;
    }
}
