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':
|
||||
# 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, \
|
||||
POSTGIS_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause
|
||||
create_spatial_db, geo_quotename, get_geo_where_clause
|
||||
else:
|
||||
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
|
||||
# with the get_geo_where_clause()
|
||||
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.
|
||||
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
||||
|
||||
# Getting the geographic parameters from the field.
|
||||
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 isinstance(geo_params, dict):
|
||||
gwc = gwc % geo_params['where']
|
||||
param_dict = isinstance(geo_params, dict)
|
||||
if param_dict:
|
||||
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)
|
||||
params.extend(geo_params)
|
||||
else:
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""
|
||||
The PostGIS spatial database backend module.
|
||||
"""
|
||||
from query import \
|
||||
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
get_geo_where_clause, geo_quotename, \
|
||||
GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
|
||||
from creation import create_spatial_db
|
||||
from field import PostGISField
|
||||
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.postgis.field import PostGISField
|
||||
|
||||
# Whether PostGIS has AsKML() support.
|
||||
if MAJOR_VERSION == 1:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
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 query import POSTGIS_TERMS, quotename
|
||||
|
||||
class PostGISField(Field):
|
||||
def _add_geom(self, style, db_table):
|
||||
@ -66,16 +66,18 @@ class PostGISField(Field):
|
||||
return sql
|
||||
|
||||
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 == 'isnull': return [value] # special case for NULL geometries.
|
||||
if not bool(value): return [None] # If invalid value passed in.
|
||||
if isinstance(value, GEOSGeometry):
|
||||
# GEOSGeometry instance passed in.
|
||||
if value.srid != self._srid:
|
||||
# Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key
|
||||
# to the where parameters, since we need to transform the geometry in the query.
|
||||
return {'where' : "Transform(%s,%s)",
|
||||
# Returning a dictionary instructs the parse_lookup() to add
|
||||
# what's in the 'where' key to the where parameters, since we
|
||||
# need to transform the geometry in the query.
|
||||
return {'where' : ["ST_Transform(%s,%s)"],
|
||||
'params' : [value, self._srid]
|
||||
}
|
||||
else:
|
||||
@ -102,6 +104,6 @@ class PostGISField(Field):
|
||||
"Provides a proper substitution value for "
|
||||
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return 'Transform(%%s, %s)' % self._srid
|
||||
return 'ST_Transform(%%s, %s)' % self._srid
|
||||
else:
|
||||
return '%s'
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
See PostGIS docs at Ch. 6.2.1 for more information on these functions.
|
||||
"""
|
||||
import re
|
||||
|
||||
def _get_postgis_func(func):
|
||||
"Helper routine for calling PostGIS functions and returning their result."
|
||||
@ -13,6 +14,7 @@ def _get_postgis_func(func):
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
### PostGIS management functions ###
|
||||
def postgis_geos_version():
|
||||
"Returns the version of the GEOS library used with PostGIS."
|
||||
return _get_postgis_func('postgis_geos_version')
|
||||
@ -33,4 +35,22 @@ def postgis_full_version():
|
||||
"Returns PostGIS version number and compile-time options."
|
||||
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.
|
||||
"""
|
||||
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
|
||||
POSTGIS_VERSION = postgis_lib_version()
|
||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.'))
|
||||
# Getting the PostGIS version information
|
||||
POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple()
|
||||
|
||||
# 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.
|
||||
if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1:
|
||||
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work.
|
||||
# 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
|
||||
|
||||
# PostGIS-specific operators. The commented descriptions of these
|
||||
# operators come from Section 6.2.2 of the official PostGIS documentation.
|
||||
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' : '&<',
|
||||
# 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' : '&>',
|
||||
# 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' : '<<',
|
||||
# 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' : '>>',
|
||||
# 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' : '&<|',
|
||||
# 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' : '|&>',
|
||||
# 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' : '<<|',
|
||||
# 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' : '|>>',
|
||||
# The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if
|
||||
# A and B are the same feature, vertex-by-vertex, the operator returns true.
|
||||
# The "~=" operator is the "same as" operator. It tests actual
|
||||
# geometric equality of two features. So if A and B are the same feature,
|
||||
# vertex-by-vertex, the operator returns true.
|
||||
'same_as' : '~=',
|
||||
'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' : '@',
|
||||
# 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' : '~',
|
||||
# The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the
|
||||
# operator returns true.
|
||||
# The "&&" operator returns true if A's bounding box overlaps
|
||||
# B's bounding box.
|
||||
'bboverlaps' : '&&',
|
||||
}
|
||||
|
||||
# PostGIS Geometry Relationship Functions -- most of these use GEOS.
|
||||
#
|
||||
# For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling
|
||||
# the more expensive GEOS routines (called 'inline index magic').
|
||||
#
|
||||
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',
|
||||
}
|
||||
)
|
||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||
# means that 'ST_' is prefixes geometry function names.
|
||||
if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
|
||||
GEOM_FUNC_PREFIX = 'ST_'
|
||||
else:
|
||||
GEOM_FUNC_PREFIX = ''
|
||||
|
||||
# Updating with the geometry function prefix.
|
||||
for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
||||
if isinstance(v, tuple):
|
||||
v = list(v)
|
||||
v[0] = v[0] % GEOM_FUNC_PREFIX
|
||||
v = tuple(v)
|
||||
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
|
||||
# first before calling the more computationally expensive GEOS routines (called
|
||||
# "inline index magic"):
|
||||
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
|
||||
# 'covers'.
|
||||
POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||
'equals' : 'Equals',
|
||||
'disjoint' : 'Disjoint',
|
||||
'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:
|
||||
v = v % GEOM_FUNC_PREFIX
|
||||
POSTGIS_GEOMETRY_FUNCTIONS[k] = v
|
||||
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.
|
||||
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
|
||||
# allowed for geographic queries.
|
||||
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 = 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):
|
||||
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
||||
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
|
||||
try:
|
||||
return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name)
|
||||
lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
|
||||
except KeyError:
|
||||
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
|
||||
if lookup_type == 'isnull':
|
||||
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
|
||||
|
||||
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):
|
||||
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
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
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.utils.datastructures import SortedDict
|
||||
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.
|
||||
|
||||
@ -16,12 +17,16 @@ class GeoQ(Q):
|
||||
class GeoQuerySet(QuerySet):
|
||||
"Geographical-enabled QuerySet object."
|
||||
|
||||
#### Overloaded QuerySet Routines ####
|
||||
def __init__(self, model=None):
|
||||
super(GeoQuerySet, self).__init__(model=model)
|
||||
|
||||
# We only want to use the GeoQ object for our queries
|
||||
self._filters = GeoQ()
|
||||
|
||||
# For replacement fields in the SELECT.
|
||||
self._custom_select = {}
|
||||
|
||||
def _filter_or_exclude(self, mapper, *args, **kwargs):
|
||||
# mapper is a callable used to transform Q objects,
|
||||
# or None for identity transform
|
||||
@ -33,11 +38,113 @@ class GeoQuerySet(QuerySet):
|
||||
|
||||
clone = self._clone()
|
||||
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:
|
||||
clone._filters = clone._filters & reduce(operator.and_, map(mapper, args))
|
||||
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):
|
||||
"""Returns KML representation of the given field name in a `kml`
|
||||
attribute on each element of the QuerySet."""
|
||||
@ -51,8 +158,23 @@ class GeoQuerySet(QuerySet):
|
||||
field = self.model._meta.get_field(field_name)
|
||||
if not isinstance(field, GeometryField):
|
||||
raise TypeError, 'KML output only available on GeometryField fields.'
|
||||
field_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table),
|
||||
backend.quote_name(field.column))
|
||||
field_col = self._field_column(field)
|
||||
|
||||
# Adding the AsKML function call to the SELECT part of the SQL.
|
||||
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')
|
||||
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):
|
||||
"Testing the 'contained' and 'contains' lookup types."
|
||||
|
||||
@ -200,6 +217,45 @@ class GeoModelTest(unittest.TestCase):
|
||||
self.assertEqual(2, len(qs))
|
||||
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():
|
||||
s = unittest.TestSuite()
|
||||
s.addTest(unittest.makeSuite(GeoModelTest))
|
||||
|
Loading…
x
Reference in New Issue
Block a user