From 6fef560d979d91d1abd1777246162e5f805b08b3 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Wed, 20 Feb 2008 21:15:43 +0000 Subject: [PATCH] gis: Fixed bug in `GeoQuerySet.distance` caused by SQL substitution that shouldn't be done there (thanks robotika); an exception is no longer raised when trying to import the spatial metadata models on backends that do not support them (e.g., MySQL). git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7138 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../contrib/gis/db/models/fields/__init__.py | 10 ++++-- django/contrib/gis/db/models/query.py | 31 ++++++++++--------- django/contrib/gis/models.py | 2 +- django/contrib/gis/tests/distapp/data.py | 1 + django/contrib/gis/tests/distapp/tests.py | 31 ++++++++++++++++--- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 50cccb1715..8948854b35 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -12,7 +12,7 @@ from django.contrib.gis.oldforms import WKTField # Attempting to get the spatial reference system. try: from django.contrib.gis.models import SpatialRefSys -except NotImplementedError: +except ImportError: SpatialRefSys = None #TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. @@ -22,6 +22,9 @@ class GeometryField(SpatialBackend.Field): # The OpenGIS Geometry name. _geom = 'GEOMETRY' + # Geodetic units. + geodetic_units = ('Decimal Degree', 'degree') + def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs): """ The initialization function for geometry fields. Takes the following @@ -82,8 +85,9 @@ class GeometryField(SpatialBackend.Field): `D(km=1)` was passed in and the units of the field were in meters, then 1000 would be returned. """ + if isinstance(dist, Distance): - if self._unit_name in ('Decimal Degree', 'degree'): + if self._unit_name in self.geodetic_units: # Spherical distance calculation parameter should be in meters. dist_param = dist.m else: @@ -93,7 +97,7 @@ class GeometryField(SpatialBackend.Field): dist_param = dist # Sphereical distance query; returning meters. - if SpatialBackend.name == 'postgis' and self._unit_name == 'degree': + if SpatialBackend.name == 'postgis' and self._unit_name in self.geodetic_units: return [gqn(self._spheroid), dist_param] else: return [dist_param] diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index c9fa8decf3..d44c834974 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -4,10 +4,10 @@ from django.db import connection from django.db.models.query import EmptyResultSet, Q, QuerySet, handle_legacy_orderlist, quote_only_if_word, orderfield2column, fill_table_cache from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict -from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.models.fields import GeometryField, PointField # parse_lookup depends on the spatial database backend. from django.contrib.gis.db.backend import gqn, parse_lookup, SpatialBackend -from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.geos import GEOSGeometry, Point # Shortcut booleans for determining the backend. oracle = SpatialBackend.name == 'oracle' @@ -279,24 +279,27 @@ class GeoQuerySet(QuerySet): # `distance_lte` lookup type. where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0)) if oracle: - # The `tolerance` keyword may be used for Oracle. + # The `tolerance` keyword may be used for Oracle; the tolerance is + # in meters -- a default of 5 centimeters is used. tolerance = kwargs.get('tolerance', 0.05) - - # More legwork here because the OracleSpatialAdaptor doesn't do - # quoting of the WKT. - tmp_params = [gqn(str(params[0]))] - tmp_params.extend(params[1:]) - dsql = where[0] % tuple(tmp_params) - dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, dsql, tolerance)} + dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)} else: - dsql = where[0] % tuple(params) if len(where) == 3: + # Spherical distance calculation was requested (b/c spheroid + # parameter was attached) However, the PostGIS ST_distance_spheroid() + # procedure may only do queries from point columns to point geometries + # some error checking is required. + if not isinstance(geo_field, PointField): + raise TypeError('Spherical distance calculation only supported on PointFields.') + if not isinstance(GEOSGeometry(params[0].wkb), Point): + raise TypeError('Spherical distance calculation only supported with Point Geometry parameters') + # Call to distance_spheroid() requires the spheroid as well. - dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, dsql, where[1]) + dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, where[0], where[1]) else: - dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, dsql) + dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0]) dist_select = {'distance' : dist_sql} - return self.extra(select=dist_select) + return self.extra(select=dist_select, params=params) def extent(self, field_name=None): """ diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py index fc0de5fa58..9d923760a3 100644 --- a/django/contrib/gis/models.py +++ b/django/contrib/gis/models.py @@ -214,4 +214,4 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2': elif settings.DATABASE_ENGINE == 'oracle': from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys else: - raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE) + pass diff --git a/django/contrib/gis/tests/distapp/data.py b/django/contrib/gis/tests/distapp/data.py index 94b28abc4c..6ccee717c6 100644 --- a/django/contrib/gis/tests/distapp/data.py +++ b/django/contrib/gis/tests/distapp/data.py @@ -8,6 +8,7 @@ au_cities = (('Wollongong', 150.902, -34.4245), ('Sydney', 151.26071, -33.887034), ('Hobart', 147.33, -42.8827), ('Adelaide', 138.6, -34.9258), + ('Hillsdale', 151.231341, -33.952685), ) stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172), diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 187d30e52b..0f5b0349dd 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -2,7 +2,7 @@ import os, unittest from decimal import Decimal from django.contrib.gis.gdal import DataSource -from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.gis.geos import GEOSGeometry, Point, LineString from django.contrib.gis.measure import D # alias for Distance from django.contrib.gis.db.models import GeoQ from django.contrib.gis.tests.utils import oracle @@ -35,7 +35,7 @@ class DistanceTest(unittest.TestCase): load_cities(AustraliaCity, 4326, au_cities) self.assertEqual(10, SouthTexasCity.objects.count()) - self.assertEqual(10, AustraliaCity.objects.count()) + self.assertEqual(11, AustraliaCity.objects.count()) def test02_dwithin(self): "Testing the `dwithin` lookup type." @@ -56,18 +56,39 @@ class DistanceTest(unittest.TestCase): 138809.684197, 158309.246259, 212183.594374, 70870.188967, 165337.758878, 102128.654360, 139196.085105] + + # Testing when the field name is explicitly set. dist1 = SouthTexasCity.objects.distance('point', lagrange) - dist2 = SouthTexasCity.objects.distance(lagrange) + dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter + dist3 = SouthTexasCity.objects.distance(lagrange.ewkt) # Using EWKT string parameter. # Original query done on PostGIS, have to adjust AlmostEqual tolerance # for Oracle. - if oracle: tol = 3 + if oracle: tol = 2 else: tol = 5 - for qs in [dist1, dist2]: + # Ensuring expected distances are returned for each distance queryset. + for qs in [dist1, dist2, dist3]: for i, c in enumerate(qs): self.assertAlmostEqual(distances[i], c.distance, tol) + # Now testing geodetic distance aggregation. + hillsdale = AustraliaCity.objects.get(name='Hillsdale') + if not oracle: + # PostGIS is limited to disance queries only to/from point geometries, + # ensuring a TypeError is raised if something else is put in. + self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)') + self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1))) + + # Got these distances using the raw SQL statement: + # SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11)); + geodetic_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647] + + # Ensuring the expected distances are returned. + qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point) + for i, c in enumerate(qs): + self.assertAlmostEqual(geodetic_distances[i], c.distance, tol) + def test04_distance_lookups(self): "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types." # Only two cities (Houston and Southside Place) should be