mirror of
https://github.com/django/django.git
synced 2025-07-04 09:49:12 +00:00
gis: added transform() manager method; _get_sql_clause() moved to GeoQuerySet; cleaned up PostGIS backend for 1.3.0; added tests for 'equals' and 'relate' lookups.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5881 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
fc1dbe70fe
commit
dcf2f144d1
@ -18,10 +18,10 @@ from django.utils.datastructures import SortedDict
|
|||||||
|
|
||||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
# PostGIS is the spatial database, getting the rquired modules, renaming as necessary.
|
# PostGIS is the spatial database, getting the rquired modules, renaming as necessary.
|
||||||
from postgis import \
|
from django.contrib.gis.db.backend.postgis import \
|
||||||
PostGISField as GeoBackendField, \
|
PostGISField as GeoBackendField, \
|
||||||
POSTGIS_TERMS as GIS_TERMS, \
|
POSTGIS_TERMS as GIS_TERMS, \
|
||||||
create_spatial_db, get_geo_where_clause
|
create_spatial_db, geo_quotename, get_geo_where_clause
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME
|
raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME
|
||||||
|
|
||||||
@ -249,16 +249,36 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
|
|||||||
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
||||||
# with the get_geo_where_clause()
|
# with the get_geo_where_clause()
|
||||||
if hasattr(field, '_geom'):
|
if hasattr(field, '_geom'):
|
||||||
|
# Do we have multiple arguments, e.g., ST_Relate, ST_DWithin lookup types
|
||||||
|
# need more than argument.
|
||||||
|
multiple_args = isinstance(value, tuple)
|
||||||
|
|
||||||
# Getting the geographic where clause.
|
# Getting the geographic where clause.
|
||||||
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
||||||
|
|
||||||
# Getting the geographic parameters from the field.
|
# Getting the geographic parameters from the field.
|
||||||
geo_params = field.get_db_prep_lookup(lookup_type, value)
|
if multiple_args:
|
||||||
|
geo_params = field.get_db_prep_lookup(lookup_type, value[0])
|
||||||
|
else:
|
||||||
|
geo_params = field.get_db_prep_lookup(lookup_type, value)
|
||||||
|
|
||||||
# If a dictionary was passed back from the field modify the where clause.
|
# If a dictionary was passed back from the field modify the where clause.
|
||||||
if isinstance(geo_params, dict):
|
param_dict = isinstance(geo_params, dict)
|
||||||
gwc = gwc % geo_params['where']
|
if param_dict:
|
||||||
geo_params = geo_params['params']
|
subst_list = geo_params['where']
|
||||||
|
if multiple_args: subst_list += map(geo_quotename, value[1:])
|
||||||
|
geo_params = geo_params['params']
|
||||||
|
gwc = gwc % tuple(subst_list)
|
||||||
|
elif multiple_args:
|
||||||
|
# Modify the where clause if we have multiple arguments -- the
|
||||||
|
# first substitution will be for another placeholder (for the
|
||||||
|
# geometry) since it is already apart of geo_params.
|
||||||
|
subst_list = ['%s']
|
||||||
|
subst_list += map(geo_quotename, value[1:])
|
||||||
|
gwc = gwc % tuple(subst_list)
|
||||||
|
|
||||||
|
# Finally, appending onto the WHERE clause, and extending with any
|
||||||
|
# additional parameters.
|
||||||
where.append(gwc)
|
where.append(gwc)
|
||||||
params.extend(geo_params)
|
params.extend(geo_params)
|
||||||
else:
|
else:
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
The PostGIS spatial database backend module.
|
The PostGIS spatial database backend module.
|
||||||
"""
|
"""
|
||||||
from query import \
|
from django.contrib.gis.db.backend.postgis.query import \
|
||||||
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
get_geo_where_clause, geo_quotename, \
|
||||||
|
GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
|
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
|
||||||
from creation import create_spatial_db
|
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||||
from field import PostGISField
|
from django.contrib.gis.db.backend.postgis.field import PostGISField
|
||||||
|
|
||||||
# Whether PostGIS has AsKML() support.
|
# Whether PostGIS has AsKML() support.
|
||||||
if MAJOR_VERSION == 1:
|
if MAJOR_VERSION == 1:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.db.models.fields import Field # Django base Field class
|
from django.db.models.fields import Field # Django base Field class
|
||||||
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
||||||
|
from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, geo_quotename as quotename
|
||||||
from types import StringType
|
from types import StringType
|
||||||
from query import POSTGIS_TERMS, quotename
|
|
||||||
|
|
||||||
class PostGISField(Field):
|
class PostGISField(Field):
|
||||||
def _add_geom(self, style, db_table):
|
def _add_geom(self, style, db_table):
|
||||||
@ -66,16 +66,18 @@ class PostGISField(Field):
|
|||||||
return sql
|
return sql
|
||||||
|
|
||||||
def get_db_prep_lookup(self, lookup_type, value):
|
def get_db_prep_lookup(self, lookup_type, value):
|
||||||
"Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value."
|
"""Returns field's value prepared for database lookup, accepts WKT and
|
||||||
|
GEOS Geometries for the value."""
|
||||||
if lookup_type in POSTGIS_TERMS:
|
if lookup_type in POSTGIS_TERMS:
|
||||||
if lookup_type == 'isnull': return [value] # special case for NULL geometries.
|
if lookup_type == 'isnull': return [value] # special case for NULL geometries.
|
||||||
if not bool(value): return [None] # If invalid value passed in.
|
if not bool(value): return [None] # If invalid value passed in.
|
||||||
if isinstance(value, GEOSGeometry):
|
if isinstance(value, GEOSGeometry):
|
||||||
# GEOSGeometry instance passed in.
|
# GEOSGeometry instance passed in.
|
||||||
if value.srid != self._srid:
|
if value.srid != self._srid:
|
||||||
# Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key
|
# Returning a dictionary instructs the parse_lookup() to add
|
||||||
# to the where parameters, since we need to transform the geometry in the query.
|
# what's in the 'where' key to the where parameters, since we
|
||||||
return {'where' : "Transform(%s,%s)",
|
# need to transform the geometry in the query.
|
||||||
|
return {'where' : ["ST_Transform(%s,%s)"],
|
||||||
'params' : [value, self._srid]
|
'params' : [value, self._srid]
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
@ -102,6 +104,6 @@ class PostGISField(Field):
|
|||||||
"Provides a proper substitution value for "
|
"Provides a proper substitution value for "
|
||||||
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
|
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
|
||||||
# Adding Transform() to the SQL placeholder.
|
# Adding Transform() to the SQL placeholder.
|
||||||
return 'Transform(%%s, %s)' % self._srid
|
return 'ST_Transform(%%s, %s)' % self._srid
|
||||||
else:
|
else:
|
||||||
return '%s'
|
return '%s'
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
See PostGIS docs at Ch. 6.2.1 for more information on these functions.
|
See PostGIS docs at Ch. 6.2.1 for more information on these functions.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
def _get_postgis_func(func):
|
def _get_postgis_func(func):
|
||||||
"Helper routine for calling PostGIS functions and returning their result."
|
"Helper routine for calling PostGIS functions and returning their result."
|
||||||
@ -13,6 +14,7 @@ def _get_postgis_func(func):
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
return row[0]
|
return row[0]
|
||||||
|
|
||||||
|
### PostGIS management functions ###
|
||||||
def postgis_geos_version():
|
def postgis_geos_version():
|
||||||
"Returns the version of the GEOS library used with PostGIS."
|
"Returns the version of the GEOS library used with PostGIS."
|
||||||
return _get_postgis_func('postgis_geos_version')
|
return _get_postgis_func('postgis_geos_version')
|
||||||
@ -33,4 +35,22 @@ def postgis_full_version():
|
|||||||
"Returns PostGIS version number and compile-time options."
|
"Returns PostGIS version number and compile-time options."
|
||||||
return _get_postgis_func('postgis_full_version')
|
return _get_postgis_func('postgis_full_version')
|
||||||
|
|
||||||
|
### Routines for parsing output of management functions. ###
|
||||||
|
version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||||
|
def postgis_version_tuple():
|
||||||
|
"Returns the PostGIS version as a tuple."
|
||||||
|
|
||||||
|
# Getting the PostGIS version
|
||||||
|
version = postgis_lib_version()
|
||||||
|
m = version_regex.match(version)
|
||||||
|
if m:
|
||||||
|
major = int(m.group('major'))
|
||||||
|
minor1 = int(m.group('minor1'))
|
||||||
|
minor2 = int(m.group('minor2'))
|
||||||
|
else:
|
||||||
|
raise Exception, 'Could not parse PostGIS version string: %s' % version
|
||||||
|
|
||||||
|
return (version, major, minor1, minor2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,98 +3,105 @@
|
|||||||
routine for PostGIS.
|
routine for PostGIS.
|
||||||
"""
|
"""
|
||||||
from django.db import backend
|
from django.db import backend
|
||||||
from management import postgis_lib_version
|
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
|
||||||
|
from types import StringType, UnicodeType
|
||||||
|
|
||||||
# Getting the PostGIS version
|
# Getting the PostGIS version information
|
||||||
POSTGIS_VERSION = postgis_lib_version()
|
POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple()
|
||||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.'))
|
|
||||||
|
|
||||||
# The supported PostGIS versions.
|
# The supported PostGIS versions.
|
||||||
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. Versions <= 1.0.x didn't use GEOS C API.
|
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work.
|
||||||
if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1:
|
# Versions <= 1.0.x do not use GEOS C API, and will not be supported.
|
||||||
|
if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
|
||||||
raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION
|
raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION
|
||||||
|
|
||||||
# PostGIS-specific operators. The commented descriptions of these
|
# PostGIS-specific operators. The commented descriptions of these
|
||||||
# operators come from Section 6.2.2 of the official PostGIS documentation.
|
# operators come from Section 6.2.2 of the official PostGIS documentation.
|
||||||
POSTGIS_OPERATORS = {
|
POSTGIS_OPERATORS = {
|
||||||
# The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box.
|
# The "&<" operator returns true if A's bounding box overlaps or
|
||||||
|
# is to the left of B's bounding box.
|
||||||
'overlaps_left' : '&<',
|
'overlaps_left' : '&<',
|
||||||
# The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box.
|
# The "&>" operator returns true if A's bounding box overlaps or
|
||||||
|
# is to the right of B's bounding box.
|
||||||
'overlaps_right' : '&>',
|
'overlaps_right' : '&>',
|
||||||
# The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box.
|
# The "<<" operator returns true if A's bounding box is strictly
|
||||||
|
# to the left of B's bounding box.
|
||||||
'left' : '<<',
|
'left' : '<<',
|
||||||
# The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box.
|
# The ">>" operator returns true if A's bounding box is strictly
|
||||||
|
# to the right of B's bounding box.
|
||||||
'right' : '>>',
|
'right' : '>>',
|
||||||
# The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box.
|
# The "&<|" operator returns true if A's bounding box overlaps or
|
||||||
|
# is below B's bounding box.
|
||||||
'overlaps_below' : '&<|',
|
'overlaps_below' : '&<|',
|
||||||
# The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box.
|
# The "|&>" operator returns true if A's bounding box overlaps or
|
||||||
|
# is above B's bounding box.
|
||||||
'overlaps_above' : '|&>',
|
'overlaps_above' : '|&>',
|
||||||
# The "<<|" operator returns true if A's bounding box is strictly below B's bounding box.
|
# The "<<|" operator returns true if A's bounding box is strictly
|
||||||
|
# below B's bounding box.
|
||||||
'strictly_below' : '<<|',
|
'strictly_below' : '<<|',
|
||||||
# The "|>>" operator returns true if A's bounding box is strictly above B's bounding box.
|
# The "|>>" operator returns true if A's bounding box is strictly
|
||||||
|
# above B's bounding box.
|
||||||
'strictly_above' : '|>>',
|
'strictly_above' : '|>>',
|
||||||
# The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if
|
# The "~=" operator is the "same as" operator. It tests actual
|
||||||
# A and B are the same feature, vertex-by-vertex, the operator returns true.
|
# geometric equality of two features. So if A and B are the same feature,
|
||||||
|
# vertex-by-vertex, the operator returns true.
|
||||||
'same_as' : '~=',
|
'same_as' : '~=',
|
||||||
'exact' : '~=',
|
'exact' : '~=',
|
||||||
# The "@" operator returns true if A's bounding box is completely contained by B's bounding box.
|
# The "@" operator returns true if A's bounding box is completely contained
|
||||||
|
# by B's bounding box.
|
||||||
'contained' : '@',
|
'contained' : '@',
|
||||||
# The "~" operator returns true if A's bounding box completely contains B's bounding box.
|
# The "~" operator returns true if A's bounding box completely contains
|
||||||
|
# by B's bounding box.
|
||||||
'bbcontains' : '~',
|
'bbcontains' : '~',
|
||||||
# The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the
|
# The "&&" operator returns true if A's bounding box overlaps
|
||||||
# operator returns true.
|
# B's bounding box.
|
||||||
'bboverlaps' : '&&',
|
'bboverlaps' : '&&',
|
||||||
}
|
}
|
||||||
|
|
||||||
# PostGIS Geometry Relationship Functions -- most of these use GEOS.
|
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||||
#
|
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||||
# For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling
|
# means that 'ST_' is prefixes geometry function names.
|
||||||
# the more expensive GEOS routines (called 'inline index magic').
|
if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
|
||||||
#
|
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS = {
|
|
||||||
'equals' : '%sEquals',
|
|
||||||
'disjoint' : '%sDisjoint',
|
|
||||||
'touches' : '%sTouches',
|
|
||||||
'crosses' : '%sCrosses',
|
|
||||||
'within' : '%sWithin',
|
|
||||||
'overlaps' : '%sOverlaps',
|
|
||||||
'contains' : '%sContains',
|
|
||||||
'intersects' : '%sIntersects',
|
|
||||||
'relate' : ('%sRelate', str),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be 'SQL-MM-centric'.
|
|
||||||
# Practically, this means that 'ST_' is appended to geometry function names.
|
|
||||||
if MINOR_VERSION1 >= 2 and MINOR_VERSION2 >= 2:
|
|
||||||
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2.
|
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
|
||||||
{'dwithin' : ('%sDWithin', float),
|
|
||||||
'coveredby' : '%sCoveredBy',
|
|
||||||
'covers' : '%sCovers',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
GEOM_FUNC_PREFIX = 'ST_'
|
GEOM_FUNC_PREFIX = 'ST_'
|
||||||
else:
|
else:
|
||||||
GEOM_FUNC_PREFIX = ''
|
GEOM_FUNC_PREFIX = ''
|
||||||
|
|
||||||
# Updating with the geometry function prefix.
|
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
|
||||||
for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
# first before calling the more computationally expensive GEOS routines (called
|
||||||
if isinstance(v, tuple):
|
# "inline index magic"):
|
||||||
v = list(v)
|
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
|
||||||
v[0] = v[0] % GEOM_FUNC_PREFIX
|
# 'covers'.
|
||||||
v = tuple(v)
|
POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||||
else:
|
'equals' : 'Equals',
|
||||||
v = v % GEOM_FUNC_PREFIX
|
'disjoint' : 'Disjoint',
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS[k] = v
|
'touches' : 'Touches',
|
||||||
|
'crosses' : 'Crosses',
|
||||||
|
'within' : 'Within',
|
||||||
|
'overlaps' : 'Overlaps',
|
||||||
|
'contains' : 'Contains',
|
||||||
|
'intersects' : 'Intersects',
|
||||||
|
'relate' : ('Relate', str),
|
||||||
|
}
|
||||||
|
|
||||||
|
if GEOM_FUNC_PREFIX == 'ST_':
|
||||||
|
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
|
||||||
|
for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
||||||
|
if isinstance(func, tuple):
|
||||||
|
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1])
|
||||||
|
else:
|
||||||
|
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
|
||||||
|
|
||||||
|
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||||
|
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||||
|
{'dwithin' : ('ST_DWithin', float),
|
||||||
|
'coveredby' : 'ST_CoveredBy',
|
||||||
|
'covers' : 'ST_Covers',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Any other lookup types that do not require a mapping.
|
# Any other lookup types that do not require a mapping.
|
||||||
MISC_TERMS = ['isnull']
|
MISC_TERMS = ['isnull']
|
||||||
|
|
||||||
# The quotation used for postgis (uses single quotes).
|
|
||||||
def quotename(value, dbl=False):
|
|
||||||
if dbl: return '"%s"' % value
|
|
||||||
else: return "'%s'" % value
|
|
||||||
|
|
||||||
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
|
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
|
||||||
# allowed for geographic queries.
|
# allowed for geographic queries.
|
||||||
POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first
|
POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first
|
||||||
@ -102,6 +109,14 @@ POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometr
|
|||||||
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
||||||
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
||||||
|
|
||||||
|
### PostGIS-specific Methods ###
|
||||||
|
def get_geom_func(lookup_type):
|
||||||
|
func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
|
||||||
|
if isinstance(func_info, tuple):
|
||||||
|
return func_info[0]
|
||||||
|
else:
|
||||||
|
return func_info
|
||||||
|
|
||||||
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||||
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
||||||
if table_prefix.endswith('.'):
|
if table_prefix.endswith('.'):
|
||||||
@ -116,12 +131,41 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
|||||||
|
|
||||||
# See if a PostGIS Geometry function matches the lookup type next
|
# See if a PostGIS Geometry function matches the lookup type next
|
||||||
try:
|
try:
|
||||||
return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name)
|
lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||||
|
# 'dwithin' lookup types.
|
||||||
|
if isinstance(lookup_info, tuple):
|
||||||
|
# First element of tuple is lookup type, second element is the type
|
||||||
|
# of the expected argument (e.g., str, float)
|
||||||
|
func, arg_type = lookup_info
|
||||||
|
|
||||||
|
# Ensuring that a tuple _value_ was passed in from the user
|
||||||
|
if not isinstance(value, tuple) or len(value) != 2:
|
||||||
|
raise TypeError, '2-element tuple required for %s lookup type.' % lookup_type
|
||||||
|
|
||||||
|
# Ensuring the argument type matches what we expect.
|
||||||
|
if not isinstance(value[1], arg_type):
|
||||||
|
raise TypeError, 'Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))
|
||||||
|
|
||||||
|
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
|
||||||
|
else:
|
||||||
|
# Returning the SQL necessary for the geometry function call. For example:
|
||||||
|
# ST_Contains("geoapp_country"."poly", ST_GeomFromText(..))
|
||||||
|
return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name)
|
||||||
|
|
||||||
# Handling 'isnull' lookup type
|
# Handling 'isnull' lookup type
|
||||||
if lookup_type == 'isnull':
|
if lookup_type == 'isnull':
|
||||||
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
|
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
|
||||||
|
|
||||||
raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type)
|
raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type)
|
||||||
|
|
||||||
|
def geo_quotename(value, dbl=False):
|
||||||
|
"Returns the quotation used for PostGIS on a given value (uses single quotes by default)."
|
||||||
|
if isinstance(value, (StringType, UnicodeType)):
|
||||||
|
if dbl: return '"%s"' % value
|
||||||
|
else: return "'%s'" % value
|
||||||
|
else:
|
||||||
|
return str(value)
|
||||||
|
@ -9,3 +9,6 @@ class GeoManager(Manager):
|
|||||||
|
|
||||||
def kml(self, field_name, **kwargs):
|
def kml(self, field_name, **kwargs):
|
||||||
return self.get_query_set().kml(field_name, **kwargs)
|
return self.get_query_set().kml(field_name, **kwargs)
|
||||||
|
|
||||||
|
def transform(self, field_name, **kwargs):
|
||||||
|
return self.get_query_set().transform(field_name, **kwargs)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import operator
|
import operator
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import backend
|
from django.db import backend
|
||||||
from django.db.models.query import Q, QuerySet
|
from django.db.models.query import Q, QuerySet, handle_legacy_orderlist, quote_only_if_word
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
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
|
||||||
from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend.
|
from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend.
|
||||||
|
|
||||||
@ -16,12 +17,16 @@ class GeoQ(Q):
|
|||||||
class GeoQuerySet(QuerySet):
|
class GeoQuerySet(QuerySet):
|
||||||
"Geographical-enabled QuerySet object."
|
"Geographical-enabled QuerySet object."
|
||||||
|
|
||||||
|
#### Overloaded QuerySet Routines ####
|
||||||
def __init__(self, model=None):
|
def __init__(self, model=None):
|
||||||
super(GeoQuerySet, self).__init__(model=model)
|
super(GeoQuerySet, self).__init__(model=model)
|
||||||
|
|
||||||
# We only want to use the GeoQ object for our queries
|
# We only want to use the GeoQ object for our queries
|
||||||
self._filters = GeoQ()
|
self._filters = GeoQ()
|
||||||
|
|
||||||
|
# For replacement fields in the SELECT.
|
||||||
|
self._custom_select = {}
|
||||||
|
|
||||||
def _filter_or_exclude(self, mapper, *args, **kwargs):
|
def _filter_or_exclude(self, mapper, *args, **kwargs):
|
||||||
# mapper is a callable used to transform Q objects,
|
# mapper is a callable used to transform Q objects,
|
||||||
# or None for identity transform
|
# or None for identity transform
|
||||||
@ -33,11 +38,113 @@ class GeoQuerySet(QuerySet):
|
|||||||
|
|
||||||
clone = self._clone()
|
clone = self._clone()
|
||||||
if len(kwargs) > 0:
|
if len(kwargs) > 0:
|
||||||
clone._filters = clone._filters & mapper(GeoQ(**kwargs)) # Using the GeoQ object for our filters instead
|
# Using the GeoQ object for our filters instead
|
||||||
|
clone._filters = clone._filters & mapper(GeoQ(**kwargs))
|
||||||
if len(args) > 0:
|
if len(args) > 0:
|
||||||
clone._filters = clone._filters & reduce(operator.and_, map(mapper, args))
|
clone._filters = clone._filters & reduce(operator.and_, map(mapper, args))
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
def _get_sql_clause(self):
|
||||||
|
opts = self.model._meta
|
||||||
|
|
||||||
|
# Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
|
||||||
|
select = []
|
||||||
|
|
||||||
|
# This is the only component of this routine that is customized for the
|
||||||
|
# GeoQuerySet. Specifically, this allows operations to be done on fields
|
||||||
|
# in the SELECT, overriding their values -- this is different from using
|
||||||
|
# QuerySet.extra(select=foo) because extra() adds an an _additional_
|
||||||
|
# field to be selected. Used in returning transformed geometries.
|
||||||
|
for f in opts.fields:
|
||||||
|
if f.column in self._custom_select: select.append(self._custom_select[f.column])
|
||||||
|
else: select.append(self._field_column(f))
|
||||||
|
|
||||||
|
tables = [quote_only_if_word(t) for t in self._tables]
|
||||||
|
joins = SortedDict()
|
||||||
|
where = self._where[:]
|
||||||
|
params = self._params[:]
|
||||||
|
|
||||||
|
# Convert self._filters into SQL.
|
||||||
|
joins2, where2, params2 = self._filters.get_sql(opts)
|
||||||
|
joins.update(joins2)
|
||||||
|
where.extend(where2)
|
||||||
|
params.extend(params2)
|
||||||
|
|
||||||
|
# Add additional tables and WHERE clauses based on select_related.
|
||||||
|
if self._select_related:
|
||||||
|
fill_table_cache(opts, select, tables, where,
|
||||||
|
old_prefix=opts.db_table,
|
||||||
|
cache_tables_seen=[opts.db_table],
|
||||||
|
max_depth=self._max_related_depth)
|
||||||
|
|
||||||
|
# Add any additional SELECTs.
|
||||||
|
if self._select:
|
||||||
|
select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), backend.quote_name(s[0])) for s in self._select.items()])
|
||||||
|
|
||||||
|
# Start composing the body of the SQL statement.
|
||||||
|
sql = [" FROM", backend.quote_name(opts.db_table)]
|
||||||
|
|
||||||
|
# Compose the join dictionary into SQL describing the joins.
|
||||||
|
if joins:
|
||||||
|
sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition)
|
||||||
|
for (alias, (table, join_type, condition)) in joins.items()]))
|
||||||
|
|
||||||
|
# Compose the tables clause into SQL.
|
||||||
|
if tables:
|
||||||
|
sql.append(", " + ", ".join(tables))
|
||||||
|
|
||||||
|
# Compose the where clause into SQL.
|
||||||
|
if where:
|
||||||
|
sql.append(where and "WHERE " + " AND ".join(where))
|
||||||
|
|
||||||
|
# ORDER BY clause
|
||||||
|
order_by = []
|
||||||
|
if self._order_by is not None:
|
||||||
|
ordering_to_use = self._order_by
|
||||||
|
else:
|
||||||
|
ordering_to_use = opts.ordering
|
||||||
|
for f in handle_legacy_orderlist(ordering_to_use):
|
||||||
|
if f == '?': # Special case.
|
||||||
|
order_by.append(backend.get_random_function_sql())
|
||||||
|
else:
|
||||||
|
if f.startswith('-'):
|
||||||
|
col_name = f[1:]
|
||||||
|
order = "DESC"
|
||||||
|
else:
|
||||||
|
col_name = f
|
||||||
|
order = "ASC"
|
||||||
|
if "." in col_name:
|
||||||
|
table_prefix, col_name = col_name.split('.', 1)
|
||||||
|
table_prefix = backend.quote_name(table_prefix) + '.'
|
||||||
|
else:
|
||||||
|
# Use the database table as a column prefix if it wasn't given,
|
||||||
|
# and if the requested column isn't a custom SELECT.
|
||||||
|
if "." not in col_name and col_name not in (self._select or ()):
|
||||||
|
table_prefix = backend.quote_name(opts.db_table) + '.'
|
||||||
|
else:
|
||||||
|
table_prefix = ''
|
||||||
|
order_by.append('%s%s %s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts)), order))
|
||||||
|
if order_by:
|
||||||
|
sql.append("ORDER BY " + ", ".join(order_by))
|
||||||
|
|
||||||
|
# LIMIT and OFFSET clauses
|
||||||
|
if self._limit is not None:
|
||||||
|
sql.append("%s " % backend.get_limit_offset_sql(self._limit, self._offset))
|
||||||
|
else:
|
||||||
|
assert self._offset is None, "'offset' is not allowed without 'limit'"
|
||||||
|
|
||||||
|
return select, " ".join(sql), params
|
||||||
|
|
||||||
|
def _clone(self, klass=None, **kwargs):
|
||||||
|
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
|
||||||
|
c._custom_select = self._custom_select
|
||||||
|
return c
|
||||||
|
|
||||||
|
#### Methods specific to the GeoQuerySet ####
|
||||||
|
def _field_column(self, field):
|
||||||
|
return "%s.%s" % (backend.quote_name(self.model._meta.db_table),
|
||||||
|
backend.quote_name(field.column))
|
||||||
|
|
||||||
def kml(self, field_name, precision=8):
|
def kml(self, field_name, precision=8):
|
||||||
"""Returns KML representation of the given field name in a `kml`
|
"""Returns KML representation of the given field name in a `kml`
|
||||||
attribute on each element of the QuerySet."""
|
attribute on each element of the QuerySet."""
|
||||||
@ -51,8 +158,23 @@ class GeoQuerySet(QuerySet):
|
|||||||
field = self.model._meta.get_field(field_name)
|
field = self.model._meta.get_field(field_name)
|
||||||
if not isinstance(field, GeometryField):
|
if not isinstance(field, GeometryField):
|
||||||
raise TypeError, 'KML output only available on GeometryField fields.'
|
raise TypeError, 'KML output only available on GeometryField fields.'
|
||||||
field_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table),
|
field_col = self._field_column(field)
|
||||||
backend.quote_name(field.column))
|
|
||||||
|
|
||||||
# Adding the AsKML function call to the SELECT part of the SQL.
|
# Adding the AsKML function call to the SELECT part of the SQL.
|
||||||
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
|
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
|
||||||
|
|
||||||
|
def transform(self, field_name, srid=4326):
|
||||||
|
"""Transforms the given geometry field to the given SRID. If no SRID is
|
||||||
|
provided, the transformation will default to using 4326 (WGS84)."""
|
||||||
|
field = self.model._meta.get_field(field_name)
|
||||||
|
if not isinstance(field, GeometryField):
|
||||||
|
raise TypeError, 'ST_Transform() only available for GeometryField fields.'
|
||||||
|
|
||||||
|
# Setting the key for the field's column with the custom SELECT SQL to
|
||||||
|
# override the geometry column returned from the database.
|
||||||
|
self._custom_select[field.column] = \
|
||||||
|
'(ST_Transform(%s, %s)) AS %s' % (self._field_column(field), srid,
|
||||||
|
backend.quote_name(field.column))
|
||||||
|
return self._clone()
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,6 +85,23 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
|
ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
|
||||||
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
|
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
|
||||||
|
|
||||||
|
def test04_transform(self):
|
||||||
|
"Testing the transform() queryset method."
|
||||||
|
|
||||||
|
# Pre-transformed points for Houston and Pueblo.
|
||||||
|
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
|
||||||
|
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
|
||||||
|
|
||||||
|
# Asserting the result of the transform operation with the values in
|
||||||
|
# the pre-transformed points.
|
||||||
|
h = City.objects.transform('point', srid=htown.srid).get(name='Houston')
|
||||||
|
self.assertAlmostEqual(htown.x, h.point.x, 8)
|
||||||
|
self.assertAlmostEqual(htown.y, h.point.y, 8)
|
||||||
|
|
||||||
|
p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
|
||||||
|
self.assertAlmostEqual(ptown.x, p.point.x, 8)
|
||||||
|
self.assertAlmostEqual(ptown.y, p.point.y, 8)
|
||||||
|
|
||||||
def test10_contains_contained(self):
|
def test10_contains_contained(self):
|
||||||
"Testing the 'contained' and 'contains' lookup types."
|
"Testing the 'contained' and 'contains' lookup types."
|
||||||
|
|
||||||
@ -200,6 +217,45 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
self.assertEqual(2, len(qs))
|
self.assertEqual(2, len(qs))
|
||||||
for c in qs: self.assertEqual(True, c.name in cities)
|
for c in qs: self.assertEqual(True, c.name in cities)
|
||||||
|
|
||||||
|
def test14_equals(self):
|
||||||
|
"Testing the 'same_as' and 'equals' lookup types."
|
||||||
|
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
||||||
|
c1 = City.objects.get(point=pnt)
|
||||||
|
c2 = City.objects.get(point__same_as=pnt)
|
||||||
|
c3 = City.objects.get(point__equals=pnt)
|
||||||
|
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
||||||
|
|
||||||
|
def test15_relate(self):
|
||||||
|
"Testing the 'relate' lookup type."
|
||||||
|
# To make things more interesting, we will have our Texas reference point in
|
||||||
|
# different SRIDs.
|
||||||
|
pnt1 = fromstr('POINT (649287.0363174345111474 4177429.4494686722755432)', srid=2847)
|
||||||
|
pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
|
||||||
|
|
||||||
|
# Testing bad argument tuples that should return a TypeError
|
||||||
|
bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')]
|
||||||
|
for args in bad_args:
|
||||||
|
try:
|
||||||
|
qs = Country.objects.filter(mpoly__relate=args)
|
||||||
|
cnt = qs.count()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.fail('Expected a TypeError')
|
||||||
|
|
||||||
|
# 'T*T***FF*' => Contains()
|
||||||
|
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T*T***FF*')).name)
|
||||||
|
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T*T***FF*')).name)
|
||||||
|
|
||||||
|
# 'T*F**F***' => Within()
|
||||||
|
ks = State.objects.get(name='Kansas')
|
||||||
|
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T*F**F***')).name)
|
||||||
|
|
||||||
|
# 'T********' => Intersects()
|
||||||
|
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T********')).name)
|
||||||
|
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T********')).name)
|
||||||
|
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T********')).name)
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
s.addTest(unittest.makeSuite(GeoModelTest))
|
s.addTest(unittest.makeSuite(GeoModelTest))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user