1
0
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:
Justin Bronn 2007-08-13 01:33:26 +00:00
parent fc1dbe70fe
commit dcf2f144d1
8 changed files with 350 additions and 82 deletions

View File

@ -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']
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:

View File

@ -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:

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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))