from __future__ import absolute_import

from django.db import connection
from django.db.models import Q
from django.contrib.gis.geos import GEOSGeometry, LineString
from django.contrib.gis.measure import D # alias for Distance
from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite
from django.test import TestCase

from .models import (AustraliaCity, Interstate, SouthTexasInterstate,
    SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode)


class DistanceTest(TestCase):

    # A point we are testing distances with -- using a WGS84
    # coordinate that'll be implicitly transormed to that to
    # the coordinate system of the field, EPSG:32140 (Texas South Central
    # w/units in meters)
    stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
    # Another one for Australia
    au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326)

    def get_names(self, qs):
        cities = [c.name for c in qs]
        cities.sort()
        return cities

    def test01_init(self):
        "Test initialization of distance models."
        self.assertEqual(9, SouthTexasCity.objects.count())
        self.assertEqual(9, SouthTexasCityFt.objects.count())
        self.assertEqual(11, AustraliaCity.objects.count())
        self.assertEqual(4, SouthTexasZipcode.objects.count())
        self.assertEqual(4, CensusZipcode.objects.count())
        self.assertEqual(1, Interstate.objects.count())
        self.assertEqual(1, SouthTexasInterstate.objects.count())

    @no_spatialite
    def test02_dwithin(self):
        "Testing the `dwithin` lookup type."
        # Distances -- all should be equal (except for the
        # degree/meter pair in au_cities, that's somewhat
        # approximate).
        tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)]
        au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)]

        # Expected cities for Australia and Texas.
        tx_cities = ['Downtown Houston', 'Southside Place']
        au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong']

        # Performing distance queries on two projected coordinate systems one
        # with units in meters and the other in units of U.S. survey feet.
        for dist in tx_dists:
            if isinstance(dist, tuple): dist1, dist2 = dist
            else: dist1 = dist2 = dist
            qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1))
            qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2))
            for qs in qs1, qs2:
                self.assertEqual(tx_cities, self.get_names(qs))

        # Now performing the `dwithin` queries on a geodetic coordinate system.
        for dist in au_dists:
            if isinstance(dist, D) and not oracle: type_error = True
            else: type_error = False

            if isinstance(dist, tuple):
                if oracle: dist = dist[1]
                else: dist = dist[0]

            # Creating the query set.
            qs = AustraliaCity.objects.order_by('name')
            if type_error:
                # A ValueError should be raised on PostGIS when trying to pass
                # Distance objects into a DWithin query using a geodetic field.
                self.assertRaises(ValueError, AustraliaCity.objects.filter(point__dwithin=(self.au_pnt, dist)).count)
            else:
                self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist))))

    def test03a_distance_method(self):
        "Testing the `distance` GeoQuerySet method on projected coordinate systems."
        # The point for La Grange, TX
        lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
        # Reference distances in feet and in meters. Got these values from
        # using the provided raw SQL statements.
        #  SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity;
        m_distances = [147075.069813, 139630.198056, 140888.552826,
                       138809.684197, 158309.246259, 212183.594374,
                       70870.188967, 165337.758878, 139196.085105]
        #  SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft;
        # Oracle 11 thinks this is not a projected coordinate system, so it's s
        # not tested.
        ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
                        455411.438904354, 519386.252102563, 696139.009211594,
                        232513.278304279, 542445.630586414, 456679.155883207]

        # Testing using different variations of parameters and using models
        # with different projected coordinate systems.
        dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
        dist2 = SouthTexasCity.objects.distance(lagrange)  # Using GEOSGeometry parameter
        if spatialite or oracle:
            dist_qs = [dist1, dist2]
        else:
            dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
            dist4 = SouthTexasCityFt.objects.distance(lagrange)
            dist_qs = [dist1, dist2, dist3, dist4]

        # Original query done on PostGIS, have to adjust AlmostEqual tolerance
        # for Oracle.
        if oracle: tol = 2
        else: tol = 5

        # Ensuring expected distances are returned for each distance queryset.
        for qs in dist_qs:
            for i, c in enumerate(qs):
                self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
                self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)

    @no_spatialite
    def test03b_distance_method(self):
        "Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
        if oracle: tol = 2
        else: tol = 5

        # Testing geodetic distance calculation with a non-point geometry
        # (a LineString of Wollongong and Shellharbour coords).
        ls = LineString( ( (150.902, -34.4245), (150.87, -34.5789) ) )
        if oracle or connection.ops.geography:
            # Reference query:
            #  SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name;
            distances = [1120954.92533513, 140575.720018241, 640396.662906304,
                         60580.9693849269, 972807.955955075, 568451.8357838,
                         40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0]
            qs = AustraliaCity.objects.distance(ls).order_by('name')
            for city, distance in zip(qs, distances):
                # Testing equivalence to within a meter.
                self.assertAlmostEqual(distance, city.distance.m, 0)
        else: