Author: jbronn
Date: 2007-08-12 20:33:26 -0500 (Sun, 12 Aug 2007)
New Revision: 5881
Modified:
django/branches/gis/django/contrib/gis/db/backend/__init__.py
django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
django/branches/gis/django/contrib/gis/db/models/manager.py
django/branches/gis/django/contrib/gis/db/models/query.py
django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
Log:
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.
Modified: django/branches/gis/django/contrib/gis/db/backend/__init__.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/__init__.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/__init__.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -18,10 +18,10 @@
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 @@
# 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.
- 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 isinstance(geo_params, dict):
- gwc = gwc % geo_params['where']
- geo_params = geo_params['params']
+ 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:
Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -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:
Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -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 @@
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 @@
"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'
Modified:
django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -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 @@
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 @@
"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)
+
+
+
Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -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').
-#
+# 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 = ''
+
+# 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' : '%sEquals',
- 'disjoint' : '%sDisjoint',
- 'touches' : '%sTouches',
- 'crosses' : '%sCrosses',
- 'within' : '%sWithin',
- 'overlaps' : '%sOverlaps',
- 'contains' : '%sContains',
- 'intersects' : '%sIntersects',
- 'relate' : ('%sRelate', str),
+ 'equals' : 'Equals',
+ 'disjoint' : 'Disjoint',
+ 'touches' : 'Touches',
+ 'crosses' : 'Crosses',
+ 'within' : 'Within',
+ 'overlaps' : 'Overlaps',
+ 'contains' : 'Contains',
+ 'intersects' : 'Intersects',
+ 'relate' : ('Relate', 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.
+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' : ('%sDWithin', float),
- 'coveredby' : '%sCoveredBy',
- 'covers' : '%sCovers',
+ {'dwithin' : ('ST_DWithin', float),
+ 'coveredby' : 'ST_CoveredBy',
+ 'covers' : 'ST_Covers',
}
)
- 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)
- else:
- v = v % GEOM_FUNC_PREFIX
- POSTGIS_GEOMETRY_FUNCTIONS[k] = v
-
# 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 += 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 @@
# 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)
Modified: django/branches/gis/django/contrib/gis/db/models/manager.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/models/manager.py 2007-08-13
01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/models/manager.py 2007-08-13
01:33:26 UTC (rev 5881)
@@ -9,3 +9,6 @@
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)
Modified: django/branches/gis/django/contrib/gis/db/models/query.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/models/query.py 2007-08-13
01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/models/query.py 2007-08-13
01:33:26 UTC (rev 5881)
@@ -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 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 @@
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 @@
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()
+
+
Modified: django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
===================================================================
--- django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
2007-08-13 01:33:26 UTC (rev 5881)
@@ -85,6 +85,23 @@
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 @@
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))
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---