Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

gis: Added distance querying capabilites via the `distance` manager m…

…ethod and the `distance_[gt|gte|lt|lte]` lookup types (works for both PostGIS and Oracle); improved Oracle query construction and fixed `transform` issues.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6886 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit fae19f8cedf14c946febd10513bfbaf9377eaf64 1 parent cc0cc9f
@jbronn jbronn authored
Showing with 786 additions and 269 deletions.
  1. +13 −34 django/contrib/gis/db/backend/__init__.py
  2. +1 −1  django/contrib/gis/db/backend/oracle/__init__.py
  3. +24 −24 django/contrib/gis/db/backend/oracle/field.py
  4. +5 −1 django/contrib/gis/db/backend/oracle/models.py
  5. +105 −26 django/contrib/gis/db/backend/oracle/query.py
  6. +1 −1  django/contrib/gis/db/backend/postgis/__init__.py
  7. +2 −2 django/contrib/gis/db/backend/postgis/adaptor.py
  8. +33 −35 django/contrib/gis/db/backend/postgis/field.py
  9. +7 −3 django/contrib/gis/db/backend/postgis/models.py
  10. +59 −46 django/contrib/gis/db/backend/postgis/query.py
  11. +3 −0  django/contrib/gis/db/backend/util.py
  12. +98 −12 django/contrib/gis/db/models/fields/__init__.py
  13. +3 −0  django/contrib/gis/db/models/manager.py
  14. +124 −26 django/contrib/gis/db/models/query.py
  15. +81 −18 django/contrib/gis/models.py
  16. +9 −4 django/contrib/gis/tests/__init__.py
  17. 0  django/contrib/gis/tests/distapp/__init__.py
  18. BIN  django/contrib/gis/tests/distapp/cities/cities.dbf
  19. +1 −0  django/contrib/gis/tests/distapp/cities/cities.prj
  20. BIN  django/contrib/gis/tests/distapp/cities/cities.shp
  21. BIN  django/contrib/gis/tests/distapp/cities/cities.shx
  22. +20 −0 django/contrib/gis/tests/distapp/models.py
  23. +86 −0 django/contrib/gis/tests/distapp/tests.py
  24. +4 −0 django/contrib/gis/tests/geoapp/models.py
  25. +8 −0 django/contrib/gis/tests/geoapp/sql/city.oracle.sql
  26. +99 −36 django/contrib/gis/tests/geoapp/tests.py
View
47 django/contrib/gis/db/backend/__init__.py
@@ -26,41 +26,33 @@
from django.contrib.gis.geos import GEOSGeometry
# These routines (needed by GeoManager), default to False.
-ASGML, ASKML, TRANSFORM, UNION= (False, False, False, False)
+ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules,
# renaming as necessary.
from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
- create_spatial_db, get_geo_where_clause, gqn, \
- ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
+ create_spatial_db, get_geo_where_clause, \
+ ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \
OracleSpatialField as GeoBackendField, \
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
- create_spatial_db, get_geo_where_clause, gqn, \
- ASGML, GEOM_SELECT, TRANSFORM, UNION
+ create_spatial_db, get_geo_where_clause, \
+ ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'oracle'
elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.mysql import \
MySQLGeoField as GeoBackendField, \
MYSQL_GIS_TERMS as GIS_TERMS, \
- create_spatial_db, get_geo_where_clause, gqn, \
+ create_spatial_db, get_geo_where_clause, \
GEOM_SELECT
SPATIAL_BACKEND = 'mysql'
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
-def geo_quotename(value):
- """
- Returns the quotation used on a given Geometry value using the geometry
- quoting from the backend (the `gqn` function).
- """
- if isinstance(value, (StringType, UnicodeType)): return gqn(value)
- else: return str(value)
-
#### query.py overloaded functions ####
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
# counterparts to support constructing SQL for geographic queries.
@@ -117,7 +109,7 @@ def parse_lookup(kwarg_items, opts):
raise ValueError, "Cannot use None as a query value"
elif callable(value):
value = value()
-
+
joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
joins.update(joins2)
where.extend(where2)
@@ -287,28 +279,15 @@ 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., `relate`, `dwithin` lookup types
- # need more than argument.
- multiple_args = isinstance(value, tuple)
-
# Getting the preparation SQL object from the field.
- if multiple_args:
- geo_prep = field.get_db_prep_lookup(lookup_type, value[0])
- else:
- geo_prep = field.get_db_prep_lookup(lookup_type, value)
-
+ geo_prep = field.get_db_prep_lookup(lookup_type, value)
+
# Getting the adapted geometry from the field.
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
-
- # A GeoFieldSQL object is returned by `get_db_prep_lookup` --
- # getting the substitution list and the geographic parameters.
- subst_list = geo_prep.where
- if multiple_args: subst_list += map(geo_quotename, value[1:])
- gwc = gwc % tuple(subst_list)
-
- # Finally, appending onto the WHERE clause, and extending with
- # the additional parameters.
- where.append(gwc)
+
+ # Substituting in the the where parameters into the geographic where
+ # clause, and extending the parameters.
+ where.append(gwc % tuple(geo_prep.where))
params.extend(geo_prep.params)
else:
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
View
2  django/contrib/gis/db/backend/oracle/__init__.py
@@ -10,5 +10,5 @@
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
from django.contrib.gis.db.backend.oracle.query import \
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
- ASGML, GEOM_SELECT, TRANSFORM, UNION
+ ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
View
48 django/contrib/gis/db/backend/oracle/field.py
@@ -6,7 +6,7 @@
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
-from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, TRANSFORM
+from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
@@ -21,12 +21,12 @@ class OracleSpatialField(Field):
empty_strings_allowed = False
- def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.00005, **kwargs):
+ def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
"""
Oracle Spatial backend needs to have the extent -- for projected coordinate
systems _you must define the extent manually_, since the coordinates are
for geodetic systems. The `tolerance` keyword specifies the tolerance
- for error (in meters).
+ for error (in meters), and defaults to 0.05 (5 centimeters).
"""
# Oracle Spatial specific keyword arguments.
self._extent = extent
@@ -104,32 +104,32 @@ def get_db_prep_lookup(self, lookup_type, value):
# special case for isnull lookup
if lookup_type == 'isnull': return GeoFieldSQL([], [])
- # When the input is not a GEOS geometry, attempt to construct one
- # from the given string input.
- if isinstance(value, GEOSGeometry):
- pass
- elif isinstance(value, (StringType, UnicodeType)):
- try:
- value = GEOSGeometry(value)
- except GEOSException:
- raise TypeError("Could not create geometry from lookup value: %s" % str(value))
- else:
- raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
-
- # Getting the SRID of the geometry, or defaulting to that of the field if
- # it is None.
- srid = get_srid(self, value)
+ # Get the geometry with SRID; defaults SRID to that
+ # of the field if it is None
+ geom = self.get_geometry(value)
# The adaptor will be used by psycopg2 for quoting the WKT.
- adapt = OracleSpatialAdaptor(value)
- if srid != self._srid:
+ adapt = OracleSpatialAdaptor(geom)
+
+ if geom.srid != self._srid:
# Adding the necessary string substitutions and parameters
# to perform a geometry transformation.
- return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
- [adapt, self._srid])
+ where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
+ params = [adapt, self._srid]
else:
- return GeoFieldSQL(['SDO_GEOMETRY(%%s, %s)' % srid], [adapt])
-
+ where = ['SDO_GEOMETRY(%%s, %s)' % geom.srid]
+ params = [adapt]
+
+ if isinstance(value, tuple):
+ if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
+ # Getting the distance parameter in the units of the field
+ where += [self.get_distance(value[1])]
+ elif lookup_type == 'relate':
+ # No extra where parameters for SDO_RELATE queries.
+ pass
+ else:
+ where += map(gqn, value[1:])
+ return GeoFieldSQL(where, params)
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
View
6 django/contrib/gis/db/backend/oracle/models.py
@@ -20,7 +20,7 @@ class Meta:
db_table = 'USER_SDO_GEOM_METADATA'
@classmethod
- def table_name_col(self):
+ def table_name_col(cls):
return 'table_name'
def __unicode__(self):
@@ -43,3 +43,7 @@ class Meta:
@property
def wkt(self):
return self.wktext
+
+ @classmethod
+ def wkt_col(cls):
+ return 'wktext'
View
131 django/contrib/gis/db/backend/oracle/query.py
@@ -2,24 +2,99 @@
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for Oracle Spatial.
"""
+import re
+from decimal import Decimal
from django.db import connection
+from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
+# The GML, distance, transform, and union procedures.
+ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
+DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
+TRANSFORM = 'SDO_CS.TRANSFORM'
+UNION = 'SDO_AGGR_UNION'
+
+class SDOOperation(object):
+ "Base class for SDO* Oracle operations."
+
+ def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
+ beg_subst='%s(%s%s, %%s'):
+ self.lookup = lookup
+ self.subst = subst
+ self.operator = operator
+ self.result = result
+ self.beg_subst = beg_subst
+ self.end_subst = ') %s %s' % (self.operator, self.result)
+
+ @property
+ def sql_subst(self):
+ return ''.join([self.beg_subst, self.subst, self.end_subst])
+
+ def as_sql(self, table, field):
+ return self.sql_subst % self.params(table, field)
+
+ def params(self, table, field):
+ return (self.lookup, table, field)
+
+class SDODistance(SDOOperation):
+ "Class for Distance queries."
+ def __init__(self, op, tolerance=0.05):
+ super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
+ self.tolerance = tolerance
+
+ def params(self, table, field):
+ return (self.lookup, table, field, self.tolerance)
+
+class SDOGeomRelate(SDOOperation):
+ "Class for using SDO_GEOM.RELATE."
+ def __init__(self, mask, tolerance=0.05):
+ super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'",
+ subst=", %%s, %s", result="'%s'" % mask)
+ self.mask = mask
+ self.tolerance = tolerance
+
+ def params(self, table, field):
+ return (self.lookup, table, field, self.mask, self.tolerance)
+
+class SDORelate(SDOOperation):
+ "Class for using SDO_RELATE."
+ masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
+ mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
+
+ def __init__(self, mask, **kwargs):
+ super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs)
+ if not self.mask_regex.match(mask):
+ raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
+ self.mask = mask
+
+ def params(self, table, field):
+ return (self.lookup, table, field, self.mask)
+
+# Valid distance types and substitutions
+dtypes = (Decimal, Distance, float, int)
+DISTANCE_FUNCTIONS = {
+ 'distance_gt' : (SDODistance('>'), dtypes),
+ 'distance_gte' : (SDODistance('>='), dtypes),
+ 'distance_lt' : (SDODistance('<'), dtypes),
+ 'distance_lte' : (SDODistance('<='), dtypes),
+ }
+
ORACLE_GEOMETRY_FUNCTIONS = {
- 'contains' : 'SDO_CONTAINS',
- 'coveredby' : 'SDO_COVEREDBY',
- 'covers' : 'SDO_COVERS',
- 'disjoint' : 'SDO_DISJOINT',
- 'dwithin' : ('SDO_WITHIN_DISTANCE', float),
- 'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
- 'equals' : 'SDO_EQUAL',
- 'exact' : 'SDO_EQUAL',
- 'overlaps' : 'SDO_OVERLAPS',
- 'same_as' : 'SDO_EQUAL',
- #'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
- 'touches' : 'SDO_TOUCH',
- 'within' : 'SDO_INSIDE',
+ 'contains' : SDOOperation('SDO_CONTAINS'),
+ 'coveredby' : SDOOperation('SDO_COVEREDBY'),
+ 'covers' : SDOOperation('SDO_COVERS'),
+ 'disjoint' : SDOGeomRelate('DISJOINT'),
+ 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
+ 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
+ 'equals' : SDOOperation('SDO_EQUAL'),
+ 'exact' : SDOOperation('SDO_EQUAL'),
+ 'overlaps' : SDOOperation('SDO_OVERLAPS'),
+ 'same_as' : SDOOperation('SDO_EQUAL'),
+ 'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
+ 'touches' : SDOOperation('SDO_TOUCH'),
+ 'within' : SDOOperation('SDO_INSIDE'),
}
+ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# This lookup type does not require a mapping.
MISC_TERMS = ['isnull']
@@ -43,25 +118,33 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
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
+ sdo_op, 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)
+ if not isinstance(value, tuple):
+ raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
+ if len(value) != 2:
+ raise ValueError('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])))
- if func == 'dwithin':
- # TODO: test and consider adding different distance options.
- return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
+ if lookup_type == 'relate':
+ # The SDORelate class handles construction for these queries, and verifies
+ # the mask argument.
+ return sdo_op(value[1]).as_sql(table_prefix, field_name)
+ elif lookup_type in DISTANCE_FUNCTIONS:
+ op = DISTANCE_FUNCTIONS[lookup_type][0]
+ return op.as_sql(table_prefix, field_name)
+ # return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
else:
- return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
+ return sdo_op.as_sql(table_prefix, field_name)
else:
- # Returning the SQL necessary for the geometry function call. For example:
+ # Lookup info is a SDOOperation instance, whos `as_sql` method returns
+ # the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
- return "%s(%s, %%s) = 'TRUE'" % (lookup_info, table_prefix + field_name)
+ return lookup_info.as_sql(table_prefix, field_name)
# Handling 'isnull' lookup type
if lookup_type == 'isnull':
@@ -69,10 +152,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
-ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
-UNION = 'SDO_AGGR_UNION'
-TRANSFORM = 'SDO_CS.TRANSFORM'
-
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
# from WKT than SDO_GEOMETRY(...) strings ;)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
View
2  django/contrib/gis/db/backend/postgis/__init__.py
@@ -6,4 +6,4 @@
from django.contrib.gis.db.backend.postgis.query import \
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
- ASKML, ASGML, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
+ ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
View
4 django/contrib/gis/db/backend/postgis/adaptor.py
@@ -7,11 +7,11 @@
from psycopg2.extensions import ISQLQuote
class PostGISAdaptor(object):
- def __init__(self, geom, srid):
+ def __init__(self, geom):
"Initializes on the geometry and the SRID."
# Getting the WKB and the SRID
self.wkb = geom.wkb
- self.srid = srid
+ self.srid = geom.srid
def __conform__(self, proto):
# Does the given protocol conform to what Psycopg2 expects?
View
68 django/contrib/gis/db/backend/postgis/field.py
@@ -1,19 +1,26 @@
-from types import StringType, UnicodeType
+from types import UnicodeType
from django.db import connection
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.util import get_srid, GeoFieldSQL
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
-from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
-from psycopg2 import Binary
+from django.contrib.gis.db.backend.postgis.query import \
+ DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively
qn = connection.ops.quote_name
def gqn(value):
- if isinstance(value, UnicodeType): value = value.encode('ascii')
- return "'%s'" % value
+ if isinstance(value, basestring):
+ if isinstance(value, UnicodeType): value = value.encode('ascii')
+ return "'%s'" % value
+ else:
+ return str(value)
class PostGISField(Field):
+ """
+ The backend-specific geographic field for PostGIS.
+ """
+
def _add_geom(self, style, db_table):
"""
Constructs the addition of the geometry to the table using the
@@ -92,53 +99,44 @@ def get_db_prep_lookup(self, lookup_type, value):
"""
if lookup_type in POSTGIS_TERMS:
# special case for isnull lookup
- if lookup_type == 'isnull':
- return GeoFieldSQL([], [value])
-
- # When the input is not a GEOS geometry, attempt to construct one
- # from the given string input.
- if isinstance(value, GEOSGeometry):
- pass
- elif isinstance(value, (StringType, UnicodeType)):
- try:
- value = GEOSGeometry(value)
- except GEOSException:
- raise TypeError("Could not create geometry from lookup value: %s" % str(value))
- else:
- raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
+ if lookup_type == 'isnull': return GeoFieldSQL([], [])
- # Getting the SRID of the geometry, or defaulting to that of the field if
- # it is None.
- srid = get_srid(self, value)
+ # Get the geometry with SRID; defaults SRID to
+ # that of the field if it is None.
+ geom = self.get_geometry(value)
# The adaptor will be used by psycopg2 for quoting the WKB.
- adapt = PostGISAdaptor(value, srid)
+ adapt = PostGISAdaptor(geom)
- if srid != self._srid:
+ if geom.srid != self._srid:
# Adding the necessary string substitutions and parameters
# to perform a geometry transformation.
- return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
- [adapt, self._srid])
+ where = ['%s(%%s,%%s)' % TRANSFORM]
+ params = [adapt, self._srid]
else:
- return GeoFieldSQL(['%s'], [adapt])
+ # Otherwise, the adaptor will take care of everything.
+ where = ['%s']
+ params = [adapt]
+
+ if isinstance(value, tuple):
+ if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
+ # Getting the distance parameter in the units of the field.
+ where += [self.get_distance(value[1])]
+ else:
+ where += map(gqn, value[1:])
+ return GeoFieldSQL(where, params)
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
+
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if not bool(value): return None
if isinstance(value, GEOSGeometry):
- return PostGISAdaptor(value, value.srid)
+ return PostGISAdaptor(value)
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
- def get_internal_type(self):
- """
- Returns NoField because a stored procedure is used by PostGIS to create
- the Geometry Fields.
- """
- return 'NoField'
-
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
View
10 django/contrib/gis/db/backend/postgis/models.py
@@ -16,17 +16,17 @@ class GeometryColumns(models.Model):
"""
f_table_catalog = models.CharField(maxlength=256)
f_table_schema = models.CharField(maxlength=256)
- f_table_name = models.CharField(maxlength=256, primary_key=True)
+ f_table_name = models.CharField(maxlength=256)
f_geometry_column = models.CharField(maxlength=256)
coord_dimension = models.IntegerField()
- srid = models.IntegerField()
+ srid = models.IntegerField(primary_key=True)
type = models.CharField(maxlength=30)
class Meta:
db_table = 'geometry_columns'
@classmethod
- def table_name_col(self):
+ def table_name_col(cls):
"Class method for returning the table name column for this model."
return 'f_table_name'
@@ -52,3 +52,7 @@ class Meta:
@property
def wkt(self):
return self.srtext
+
+ @classmethod
+ def wkt_col(cls):
+ return 'srtext'
View
105 django/contrib/gis/db/backend/postgis/query.py
@@ -2,9 +2,10 @@
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for PostGIS.
"""
+from decimal import Decimal
from django.db import connection
+from django.contrib.gis.measure import Distance
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
-from types import StringType, UnicodeType
qn = connection.ops.quote_name
# Getting the PostGIS version information
@@ -62,16 +63,38 @@
# 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_'
+GEOM_FUNC_PREFIX = ''
+if MAJOR_VERSION >= 1:
+ if (MINOR_VERSION1 > 2 or
+ (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
+ GEOM_FUNC_PREFIX = 'ST_'
+
+ def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
+
+ # Functions used by the GeoManager & GeoQuerySet
+ ASKML = get_func('AsKML')
+ ASGML = get_func('AsGML')
+ DISTANCE = get_func('Distance')
+ GEOM_FROM_TEXT = get_func('GeomFromText')
+ GEOM_FROM_WKB = get_func('GeomFromWKB')
+ TRANSFORM = get_func('Transform')
+
+ # Special cases for union and KML methods.
+ if MINOR_VERSION1 < 3:
+ UNION = 'GeomUnion'
+ else:
+ UNION = 'ST_Union'
+
+ if MINOR_VERSION1 == 1:
+ ASKML = False
else:
- GEOM_FUNC_PREFIX = ''
+ raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
# 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'.
+# 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',
@@ -81,25 +104,36 @@
'overlaps' : 'Overlaps',
'contains' : 'Contains',
'intersects' : 'Intersects',
- 'relate' : ('Relate', str),
+ 'relate' : ('Relate', basestring),
+ }
+
+# Valid distance types and substitutions
+dtypes = (Decimal, Distance, float, int)
+DISTANCE_FUNCTIONS = {
+ 'distance_gt' : ('>', dtypes),
+ 'distance_gte' : ('>=', dtypes),
+ 'distance_lt' : ('<', dtypes),
+ 'distance_lte' : ('<=', dtypes),
}
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])
+ for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
+ if isinstance(f, tuple):
+ POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
else:
- POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
+ POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
- {'dwithin' : ('ST_DWithin', float),
+ {'dwithin' : ('ST_DWithin', dtypes),
'coveredby' : 'ST_CoveredBy',
'covers' : 'ST_Covers',
}
)
+POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
+
# Any other lookup types that do not require a mapping.
MISC_TERMS = ['isnull']
@@ -139,52 +173,31 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
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)
+ if not isinstance(value, tuple):
+ raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
+ if len(value) != 2:
+ raise ValueError('2-element tuple required or `%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)
+
+ if lookup_type in DISTANCE_FUNCTIONS:
+ op = DISTANCE_FUNCTIONS[lookup_type][0]
+ return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
+ else:
+ 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_GeomFromWKB(..))
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))
-# Functions that we define manually.
-if MAJOR_VERSION == 1:
- if MINOR_VERSION1 == 3:
- # PostGIS versions 1.3.x
- ASKML = 'ST_AsKML'
- ASGML = 'ST_AsGML'
- GEOM_FROM_TEXT = 'ST_GeomFromText'
- GEOM_FROM_WKB = 'ST_GeomFromWKB'
- UNION = 'ST_Union'
- TRANSFORM = 'ST_Transform'
- elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1:
- # PostGIS versions 1.2.x
- ASKML = 'AsKML'
- ASGML = 'AsGML'
- GEOM_FROM_TEXT = 'GeomFromText'
- GEOM_FROM_WKB = 'GeomFromWKB'
- UNION = 'GeomUnion'
- TRANSFORM = 'Transform'
- elif MINOR_VERSION1 == 1 and MINOR_VERSION2 >= 0:
- # PostGIS versions 1.1.x
- ASKML = False
- ASGML = 'AsGML'
- GEOM_FROM_TEXT = 'GeomFromText'
- GEOM_FROM_WKB = 'GeomFromWKB'
- TRANSFORM = 'Transform'
- UNION = 'GeomUnion'
-
# Custom selection not needed for PostGIS since GEOS geometries may be
# instantiated directly from the HEXEWKB returned by default. If
# WKT is needed for some reason in the future, this value may be changed,
View
3  django/contrib/gis/db/backend/util.py
@@ -7,6 +7,9 @@ def __init__(self, where=[], params=[]):
self.where = where
self.params = params
+ def __str__(self):
+ return self.where[0] % tuple(self.params)
+
def get_srid(field, geom):
"""
Gets the SRID depending on the value of the SRID setting of the field
View
110 django/contrib/gis/db/models/fields/__init__.py
@@ -1,8 +1,17 @@
+from decimal import Decimal
from django.conf import settings
+from django.db import connection
from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend.
from django.contrib.gis.db.models.proxy import GeometryProxy
+from django.contrib.gis.geos import GEOSException, GEOSGeometry
+from django.contrib.gis.measure import Distance
from django.contrib.gis.oldforms import WKTField
-from django.contrib.gis.geos import GEOSGeometry
+
+# Attempting to get the spatial reference system.
+try:
+ from django.contrib.gis.models import SpatialRefSys
+except NotImplementedError:
+ SpatialRefSys = None
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
class GeometryField(GeoBackendField):
@@ -11,30 +20,107 @@ class GeometryField(GeoBackendField):
# The OpenGIS Geometry name.
_geom = 'GEOMETRY'
- def __init__(self, srid=4326, index=True, dim=2, **kwargs):
- """The initialization function for geometry fields. Takes the following
+ def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
+ """
+ The initialization function for geometry fields. Takes the following
as keyword arguments:
- srid - The spatial reference system identifier. An OGC standard.
- Defaults to 4326 (WGS84)
+ srid:
+ The spatial reference system identifier, an OGC standard.
+ Defaults to 4326 (WGS84).
- index - Indicates whether to create a GiST index. Defaults to True.
- Set this instead of 'db_index' for geographic fields since index
- creation is different for geometry columns.
+ spatial_index:
+ Indicates whether to create a spatial index. Defaults to True.
+ Set this instead of 'db_index' for geographic fields since index
+ creation is different for geometry columns.
- dim - The number of dimensions for this geometry. Defaults to 2.
+ dim:
+ The number of dimensions for this geometry. Defaults to 2.
"""
- self._index = index
+
+ # Backward-compatibility notice, this will disappear in future revisions.
+ if 'index' in kwargs:
+ from warnings import warn
+ warn('The `index` keyword has been deprecated, please use the `spatial_index` keyword instead.')
+ self._index = kwargs['index']
+ else:
+ self._index = spatial_index
+
+ # Setting the SRID and getting the units.
self._srid = srid
+ if SpatialRefSys:
+ # This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
+ # Why? `syncdb` fails to recognize installed geographic models when there's
+ # an ORM query instantiated within a model field. No matter, this works fine
+ # too.
+ cur = connection.cursor()
+ qn = connection.ops.quote_name
+ stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
+ stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
+ 'wkt_col' : qn(SpatialRefSys.wkt_col()),
+ 'srid_col' : qn('srid'),
+ 'srid' : srid,
+ }
+ cur.execute(stmt)
+ row = cur.fetchone()
+ self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
+
+ # Setting the dimension of the geometry field.
self._dim = dim
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
+ ### Routines specific to GeometryField ###
+ def get_distance(self, dist):
+ if isinstance(dist, Distance):
+ return getattr(dist, Distance.unit_attname(self._unit_name))
+ elif isinstance(dist, (int, float, Decimal)):
+ # Assuming the distance is in the units of the field.
+ return dist
+
+ def get_geometry(self, value):
+ """
+ Retrieves the geometry, setting the default SRID from the given
+ lookup parameters.
+ """
+ if isinstance(value, tuple):
+ geom = value[0]
+ else:
+ geom = value
+
+ # When the input is not a GEOS geometry, attempt to construct one
+ # from the given string input.
+ if isinstance(geom, GEOSGeometry):
+ pass
+ elif isinstance(geom, basestring):
+ try:
+ geom = GEOSGeometry(geom)
+ except GEOSException:
+ raise ValueError('Could not create geometry from lookup value: %s' % str(value))
+ else:
+ raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
+
+ # Assigning the SRID value.
+ geom.srid = self.get_srid(geom)
+
+ return geom
+
+ def get_srid(self, geom):
+ """
+ Has logic for retrieving the default SRID taking into account
+ the SRID of the field.
+ """
+ if geom.srid is None or (geom.srid == -1 and self._srid != -1):
+ return self._srid
+ else:
+ return geom.srid
+
+ ### Routines overloaded from Field ###
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)
-
+
# Setup for lazy-instantiated GEOSGeometry object.
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
-
+
def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator."
return [WKTField]
View
3  django/contrib/gis/db/models/manager.py
@@ -7,6 +7,9 @@ class GeoManager(Manager):
def get_query_set(self):
return GeoQuerySet(model=self.model)
+ def distance(self, *args, **kwargs):
+ return self.get_query_set().distance(*args, **kwargs)
+
def gml(self, *args, **kwargs):
return self.get_query_set().gml(*args, **kwargs)
View
150 django/contrib/gis/db/models/query.py
@@ -6,9 +6,13 @@
from django.utils.datastructures import SortedDict
from django.contrib.gis.db.models.fields import GeometryField
# parse_lookup depends on the spatial database backend.
-from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
+from django.contrib.gis.db.backend import parse_lookup, \
+ ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
from django.contrib.gis.geos import GEOSGeometry
+# Flag indicating whether the backend is Oracle.
+oracle = SPATIAL_BACKEND == 'oracle'
+
class GeoQ(Q):
"Geographical query encapsulation object."
@@ -28,11 +32,23 @@ def __init__(self, model=None):
# For replacement fields in the SELECT.
self._custom_select = {}
+ self._ewkt = None
# If GEOM_SELECT is defined in the backend, then it will be used
# for the selection format of the geometry column.
- if GEOM_SELECT: self._geo_fmt = GEOM_SELECT
- else: self._geo_fmt = '%s'
+ if GEOM_SELECT:
+ #if oracle and hasattr(self, '_ewkt'):
+ # Transformed geometries in Oracle use EWKT so that the SRID
+ # on the transformed lazy geometries is set correctly).
+ #print '-=' * 20
+ #print self._ewkt, GEOM_SELECT
+ #self._geo_fmt = "'SRID=%d;'||%s" % (self._ewkt, GEOM_SELECT)
+ #self._geo_fmt = GEOM_SELECT
+ #else:
+ #print '-=' * 20
+ self._geo_fmt = GEOM_SELECT
+ else:
+ self._geo_fmt = '%s'
def _filter_or_exclude(self, mapper, *args, **kwargs):
# mapper is a callable used to transform Q objects,
@@ -59,15 +75,23 @@ def _get_sql_clause(self, get_full_query=False):
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, and
- # handling the selection of native database geometry formats.
+ # 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, and
+ # handling the selection of native database geometry formats.
for f in opts.fields:
# Getting the selection format string.
- if hasattr(f, '_geom'): sel_fmt = self._geo_fmt
- else: sel_fmt = '%s'
+ if hasattr(f, '_geom'):
+ sel_fmt = self._geo_fmt
+
+ # If an SRID needs to specified other than what is in the field
+ # (like when `transform` is called), make sure to explicitly set
+ # the SRID by returning EWKT.
+ if self._ewkt and oracle:
+ sel_fmt = "'SRID=%d;'||%s" % (self._ewkt, sel_fmt)
+ else:
+ sel_fmt = '%s'
# Getting the field selection substitution string
if f.column in self._custom_select:
@@ -147,7 +171,7 @@ def _get_sql_clause(self, get_full_query=False):
sql.append("ORDER BY " + ", ".join(order_by))
# LIMIT and OFFSET clauses
- if SPATIAL_BACKEND != 'oracle':
+ if not oracle:
if self._limit is not None:
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
else:
@@ -206,6 +230,7 @@ def _get_sql_clause(self, get_full_query=False):
def _clone(self, klass=None, **kwargs):
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
c._custom_select = self._custom_select
+ c._ewkt = self._ewkt
return c
#### Methods specific to the GeoQuerySet ####
@@ -227,7 +252,60 @@ def _geo_column(self, field_name):
else:
return False
- def gml(self, field_name, precision=8, version=2):
+ def _get_geofield(self):
+ "Returns the name of the first Geometry field encountered."
+ for field in self.model._meta.fields:
+ if isinstance(field, GeometryField):
+ return field.name
+ raise Exception('No GeometryFields in the model.')
+
+ def distance(self, *args, **kwargs):
+ """
+ Returns the distance from the given geographic field name to the
+ given geometry in a `distance` attribute on each element of the
+ GeoQuerySet.
+ """
+ if not DISTANCE:
+ raise ImproperlyConfigured('Distance() stored proecedure not available.')
+
+ # Getting the geometry field and GEOSGeometry object to base distance
+ # calculations from.
+ nargs = len(args)
+ if nargs == 1:
+ field_name = self._get_geofield()
+ geom = args[0]
+ elif nargs == 2:
+ field_name, geom = args
+ else:
+ raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
+
+ # Getting the quoted column.
+ field_col = self._geo_column(field_name)
+ if not field_col:
+ raise TypeError('Distance output only available on GeometryFields.')
+
+ # Getting the geographic field instance.
+ geo_field = self.model._meta.get_field(field_name)
+
+ # Using the field's get_db_prep_lookup() to get any needed
+ # transformation SQL -- we pass in a 'dummy' `contains` lookup
+ # type.
+ geom_sql = geo_field.get_db_prep_lookup('contains', geom)
+ if oracle:
+ # The `tolerance` keyword may be used for Oracle.
+ tolerance = kwargs.get('tolerance', 0.05)
+
+ # More legwork here because the OracleSpatialAdaptor doesn't do
+ # quoting of the WKT.
+ params = ["'%s'" % geom_sql.params[0]]
+ params.extend(geom_sql.params[1:])
+ gsql = geom_sql.where[0] % tuple(params)
+ dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
+ else:
+ dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
+ return self.extra(select=dist_select)
+
+ def gml(self, field_name=None, precision=8, version=2):
"""
Returns GML representation of the given field in a `gml` attribute
on each element of the GeoQuerySet.
@@ -236,12 +314,16 @@ def gml(self, field_name, precision=8, version=2):
if not ASGML:
raise ImproperlyConfigured('AsGML() stored procedure not available.')
- # Is the given field name a geographic field?
+ # If no field name explicitly given, get the first GeometryField from
+ # the model.
+ if not field_name:
+ field_name = self._get_geofield()
+
field_col = self._geo_column(field_name)
if not field_col:
- raise TypeError('GML output only available on GeometryFields')
-
- if SPATIAL_BACKEND == 'oracle':
+ raise TypeError('GML output only available on GeometryFields.')
+
+ if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
@@ -249,7 +331,7 @@ def gml(self, field_name, precision=8, version=2):
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)
- def kml(self, field_name, precision=8):
+ def kml(self, field_name=None, precision=8):
"""
Returns KML representation of the given field name in a `kml`
attribute on each element of the GeoQuerySet.
@@ -258,7 +340,10 @@ def kml(self, field_name, precision=8):
if not ASKML:
raise ImproperlyConfigured('AsKML() stored procedure not available.')
- # Is the given field name a geographic field?
+ # Getting the geographic field.
+ if not field_name:
+ field_name = self._get_geofield()
+
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('KML output only available on GeometryFields.')
@@ -266,30 +351,40 @@ def kml(self, field_name, precision=8):
# Adding the AsKML function call to SELECT part of the SQL.
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
- def transform(self, field_name, srid=4326):
+ def transform(self, field_name=None, srid=4326):
"""
Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84).
"""
- # Is the given field name a geographic field?
+ # Getting the geographic field.
+ if not field_name:
+ field_name = self._get_geofield()
+ elif isinstance(field_name, int):
+ srid = field_name
+ field_name = self._get_geofield()
+
field = self.model._meta.get_field(field_name)
if not isinstance(field, GeometryField):
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
- # If there's already custom select SQL.
+ # Why cascading substitutions? Because spatial backends like
+ # Oracle and MySQL already require a function call to convert to text, thus
+ # when there's also a transformation we need to cascade the substitutions.
+ # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
col = self._custom_select.get(field.column, self._field_column(field))
# Setting the key for the field's column with the custom SELECT SQL to
- # override the geometry column returned from the database.
- if SPATIAL_BACKEND == 'oracle':
+ # override the geometry column returned from the database.
+ if oracle:
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
+ self._ewkt = srid
else:
custom_sel = '(%s(%s, %s)) AS %s' % \
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
self._custom_select[field.column] = custom_sel
return self._clone()
- def union(self, field_name, tolerance=0.0005):
+ def union(self, field_name=None, tolerance=0.0005):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty. The `tolerance` keyword is for
@@ -300,6 +395,9 @@ def union(self, field_name, tolerance=0.0005):
raise ImproperlyConfigured('Union stored procedure not available.')
# Getting the geographic field column
+ if not field_name:
+ field_name = self._get_geofield()
+
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('Aggregate Union only available on GeometryFields.')
@@ -312,7 +410,7 @@ def union(self, field_name, tolerance=0.0005):
# Replacing the select with a call to the ST_Union stored procedure
# on the geographic field column.
- if SPATIAL_BACKEND == 'oracle':
+ if oracle:
union_sql = 'SELECT %s' % self._geo_fmt
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
union_sql += sql
@@ -323,7 +421,7 @@ def union(self, field_name, tolerance=0.0005):
cursor = connection.cursor()
cursor.execute(union_sql, params)
- if SPATIAL_BACKEND == 'oracle':
+ if oracle:
# On Oracle have to read out WKT from CLOB first.
clob = cursor.fetchone()[0]
if clob: u = clob.read()
View
99 django/contrib/gis/models.py
@@ -10,17 +10,24 @@
if HAS_GDAL:
from django.contrib.gis.gdal import SpatialReference
-# For pulling out the spheroid from the spatial reference string. This
-# regular expression is used only if the user does not have GDAL installed.
-# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
-# parameter.
-spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
-
class SpatialRefSysMixin(object):
"""
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
+
+ # For pulling out the spheroid from the spatial reference string. This
+ # regular expression is used only if the user does not have GDAL installed.
+ # TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
+ # parameter.
+ spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
+
+ # For pulling out the units on platforms w/o GDAL installed.
+ # TODO: Figure out how to pull out angular units of projected coordinate system and
+ # fix for LOCAL_CS types. GDAL should be highly recommended for performing
+ # distance queries.
+ units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
+
@property
def srs(self):
"""
@@ -53,7 +60,7 @@ def ellipsoid(self):
if HAS_GDAL:
return self.srs.ellipsoid
else:
- m = spheroid_regex.match(self.wkt)
+ m = self.spheroid_regex.match(self.wkt)
if m: return (float(m.group('major')), float(m.group('flattening')))
else: return None
@@ -75,37 +82,93 @@ def datum(self):
@property
def projected(self):
"Is this Spatial Reference projected?"
- return self.srs.projected
+ if HAS_GDAL:
+ return self.srs.projected
+ else:
+ return self.wkt.startswith('PROJCS')
@property
def local(self):
"Is this Spatial Reference local?"
- return self.srs.local
+ if HAS_GDAL:
+ return self.srs.local
+ else:
+ return self.wkt.startswith('LOCAL_CS')
@property
def geographic(self):
"Is this Spatial Reference geographic?"
- return self.srs.geographic
+ if HAS_GDAL:
+ return self.srs.geographic
+ else:
+ return self.wkt.startswith('GEOGCS')
@property
def linear_name(self):
"Returns the linear units name."
- return self.srs.linear_name
-
+ if HAS_GDAL:
+ return self.srs.linear_name
+ elif self.geographic:
+ return None
+ else:
+ m = self.units_regex.match(self.wkt)
+ return m.group('unit_name')
+
@property
def linear_units(self):
"Returns the linear units."
- return self.srs.linear_units
+ if HAS_GDAL:
+ return self.srs.linear_units
+ elif self.geographic:
+ return None
+ else:
+ m = self.units_regex.match(self.wkt)
+ return m.group('unit')
+
+ @property
+ def angular_name(self):
+ "Returns the name of the angular units."
+ if HAS_GDAL:
+ return self.srs.angular_name
+ elif self.projected:
+ return None
+ else:
+ m = self.units_regex.match(self.wkt)
+ return m.group('unit_name')
@property
def angular_units(self):
"Returns the angular units."
- return self.srs.angular_units
+ if HAS_GDAL:
+ return self.srs.angular_units
+ elif self.projected:
+ return None
+ else:
+ m = self.units_regex.match(self.wkt)
+ return m.group('unit')
@property
- def angular_name(self):
- "Returns the name of the angular units."
- return self.srs.angular_name
+ def units(self):
+ "Returns a tuple of the units and the name."
+ if self.projected or self.local:
+ return (self.linear_units, self.linear_name)
+ elif self.geographic:
+ return (self.angular_units, self.angular_name)
+ else:
+ return (None, None)
+
+ @classmethod
+ def get_units(cls, wkt):
+ """
+ Class method used by GeometryField on initialization to
+ retrive the units on the given WKT, without having to use
+ any of the database fields.
+ """
+ if HAS_GDAL:
+ return SpatialReference(wkt).units
+ else:
+ m = cls.units_regex.match(wkt)
+ return m.group('unit'), m.group('unit_name')
def __unicode__(self):
"""
@@ -115,7 +178,7 @@ def __unicode__(self):
try:
return unicode(self.srs)
except:
- return unicode(self.srtext)
+ return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
View
13 django/contrib/gis/tests/__init__.py
@@ -3,9 +3,9 @@
from unittest import TestSuite, TextTestRunner
from django.contrib.gis.gdal import HAS_GDAL
try:
- from django.contrib.gis.tests.utils import mysql
+ from django.contrib.gis.tests.utils import mysql, oracle
except:
- mysql = False
+ mysql, oracle = (False, False)
# Tests that require use of a spatial database (e.g., creation of models)
test_models = ['geoapp']
@@ -16,7 +16,12 @@
'test_measure',
]
if HAS_GDAL:
- test_models += ['layermap']
+ if oracle:
+ # TODO: There is a problem with the `syncdb` SQL for the LayerMapping
+ # tests on Oracle.
+ test_models += ['distapp']
+ else:
+ test_models += ['distapp', 'layermap']
test_suite_names += [
'test_gdal_driver',
'test_gdal_ds',
@@ -54,7 +59,7 @@ def run_tests(module_list, verbosity=1, interactive=True):
(3) Start this database `pg_ctl -D /path/to/user/db start`
On Windows platforms simply use the pgAdmin III utility to add superuser
- priviliges to your database user.
+ privileges to your database user.
Make sure your settings.py matches the settings of the user database.
For example, set the same port number (`DATABASE_PORT=5433`).
View
0  django/contrib/gis/tests/distapp/__init__.py
No changes.
View
BIN  django/contrib/gis/tests/distapp/cities/cities.dbf
Binary file not shown
View
1  django/contrib/gis/tests/distapp/cities/cities.prj
@@ -0,0 +1 @@
+PROJCS["NAD83 / Texas South Central",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",30.28333333333334],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["Meter",1]]
View
BIN  django/contrib/gis/tests/distapp/cities/cities.shp
Binary file not shown
View
BIN  django/contrib/gis/tests/distapp/cities/cities.shx
Binary file not shown
View
20 django/contrib/gis/tests/distapp/models.py
@@ -0,0 +1,20 @@
+from django.contrib.gis.db import models
+
+class City(models.Model):
+ name = models.CharField(max_length=30)
+ point = models.PointField(srid=32140)
+ objects = models.GeoManager()
+ def __unicode__(self): return self.name
+
+#class County(models.Model):
+# name = models.CharField(max_length=30)
+# mpoly = models.MultiPolygonField(srid=32140)
+# objects = models.GeoManager()
+
+city_mapping = {'name' : 'Name',
+ 'point' : 'POINT',
+ }
+
+#county_mapping = {'name' : 'Name',
+# 'mpoly' : 'MULTIPOLYGON',
+# }
View
86 django/contrib/gis/tests/distapp/tests.py
@@ -0,0 +1,86 @@
+import os, unittest
+from decimal import Decimal
+from models import *
+from django.contrib.gis.utils import LayerMapping
+from django.contrib.gis.gdal import DataSource
+from django.contrib.gis.geos import GEOSGeometry
+from django.contrib.gis.measure import D # alias for Distance
+from django.contrib.gis.tests.utils import oracle
+
+shp_path = os.path.dirname(__file__)
+city_shp = os.path.join(shp_path, 'cities/cities.shp')
+#county_shp = os.path.join(shp_path, 'counties/counties.shp')
+
+class DistanceTest(unittest.TestCase):
+
+ def test01_init(self):
+ "LayerMapping initialization of distance models."
+
+ city_lm = LayerMapping(City, city_shp, city_mapping, transform=False)
+ city_lm.save()
+
+ # TODO: complete tests with distance from multipolygons.
+ #county_lm = LayerMapping(County, county_shp, county_mapping, transform=False)
+ #county_lm.save()
+
+ self.assertEqual(12, City.objects.count())
+ #self.assertEqual(60, County.objects.count())
+
+ # TODO: Complete tests for `dwithin` lookups.
+ #def test02_dwithin(self):
+ # "Testing the `dwithin` lookup type."
+ # pass
+
+ def test03_distance_aggregate(self):
+ "Testing the `distance` GeoQuerySet method."
+ # The point for La Grange, TX
+ lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
+ # Got these from using the raw SQL statement:
+ # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_city;
+ distances = [147075.069813436, 139630.198056286, 140888.552826286,
+ 138809.684197415, 158309.246259353, 212183.594374882,
+ 70870.1889675217, 319225.965633536, 165337.758878256,
+ 92630.7446925393, 102128.654360872, 139196.085105372]
+ dist1 = City.objects.distance('point', lagrange)
+ dist2 = City.objects.distance(lagrange)
+
+ # Original query done on PostGIS, have to adjust AlmostEqual tolerance
+ # for Oracle.
+ if oracle: tol = 3
+ else: tol = 7
+
+ for qs in [dist1, dist2]:
+ for i, c in enumerate(qs):
+ self.assertAlmostEqual(distances[i], c.distance, tol)
+
+ def test04_distance_lookups(self):
+ "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
+ # The point we are testing distances with -- using a WGS84
+ # coordinate that'll be implicitly transormed to that to
+ # the coordinate system of the field, EPSG:32140 (Texas South Central
+ # w/units in meters)
+ pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
+
+ # Only two cities (Houston and Southside Place) should be
+ # within 7km of the given point.
+ qs1 = City.objects.filter(point__distance_lte=(pnt, D(km=7))) # Query w/Distance instance.
+ qs2 = City.objects.filter(point__distance_lte=(pnt, 7000)) # Query w/int (units are assumed to be that of the field)
+ qs3 = City.objects.filter(point__distance_lte=(pnt, 7000.0)) # Query w/float
+ qs4 = City.objects.filter(point__distance_lte=(pnt, Decimal(7000))) # Query w/Decimal
+
+ for qs in [qs1, qs2, qs3, qs4]:
+ for c in qs:
+ self.assertEqual(2, qs.count())
+ self.failIf(not c.name in ['Downtown Houston', 'Southside Place'])
+
+ # Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
+ # (thus, Houston and Southside place will be excluded)
+ qs = City.objects.filter(point__distance_gte=(pnt, D(km=7))).filter(point__distance_lte=(pnt, D(km=20)))
+ self.assertEqual(3, qs.count())
+ for c in qs:
+ self.failIf(not c.name in ['Pearland', 'Bellaire', 'West University Place'])
+
+def suite():
+ s = unittest.TestSuite()
+ s.addTest(unittest.makeSuite(DistanceTest))
+ return s
View
4 django/contrib/gis/tests/geoapp/models.py
@@ -8,18 +8,22 @@ class Country(models.Model):
name = models.CharField(max_length=30)
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
objects = models.GeoManager()
+ def __unicode__(self): return self.name
class City(models.Model):
name = models.CharField(max_length=30)
point = models.PointField()
objects = models.GeoManager()
+ def __unicode__(self): return self.name
class State(models.Model):
name = models.CharField(max_length=30)
poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here.
objects = models.GeoManager()
+ def __unicode__(self): return self.name
class Feature(models.Model):
name = models.CharField(max_length=20)
geom = models.GeometryField()
objects = models.GeoManager()
+ def __unicode__(self): return self.name
View
8 django/contrib/gis/tests/geoapp/sql/city.oracle.sql
@@ -0,0 +1,8 @@
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Houston', SDO_GEOMETRY('POINT (-95.363151 29.763374)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Dallas', SDO_GEOMETRY('POINT (-96.801611 32.782057)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Oklahoma City', SDO_GEOMETRY('POINT (-97.521157 34.464642)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Wellington', SDO_GEOMETRY('POINT (174.783117 -41.315268)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Pueblo', SDO_GEOMETRY('POINT (-104.609252 38.255001)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Lawrence', SDO_GEOMETRY('POINT (-95.235060 38.971823)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Chicago', SDO_GEOMETRY('POINT (-87.650175 41.850385)', 4326));
+INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Victoria', SDO_GEOMETRY('POINT (-123.305196 48.462611)', 4326));
View
135 django/contrib/gis/tests/geoapp/tests.py
@@ -2,12 +2,20 @@
from models import Country, City, State, Feature
from django.contrib.gis import gdal
from django.contrib.gis.geos import *
+from django.contrib.gis.measure import Distance
from django.contrib.gis.tests.utils import no_oracle, no_postgis, oracle, postgis
+# TODO: Some tests depend on the success/failure of previous tests, these should
+# be decoupled. This flag is an artifact of this problem, and makes debugging easier;
+# specifically, the DISABLE flag will disables all tests, allowing problem tests to
+# be examined individually.
+DISABLE = False
+
class GeoModelTest(unittest.TestCase):
def test01_initial_sql(self):
"Testing geographic initial SQL."
+ if DISABLE: return
if oracle:
# Oracle doesn't allow strings longer than 4000 characters
# in SQL files, and I'm stumped on how to use Oracle BFILE's
@@ -31,10 +39,15 @@ def get_file(wkt_file):
# Ensuring that data was loaded from initial SQL.
self.assertEqual(2, Country.objects.count())
self.assertEqual(8, City.objects.count())
- self.assertEqual(3, State.objects.count())
+
+ # Oracle cannot handle NULL geometry values w/certain queries.
+ if oracle: n_state = 2
+ else: n_state = 3
+ self.assertEqual(n_state, State.objects.count())
def test02_proxy(self):
"Testing Lazy-Geometry support (using the GeometryProxy)."
+ if DISABLE: return
#### Testing on a Point
pnt = Point(0, 0)
nullcity = City(name='NullCity', point=pnt)
@@ -104,32 +117,41 @@ def test02_proxy(self):
@no_oracle # Oracle does not support KML.
def test03a_kml(self):
"Testing KML output from the database using GeoManager.kml()."
+ if DISABLE: return
# Should throw a TypeError when trying to obtain KML from a
# non-geometry field.
qs = City.objects.all()
self.assertRaises(TypeError, qs.kml, 'name')
# Ensuring the KML is as expected.
- ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
- self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
+ ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
+ ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
+ for ptown in [ptown1, ptown2]:
+ self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
def test03b_gml(self):
"Testing GML output from the database using GeoManager.gml()."
+ if DISABLE: return
# Should throw a TypeError when tyring to obtain GML from a
# non-geometry field.
qs = City.objects.all()
self.assertRaises(TypeError, qs.gml, 'name')
- ptown = City.objects.gml('point', precision=9).get(name='Pueblo')
+ ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
+ ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
+
if oracle:
# No precision parameter for Oracle :-/
import re
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925199\d+,38.25500\d+ </gml:coordinates></gml:Point>')
- self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
+ for ptown in [ptown1, ptown2]:
+ self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
else:
- self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
+ for ptown in [ptown1, ptown2]:
+ self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
def test04_transform(self):
"Testing the transform() GeoManager method."
+ if DISABLE: return
# 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)
@@ -142,13 +164,31 @@ def test04_transform(self):
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.assertEqual(2774, p.point.srid)
- self.assertAlmostEqual(ptown.x, p.point.x, 7)
- self.assertAlmostEqual(ptown.y, p.point.y, 7)
+ p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
+ p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
+ for p in [p1, p2]:
+ self.assertEqual(2774, p.point.srid)
+ self.assertAlmostEqual(ptown.x, p.point.x, 7)
+ self.assertAlmostEqual(ptown.y, p.point.y, 7)
+
+ def test09_disjoint(self):
+ "Testing the `disjoint` lookup type."
+ if DISABLE: return
+ ptown = City.objects.get(name='Pueblo')
+ qs1 = City.objects.filter(point__disjoint=ptown.point)
+ self.assertEqual(7, qs1.count())
+
+ if not postgis:
+ # TODO: Do NULL columns bork queries on PostGIS? The following
+ # error is encountered:
+ # psycopg2.ProgrammingError: invalid memory alloc request size 4294957297
+ qs2 = State.objects.filter(poly__disjoint=ptown.point)
+ self.assertEqual(1, qs2.count())
+ self.assertEqual('Kansas', qs2[0].name)
def test10_contains_contained(self):
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
+ if DISABLE: return
# Getting Texas, yes we were a country -- once ;)
texas = Country.objects.get(name='Texas')
@@ -190,6 +230,7 @@ def test10_contains_contained(self):
def test11_lookup_insert_transform(self):
"Testing automatic transform for lookups and inserts."
+ if DISABLE: return
# San Antonio in 'WGS84' (SRID 4326)
sa_4326 = 'POINT (-98.493183 29.424170)'
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
@@ -225,8 +266,12 @@ def test11_lookup_insert_transform(self):
self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6)
self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6)
+ # Oracle does not support NULL geometries in its spatial index for
+ # some routines (e.g., SDO_GEOM.RELATE).
+ @no_oracle
def test12_null_geometries(self):
"Testing NULL geometry support, and the `isnull` lookup type."
+ if DISABLE: return
# Querying for both NULL and Non-NULL values.
nullqs = State.objects.filter(poly__isnull=True)
validqs = State.objects.filter(poly__isnull=False)
@@ -250,6 +295,7 @@ def test12_null_geometries(self):
@no_oracle # No specific `left` or `right` operators in Oracle.
def test13_left_right(self):
"Testing the 'left' and 'right' lookup types."
+ if DISABLE: return
# Left: A << B => true if xmax(A) < xmin(B)
# Right: A >> B => true if xmin(A) > xmax(B)
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
@@ -285,6 +331,7 @@ def test13_left_right(self):
for c in qs: self.assertEqual(True, c.name in cities)
def test14_equals(self):
+ if DISABLE: return
"Testing the 'same_as' and 'equals' lookup types."
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
c1 = City.objects.get(point=pnt)
@@ -292,57 +339,73 @@ def test14_equals(self):
c3 = City.objects.get(point__equals=pnt)
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
- @no_oracle # Oracle SDO_RELATE() uses a different system.
def test15_relate(self):
"Testing the 'relate' lookup type."
+ if DISABLE: return
# To make things more interesting, we will have our Texas reference point in
- # different SRIDs.
- pnt1 = fromstr('POINT (649287.0363174345111474 4177429.4494686722755432)', srid=2847)
+ # different SRIDs.
+ pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', 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()
+ # Testing bad argument tuples that should return a TypeError or
+ # a ValueError.
+ bad_args = [((pnt1, 0), TypeError),
+ ((pnt2, 'T*T***FF*', 0), ValueError),
+ ((23, 'foo'), TypeError),
+ ]
+ for args, e in bad_args:
+ qs = Country.objects.filter(mpoly__relate=args)
+ self.assertRaises(e, qs.count)
+
+ # Relate works differently for the different backends.
+ if postgis:
+ contains_mask = 'T*T***FF*'
+ within_mask = 'T*F**F***'
+ intersects_mask = 'T********'
+ elif oracle:
+ contains_mask = 'contains'
+ within_mask = 'inside'
+ # TODO: This is not quite the same as the PostGIS mask above
+ intersects_mask = 'overlapbdyintersect'
+
+ # Testing contains relation mask.
+ self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, contains_mask)).name)
+ self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, contains_mask)).name)
+
+ # Testing within relation mask.
ks = State.objects.get(name='Kansas')
- self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T*F**F***')).name)
+ self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).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)
+ # Testing intersection relation mask.
+ if not oracle:
+ self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name)
+ self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name)
+ self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name)
def test16_createnull(self):
"Testing creating a model instance and the geometry being None"
+ if DISABLE: return
c = City()
self.assertEqual(c.point, None)
def test17_union(self):
"Testing the union() GeoManager method."
+ if DISABLE: return
tx = Country.objects.get(name='Texas').mpoly
# Houston, Dallas, San Antonio
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
qs = City.objects.filter(point__within=tx)
self.assertRaises(TypeError, qs.union, 'name')
- u = qs.union('point')
- self.assertEqual(True, union.equals_exact(u, 10)) # Going up to 10 digits of precision.
+ u1 = qs.union('point')
+ u2 = qs.union()
+ self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
+ self.assertEqual(True, union.equals_exact(u2, 10))
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.union('point'))
def test18_geometryfield(self):
"Testing GeometryField."
+ if DISABLE: return
f1 = Feature(name='Point', geom=Point(1, 1))
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
Please sign in to comment.
Something went wrong with that request. Please try again.