diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index afda586dea..514a9e02d6 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -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. - 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: diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 7a1cab251b..154e20b9bd 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -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: diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py index 09bc4e642b..2f2834f31f 100644 --- a/django/contrib/gis/db/backend/postgis/field.py +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -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' diff --git a/django/contrib/gis/db/backend/postgis/management.py b/django/contrib/gis/db/backend/postgis/management.py index 33ff44066f..e323d83f7d 100644 --- a/django/contrib/gis/db/backend/postgis/management.py +++ b/django/contrib/gis/db/backend/postgis/management.py @@ -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\d)\.(?P\d)\.(?P\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) + + + diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 3e80d285f8..3dbba3ceb2 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -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) - else: - v = v % GEOM_FUNC_PREFIX - POSTGIS_GEOMETRY_FUNCTIONS[k] = 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: + 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) diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index e01f1aa724..f0615104bc 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -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) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 6bf51fc231..8c1cba3c45 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -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() + + diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index f416b5b355..e7f140dc0d 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -85,6 +85,23 @@ class GeoModelTest(unittest.TestCase): ptown = City.objects.kml('point', precision=9).get(name='Pueblo') self.assertEqual('-104.609252,38.255001,0', 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))