1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

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
This commit is contained in:
Justin Bronn 2008-02-20 21:15:43 +00:00
parent bdab7abe2c
commit 6fef560d97
5 changed files with 52 additions and 23 deletions

View File

@ -12,7 +12,7 @@ from django.contrib.gis.oldforms import WKTField
# Attempting to get the spatial reference system. # Attempting to get the spatial reference system.
try: try:
from django.contrib.gis.models import SpatialRefSys from django.contrib.gis.models import SpatialRefSys
except NotImplementedError: except ImportError:
SpatialRefSys = None SpatialRefSys = None
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. #TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
@ -22,6 +22,9 @@ class GeometryField(SpatialBackend.Field):
# The OpenGIS Geometry name. # The OpenGIS Geometry name.
_geom = 'GEOMETRY' _geom = 'GEOMETRY'
# Geodetic units.
geodetic_units = ('Decimal Degree', 'degree')
def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs): def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
""" """
The initialization function for geometry fields. Takes the following 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, `D(km=1)` was passed in and the units of the field were in meters,
then 1000 would be returned. then 1000 would be returned.
""" """
if isinstance(dist, Distance): 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. # Spherical distance calculation parameter should be in meters.
dist_param = dist.m dist_param = dist.m
else: else:
@ -93,7 +97,7 @@ class GeometryField(SpatialBackend.Field):
dist_param = dist dist_param = dist
# Sphereical distance query; returning meters. # 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] return [gqn(self._spheroid), dist_param]
else: else:
return [dist_param] return [dist_param]

View File

@ -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.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.db.models.fields import FieldDoesNotExist
from django.utils.datastructures import SortedDict 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. # parse_lookup depends on the spatial database backend.
from django.contrib.gis.db.backend import gqn, parse_lookup, SpatialBackend 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. # Shortcut booleans for determining the backend.
oracle = SpatialBackend.name == 'oracle' oracle = SpatialBackend.name == 'oracle'
@ -279,24 +279,27 @@ class GeoQuerySet(QuerySet):
# `distance_lte` lookup type. # `distance_lte` lookup type.
where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0)) where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
if oracle: 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) tolerance = kwargs.get('tolerance', 0.05)
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)}
# 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)}
else: else:
dsql = where[0] % tuple(params)
if len(where) == 3: 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. # 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: 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} 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): def extent(self, field_name=None):
""" """

View File

@ -214,4 +214,4 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
elif settings.DATABASE_ENGINE == 'oracle': elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
else: else:
raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE) pass

View File

@ -8,6 +8,7 @@ au_cities = (('Wollongong', 150.902, -34.4245),
('Sydney', 151.26071, -33.887034), ('Sydney', 151.26071, -33.887034),
('Hobart', 147.33, -42.8827), ('Hobart', 147.33, -42.8827),
('Adelaide', 138.6, -34.9258), ('Adelaide', 138.6, -34.9258),
('Hillsdale', 151.231341, -33.952685),
) )
stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172), stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),

View File

@ -2,7 +2,7 @@ import os, unittest
from decimal import Decimal from decimal import Decimal
from django.contrib.gis.gdal import DataSource 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.measure import D # alias for Distance
from django.contrib.gis.db.models import GeoQ from django.contrib.gis.db.models import GeoQ
from django.contrib.gis.tests.utils import oracle from django.contrib.gis.tests.utils import oracle
@ -35,7 +35,7 @@ class DistanceTest(unittest.TestCase):
load_cities(AustraliaCity, 4326, au_cities) load_cities(AustraliaCity, 4326, au_cities)
self.assertEqual(10, SouthTexasCity.objects.count()) self.assertEqual(10, SouthTexasCity.objects.count())
self.assertEqual(10, AustraliaCity.objects.count()) self.assertEqual(11, AustraliaCity.objects.count())
def test02_dwithin(self): def test02_dwithin(self):
"Testing the `dwithin` lookup type." "Testing the `dwithin` lookup type."
@ -56,18 +56,39 @@ class DistanceTest(unittest.TestCase):
138809.684197, 158309.246259, 212183.594374, 138809.684197, 158309.246259, 212183.594374,
70870.188967, 165337.758878, 102128.654360, 70870.188967, 165337.758878, 102128.654360,
139196.085105] 139196.085105]
# Testing when the field name is explicitly set.
dist1 = SouthTexasCity.objects.distance('point', lagrange) 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 # Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle. # for Oracle.
if oracle: tol = 3 if oracle: tol = 2
else: tol = 5 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): for i, c in enumerate(qs):
self.assertAlmostEqual(distances[i], c.distance, tol) 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): def test04_distance_lookups(self):
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types." "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
# Only two cities (Houston and Southside Place) should be # Only two cities (Houston and Southside Place) should be