summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Bronn <jbronn@gmail.com>2008-02-11 02:25:01 +0000
committerJustin Bronn <jbronn@gmail.com>2008-02-11 02:25:01 +0000
commit21a019681c4ef2cfe486beb5d43ba068b3247d24 (patch)
treee4a37c1584eda4e977d7c4eabc3be0231a2f9910
parentd06e33a54d93c35584c348b286b6fea4ffdd3868 (diff)
gis: Fixed 6414, and applied DRY to spatial backend internals. Changes include:
(1) Support for distance calculations on geometry fields with geodetic coordinate systems (e.g., WGS84, the default). (2) The `get_db_prep_save` and `get_db_prep_lookup` have been moved from the spatial backends to common implementations in `GeometryField`. (3) Simplified SQL construction for `GeoQuerySet` methods. (4) `SpatialBackend` now contains all spatial backend dependent settings. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7104 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/contrib/gis/db/backend/__init__.py98
-rw-r--r--django/contrib/gis/db/backend/mysql/__init__.py11
-rw-r--r--django/contrib/gis/db/backend/mysql/adaptor.py10
-rw-r--r--django/contrib/gis/db/backend/mysql/field.py49
-rw-r--r--django/contrib/gis/db/backend/mysql/query.py13
-rw-r--r--django/contrib/gis/db/backend/oracle/__init__.py14
-rw-r--r--django/contrib/gis/db/backend/oracle/adaptor.py10
-rw-r--r--django/contrib/gis/db/backend/oracle/field.py65
-rw-r--r--django/contrib/gis/db/backend/oracle/query.py25
-rw-r--r--django/contrib/gis/db/backend/postgis/__init__.py9
-rw-r--r--django/contrib/gis/db/backend/postgis/field.py64
-rw-r--r--django/contrib/gis/db/backend/postgis/query.py51
-rw-r--r--django/contrib/gis/db/backend/util.py29
-rw-r--r--django/contrib/gis/db/models/fields/__init__.py102
-rw-r--r--django/contrib/gis/db/models/query.py140
-rw-r--r--django/contrib/gis/models.py32
-rw-r--r--django/contrib/gis/tests/distapp/cities/cities.dbfbin1037 -> 0 bytes
-rw-r--r--django/contrib/gis/tests/distapp/cities/cities.prj1
-rw-r--r--django/contrib/gis/tests/distapp/cities/cities.shpbin436 -> 0 bytes
-rw-r--r--django/contrib/gis/tests/distapp/cities/cities.shxbin196 -> 0 bytes
-rw-r--r--django/contrib/gis/tests/distapp/data.py25
-rw-r--r--django/contrib/gis/tests/distapp/models.py18
-rw-r--r--django/contrib/gis/tests/distapp/tests.py123
23 files changed, 461 insertions, 428 deletions
diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py
index fd838821da..e797942c0e 100644
--- a/django/contrib/gis/db/backend/__init__.py
+++ b/django/contrib/gis/db/backend/__init__.py
@@ -9,66 +9,101 @@
the backend.
(3) The `parse_lookup` function, used for spatial SQL construction by
the GeoQuerySet.
- (4) The `create_spatial_db`, and `get_geo_where_clause`
- routines (needed by `parse_lookup`).
+ (4) The `create_spatial_db`, and `get_geo_where_clause`
+ (needed by `parse_lookup`) functions.
(5) The `SpatialBackend` object, which contains information specific
to the spatial backend.
"""
-from types import StringType, UnicodeType
from django.conf import settings
from django.db import connection
from django.db.models.query import field_choices, find_field, get_where_clause, \
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
from django.utils.datastructures import SortedDict
-from django.contrib.gis.geos import GEOSGeometry
+from django.contrib.gis.db.backend.util import gqn
# These routines (needed by GeoManager), default to False.
-ASGML, ASKML, DISTANCE, EXTENT, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False, False)
+ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
+# Lookup types in which the rest of the parameters are not
+# needed to be substitute in the WHERE SQL (e.g., the 'relate'
+# operation on Oracle does not need the mask substituted back
+# into the query SQL.).
+LIMITED_WHERE = []
+
+# Retrieving the necessary settings from the backend.
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, \
- ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
+ from django.contrib.gis.db.backend.postgis.adaptor import \
+ PostGISAdaptor as GeoAdaptor
+ from django.contrib.gis.db.backend.postgis.field import \
+ PostGISField as GeoBackendField
+ from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
+ from django.contrib.gis.db.backend.postgis.query import \
+ get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
+ ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
+ EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
+ # PostGIS version info is needed to determine calling order of some
+ # stored procedures (e.g., AsGML()).
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
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, \
- ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
+ from django.contrib.gis.db.backend.oracle.adaptor import \
+ OracleSpatialAdaptor as GeoAdaptor
+ from django.contrib.gis.db.backend.oracle.field import \
+ OracleSpatialField as GeoBackendField
+ from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
+ from django.contrib.gis.db.backend.oracle.query import \
+ get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
+ ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'oracle'
+ LIMITED_WHERE = ['relate']
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, \
- GEOM_SELECT
+ from django.contrib.gis.db.backend.mysql.adaptor import \
+ MySQLAdaptor as GeoAdaptor
+ from django.contrib.gis.db.backend.mysql.field import \
+ MySQLGeoField as GeoBackendField
+ from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
+ from django.contrib.gis.db.backend.mysql.query import \
+ get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
+ DISTANCE_FUNCTIONS = {}
SPATIAL_BACKEND = 'mysql'
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
class SpatialBackend(object):
- "A container for properties of the Spatial Backend."
+ "A container for properties of the SpatialBackend."
+ # Stored procedure names used by the `GeoManager`.
as_kml = ASKML
as_gml = ASGML
distance = DISTANCE
+ distance_spheroid = DISTANCE_SPHEROID
extent = EXTENT
name = SPATIAL_BACKEND
select = GEOM_SELECT
transform = TRANSFORM
union = UNION
+
+ # Version information, if defined.
version = VERSION
+
+ # All valid GIS lookup terms, and distance functions.
+ gis_terms = GIS_TERMS
+ distance_functions = DISTANCE_FUNCTIONS
+
+ # Lookup types where additional WHERE parameters are excluded.
+ limited_where = LIMITED_WHERE
+
+ # Class for the backend field.
+ Field = GeoBackendField
+
+ # Adaptor class used for quoting GEOS geometries in the database.
+ Adaptor = GeoAdaptor
#### 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.
#
-# Status: Synced with r5982.
+# Status: Synced with r7098.
#
def parse_lookup(kwarg_items, opts):
# Helper function that handles converting API kwargs
@@ -290,16 +325,17 @@ 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'):
- # Getting the preparation SQL object from the field.
- geo_prep = field.get_db_prep_lookup(lookup_type, value)
+ # Getting additional SQL WHERE and params arrays associated with
+ # the geographic field.
+ geo_where, geo_params = 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)
+ # Getting the geographic WHERE clause.
+ gwc = get_geo_where_clause(lookup_type, current_table, field, value)
- # 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)
+ # Appending the geographic WHERE componnents and parameters onto
+ # the where and params arrays.
+ where.append(gwc % tuple(geo_where))
+ params.extend(geo_params)
else:
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
params.extend(field.get_db_prep_lookup(lookup_type, value))
diff --git a/django/contrib/gis/db/backend/mysql/__init__.py b/django/contrib/gis/db/backend/mysql/__init__.py
index 3e6bee9082..8b13789179 100644
--- a/django/contrib/gis/db/backend/mysql/__init__.py
+++ b/django/contrib/gis/db/backend/mysql/__init__.py
@@ -1,12 +1 @@
-"""
- The MySQL spatial database backend module.
- Please note that MySQL only supports bounding box queries, also
- known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
- indices may only be used on MyISAM tables -- if you need
- transactions, take a look at PostGIS.
-"""
-
-from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
-from django.contrib.gis.db.backend.mysql.field import MySQLGeoField, gqn
-from django.contrib.gis.db.backend.mysql.query import get_geo_where_clause, MYSQL_GIS_TERMS, GEOM_SELECT
diff --git a/django/contrib/gis/db/backend/mysql/adaptor.py b/django/contrib/gis/db/backend/mysql/adaptor.py
new file mode 100644
index 0000000000..9a702d3fdf
--- /dev/null
+++ b/django/contrib/gis/db/backend/mysql/adaptor.py
@@ -0,0 +1,10 @@
+"""
+ This object provides quoting for GEOS geometries into MySQL.
+"""
+class MySQLAdaptor(object):
+ def __init__(self, geom):
+ self.wkt = geom.wkt
+
+ def __str__(self):
+ "WKT is used as for the substitution value for the geometry."
+ return self.wkt
diff --git a/django/contrib/gis/db/backend/mysql/field.py b/django/contrib/gis/db/backend/mysql/field.py
index c53ada1f1b..f3151f93ff 100644
--- a/django/contrib/gis/db/backend/mysql/field.py
+++ b/django/contrib/gis/db/backend/mysql/field.py
@@ -1,16 +1,9 @@
-import re
-from types import StringType, UnicodeType
from django.db import connection
from django.db.models.fields import Field # Django base Field class
-from django.contrib.gis.geos import GEOSGeometry
-from django.contrib.gis.db.backend.util import GeoFieldSQL
-from django.contrib.gis.db.backend.mysql.query import MYSQL_GIS_TERMS, GEOM_FROM_TEXT
+from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
-def gqn(value):
- if isinstance(value, UnicodeType): value = value.encode('ascii')
- return "'%s'" % value
class MySQLGeoField(Field):
"""
@@ -23,7 +16,7 @@ class MySQLGeoField(Field):
used an R-Tree index is created, otherwise a B-Tree index is created.
Thus, for best spatial performance, you should use MyISAM tables
(which do not support transactions). For more information, see Ch.
- 17.6.1 of the MySQL 5.0 documentation.
+ 16.6.1 of the MySQL 5.0 documentation.
"""
# Getting the index name.
@@ -50,43 +43,11 @@ class MySQLGeoField(Field):
def db_type(self):
"The OpenGIS name is returned for the MySQL database column type."
return self._geom
-
- 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.
- """
- if lookup_type in MYSQL_GIS_TERMS:
- # 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))
-
- return GeoFieldSQL(['%s(%%s)' % GEOM_FROM_TEXT], [value])
-
- 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 value
- else:
- raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
def get_placeholder(self, value):
"""
- Nothing special happens here because MySQL does not support transformations.
+ The placeholder here has to include MySQL's WKT constructor. Because
+ MySQL does not support spatial transformations, there is no need to
+ modify the placeholder based on the contents of the given value.
"""
return '%s(%%s)' % GEOM_FROM_TEXT
diff --git a/django/contrib/gis/db/backend/mysql/query.py b/django/contrib/gis/db/backend/mysql/query.py
index d7bac0f84f..4a4cca45af 100644
--- a/django/contrib/gis/db/backend/mysql/query.py
+++ b/django/contrib/gis/db/backend/mysql/query.py
@@ -1,6 +1,11 @@
"""
- This module contains the spatial lookup types, and the get_geo_where_clause()
- routine for MySQL
+ This module contains the spatial lookup types, and the `get_geo_where_clause`
+ routine for MySQL.
+
+ Please note that MySQL only supports bounding box queries, also
+ known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
+ indices may only be used on MyISAM tables -- if you need
+ transactions, take a look at PostGIS.
"""
from django.db import connection
qn = connection.ops.quote_name
@@ -34,10 +39,10 @@ MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
MYSQL_GIS_TERMS += MISC_TERMS
MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
-def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
+def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
# Getting the quoted field as `geo_col`.
- geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
+ geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
# See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
diff --git a/django/contrib/gis/db/backend/oracle/__init__.py b/django/contrib/gis/db/backend/oracle/__init__.py
index 7384b56027..e69de29bb2 100644
--- a/django/contrib/gis/db/backend/oracle/__init__.py
+++ b/django/contrib/gis/db/backend/oracle/__init__.py
@@ -1,14 +0,0 @@
-"""
- The Oracle spatial database backend module.
-
- Please note that WKT support is broken on the XE version, and thus
- this backend will not work on such platforms. Specifically, XE lacks
- support for an internal JVM, and Java libraries are required to use
- the WKT constructors.
-"""
-from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
-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, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
-
diff --git a/django/contrib/gis/db/backend/oracle/adaptor.py b/django/contrib/gis/db/backend/oracle/adaptor.py
index e23ecb5c5c..7b1caa2ad9 100644
--- a/django/contrib/gis/db/backend/oracle/adaptor.py
+++ b/django/contrib/gis/db/backend/oracle/adaptor.py
@@ -1,8 +1,6 @@
"""
This object provides the database adaptor for Oracle geometries.
"""
-from cx_Oracle import CLOB
-
class OracleSpatialAdaptor(object):
def __init__(self, geom):
"Initializes only on the geometry object."
@@ -11,11 +9,3 @@ class OracleSpatialAdaptor(object):
def __str__(self):
"WKT is used for the substitution value of the geometry."
return self.wkt
-
- def oracle_type(self):
- """
- The parameter type is a CLOB because no string (VARCHAR2) greater
- than 4000 characters will be accepted through the Oracle database
- API and/or SQL*Plus.
- """
- return CLOB
diff --git a/django/contrib/gis/db/backend/oracle/field.py b/django/contrib/gis/db/backend/oracle/field.py
index a7d8e68778..829d5a5b02 100644
--- a/django/contrib/gis/db/backend/oracle/field.py
+++ b/django/contrib/gis/db/backend/oracle/field.py
@@ -1,18 +1,11 @@
-import re
-from types import StringType, UnicodeType
from django.db import connection
from django.db.backends.util import truncate_name
from django.db.models.fields import Field # Django base Field class
-from django.contrib.gis.geos import GEOSGeometry
-from django.contrib.gis.db.backend.util import GeoFieldSQL
-from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
-from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
+from django.contrib.gis.db.backend.util import gqn
+from django.contrib.gis.db.backend.oracle.query import TRANSFORM
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
-def gqn(value):
- if isinstance(value, UnicodeType): value = value.encode('ascii')
- return "'%s'" % value
class OracleSpatialField(Field):
"""
@@ -95,64 +88,16 @@ class OracleSpatialField(Field):
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
return 'MDSYS.SDO_GEOMETRY'
- 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.
- """
- if lookup_type in ORACLE_SPATIAL_TERMS:
- # special case for isnull lookup
- if lookup_type == 'isnull': return GeoFieldSQL([], [])
-
- # 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(geom)
-
- if geom.srid != self._srid:
- # Adding the necessary string substitutions and parameters
- # to perform a geometry transformation.
- where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
- params = [adapt, self._srid]
- else:
- 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)
-
- def get_db_prep_save(self, value):
- "Prepares the value for saving in the database."
- if not bool(value):
- # Return an empty string for NULL -- but this doesn't work yet.
- return ''
- if isinstance(value, GEOSGeometry):
- return OracleSpatialAdaptor(value)
- else:
- raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
-
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
SDO_CS.TRANSFORM() function call.
"""
- if isinstance(value, GEOSGeometry) and value.srid != self._srid:
+ if value is None:
+ return '%s'
+ elif value.srid != self._srid:
# Adding Transform() to the SQL placeholder.
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
- elif value is None:
- return '%s'
else:
return 'SDO_GEOMETRY(%%s, %s)' % self._srid
diff --git a/django/contrib/gis/db/backend/oracle/query.py b/django/contrib/gis/db/backend/oracle/query.py
index 7303eacf2d..f1841e0fc0 100644
--- a/django/contrib/gis/db/backend/oracle/query.py
+++ b/django/contrib/gis/db/backend/oracle/query.py
@@ -1,6 +1,11 @@
"""
- This module contains the spatial lookup types, and the get_geo_where_clause()
+ This module contains the spatial lookup types, and the `get_geo_where_clause`
routine for Oracle Spatial.
+
+ Please note that WKT support is broken on the XE version, and thus
+ this backend will not work on such platforms. Specifically, XE lacks
+ support for an internal JVM, and Java libraries are required to use
+ the WKT constructors.
"""
import re
from decimal import Decimal
@@ -25,8 +30,11 @@ GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
#### Classes used in constructing Oracle spatial SQL ####
class SDOOperation(SpatialFunction):
"Base class for SDO* Oracle operations."
- def __init__(self, func, end_subst=") %s '%s'"):
- super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE')
+ def __init__(self, func, **kwargs):
+ kwargs.setdefault('operator', '=')
+ kwargs.setdefault('result', 'TRUE')
+ kwargs.setdefault('end_subst', ") %s '%s'")
+ super(SDOOperation, self).__init__(func, **kwargs)
class SDODistance(SpatialFunction):
"Class for Distance queries."
@@ -55,12 +63,14 @@ class SDORelate(SpatialFunction):
#### Lookup type mapping dictionaries of Oracle spatial operations ####
# Valid distance types and substitutions
-dtypes = (Decimal, Distance, float, int)
+dtypes = (Decimal, Distance, float, int, long)
DISTANCE_FUNCTIONS = {
'distance_gt' : (SDODistance('>'), dtypes),
'distance_gte' : (SDODistance('>='), dtypes),
'distance_lt' : (SDODistance('<'), dtypes),
'distance_lte' : (SDODistance('<='), dtypes),
+ 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
+ beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
}
ORACLE_GEOMETRY_FUNCTIONS = {
@@ -68,7 +78,6 @@ ORACLE_GEOMETRY_FUNCTIONS = {
'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'),
- 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', end_subst=", %%s, 'distance=%%s') %s '%s'"), dtypes),
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'),
'exact' : SDOOperation('SDO_EQUAL'),
@@ -89,10 +98,10 @@ ORACLE_SPATIAL_TERMS += MISC_TERMS
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
#### The `get_geo_where_clause` function for Oracle ####
-def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
+def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
# Getting the quoted table name as `geo_col`.
- geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
+ geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
# See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
@@ -122,7 +131,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
return sdo_op.as_sql(geo_col)
else:
- # Lookup info is a SDOOperation instance, whos `as_sql` method returns
+ # Lookup info is a SDOOperation instance, whose `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 lookup_info.as_sql(geo_col)
diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py
index 334b1197c4..e69de29bb2 100644
--- a/django/contrib/gis/db/backend/postgis/__init__.py
+++ b/django/contrib/gis/db/backend/postgis/__init__.py
@@ -1,9 +0,0 @@
-"""
- The PostGIS spatial database backend module.
-"""
-from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
-from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn
-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, DISTANCE, EXTENT, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py
index cb7efd5eaa..9e20a72eee 100644
--- a/django/contrib/gis/db/backend/postgis/field.py
+++ b/django/contrib/gis/db/backend/postgis/field.py
@@ -1,20 +1,11 @@
-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
-from django.contrib.gis.db.backend.util import GeoFieldSQL
-from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
-from django.contrib.gis.db.backend.postgis.query import \
- DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
+from django.contrib.gis.db.backend.util import gqn
+from django.contrib.gis.db.backend.postgis.query import TRANSFORM
# Quotename & geographic quotename, respectively
qn = connection.ops.quote_name
-def gqn(value):
- if isinstance(value, basestring):
- if isinstance(value, UnicodeType): value = value.encode('ascii')
- return "'%s'" % value
- else:
- return str(value)
class PostGISField(Field):
"""
@@ -92,59 +83,14 @@ class PostGISField(Field):
"""
return None
- 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.
- """
- if lookup_type in POSTGIS_TERMS:
- # special case for isnull lookup
- if lookup_type == 'isnull': return GeoFieldSQL([], [])
-
- # 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(geom)
-
- if geom.srid != self._srid:
- # Adding the necessary string substitutions and parameters
- # to perform a geometry transformation.
- where = ['%s(%%s,%%s)' % TRANSFORM]
- params = [adapt, self._srid]
- else:
- # 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)
- else:
- raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
-
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call.
"""
- if isinstance(value, GEOSGeometry) and value.srid != self._srid:
+ if value is None or value.srid == self._srid:
+ return '%s'
+ else:
# Adding Transform() to the SQL placeholder.
return '%s(%%s, %s)' % (TRANSFORM, self._srid)
- else:
- return '%s'
diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py
index f920a8f7bc..1b8c3fa029 100644
--- a/django/contrib/gis/db/backend/postgis/query.py
+++ b/django/contrib/gis/db/backend/postgis/query.py
@@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1:
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
DISTANCE = get_func('Distance')
+ DISTANCE_SPHEROID = get_func('distance_spheroid')
EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
@@ -74,8 +75,20 @@ class PostGISFunctionParam(PostGISFunction):
class PostGISDistance(PostGISFunction):
"For PostGIS distance operations."
+ dist_func = 'Distance'
def __init__(self, operator):
- super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s')
+ super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
+ operator=operator, result='%%s')
+
+class PostGISSphereDistance(PostGISFunction):
+ "For PostGIS spherical distance operations."
+ dist_func = 'distance_spheroid'
+ def __init__(self, operator):
+ # An extra parameter in `end_subst` is needed for the spheroid string.
+ super(PostGISSphereDistance, self).__init__(self.dist_func,
+ beg_subst='%s(%s, %%s, %%s',
+ end_subst=') %s %s',
+ operator=operator, result='%%s')
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls."
@@ -148,21 +161,24 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
}
# Valid distance types and substitutions
-dtypes = (Decimal, Distance, float, int)
+dtypes = (Decimal, Distance, float, int, long)
+def get_dist_ops(operator):
+ "Returns operations for both regular and spherical distances."
+ return (PostGISDistance(operator), PostGISSphereDistance(operator))
DISTANCE_FUNCTIONS = {
- 'distance_gt' : (PostGISDistance('>'), dtypes),
- 'distance_gte' : (PostGISDistance('>='), dtypes),
- 'distance_lt' : (PostGISDistance('<'), dtypes),
- 'distance_lte' : (PostGISDistance('<='), dtypes),
+ 'distance_gt' : (get_dist_ops('>'), dtypes),
+ 'distance_gte' : (get_dist_ops('>='), dtypes),
+ 'distance_lt' : (get_dist_ops('<'), dtypes),
+ 'distance_lte' : (get_dist_ops('<='), dtypes),
}
if GEOM_FUNC_PREFIX == 'ST_':
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
- {'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
- 'coveredby' : PostGISFunction('CoveredBy'),
+ {'coveredby' : PostGISFunction('CoveredBy'),
'covers' : PostGISFunction('Covers'),
})
+ DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
# Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
@@ -178,10 +194,10 @@ POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnul
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
#### The `get_geo_where_clause` function for PostGIS. ####
-def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
+def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
# Getting the quoted field as `geo_col`.
- geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
+ geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
if lookup_type in POSTGIS_OPERATORS:
# See if a PostGIS operator matches the lookup type.
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
@@ -198,7 +214,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
- if not isinstance(value, tuple):
+ if not isinstance(value, (tuple, list)):
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)
@@ -209,7 +225,18 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
- if lookup_type == 'relate': op = op(value[1])
+ if lookup_type == 'relate':
+ op = op(value[1])
+ elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
+ if field._unit_name == 'degree':
+ # Geodetic distances are only availble from Points to PointFields.
+ if field._geom != 'POINT':
+ raise TypeError('PostGIS spherical operations are only valid on PointFields.')
+ if value[0].geom_typeid != 0:
+ raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
+ op = op[1]
+ else:
+ op = op[0]
else:
op = tmp
# Calling the `as_sql` function on the operation instance.
diff --git a/django/contrib/gis/db/backend/util.py b/django/contrib/gis/db/backend/util.py
index 384fe4de02..a19dd975c1 100644
--- a/django/contrib/gis/db/backend/util.py
+++ b/django/contrib/gis/db/backend/util.py
@@ -1,23 +1,16 @@
-class GeoFieldSQL(object):
+from types import UnicodeType
+
+def gqn(val):
"""
- Container for passing values to `parse_lookup` from the various
- backend geometry fields.
+ The geographic quote name function; used for quoting tables and
+ geometries (they use single rather than the double quotes of the
+ backend quotename function).
"""
- def __init__(self, where=[], params=[]):
- self.where = where
- self.params = params
-
- def __str__(self):
- return self.as_sql()
-
- def as_sql(self, quote=False):
- if not quote:
- return self.where[0] % tuple(self.params)
- else:
- # Used for quoting WKT on certain backends.
- tmp_params = ["'%s'" % self.params[0]]
- tmp_params.extend(self.params[1:])
- return self.where[0] % tuple(tmp_params)
+ if isinstance(val, basestring):
+ if isinstance(val, UnicodeType): val = val.encode('ascii')
+ return "'%s'" % val
+ else:
+ return str(val)
class SpatialOperation(object):
"""
diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py
index a4eeb4f39a..50cccb1715 100644
--- a/django/contrib/gis/db/models/fields/__init__.py
+++ b/django/contrib/gis/db/models/fields/__init__.py
@@ -1,7 +1,9 @@
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.
+# Getting the SpatialBackend container and the geographic quoting method.
+from django.contrib.gis.db.backend import SpatialBackend, gqn
+# GeometryProxy, GEOS, Distance, and oldforms imports.
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.measure import Distance
@@ -14,7 +16,7 @@ except NotImplementedError:
SpatialRefSys = None
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
-class GeometryField(GeoBackendField):
+class GeometryField(SpatialBackend.Field):
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name.
@@ -38,21 +40,19 @@ class GeometryField(GeoBackendField):
The number of dimensions for this geometry. Defaults to 2.
"""
- # 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 index flag with the value of the `spatial_index` keyword.
+ self._index = spatial_index
- # Setting the SRID and getting the units.
+ # Setting the SRID and getting the units. Unit information must be
+ # easily available in the field instance for distance queries.
self._srid = srid
if SpatialRefSys:
- # This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
+ # Getting the spatial reference WKT associated with the SRID from the
+ # `spatial_ref_sys` (or equivalent) spatial database table.
+ #
+ # The following doesn't work: 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.
+ # an ORM query instantiated within a model field.
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)'
@@ -62,27 +62,48 @@ class GeometryField(GeoBackendField):
'srid' : srid,
}
cur.execute(stmt)
- row = cur.fetchone()
- self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
+ srs_wkt = cur.fetchone()[0]
+ # Getting metadata associated with the spatial reference system identifier.
+ # Specifically, getting the unit information and spheroid information
+ # (both required for distance queries).
+ self._unit, self._unit_name = SpatialRefSys.get_units(srs_wkt)
+ self._spheroid = SpatialRefSys.get_spheroid(srs_wkt)
+
# 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):
+ """
+ Returns a distance number in units of the field. For example, if
+ `D(km=1)` was passed in and the units of the field were in meters,
+ then 1000 would be returned.
+ """
if isinstance(dist, Distance):
- return getattr(dist, Distance.unit_attname(self._unit_name))
- elif isinstance(dist, (int, float, Decimal)):
+ if self._unit_name in ('Decimal Degree', 'degree'):
+ # Spherical distance calculation parameter should be in meters.
+ dist_param = dist.m
+ else:
+ dist_param = getattr(dist, Distance.unit_attname(self._unit_name))
+ else:
# Assuming the distance is in the units of the field.
- return dist
+ dist_param = dist
+
+ # Sphereical distance query; returning meters.
+ if SpatialBackend.name == 'postgis' and self._unit_name == 'degree':
+ return [gqn(self._spheroid), dist_param]
+ else:
+ return [dist_param]
def get_geometry(self, value):
"""
Retrieves the geometry, setting the default SRID from the given
lookup parameters.
"""
- if isinstance(value, tuple):
+ if isinstance(value, (tuple, list)):
geom = value[0]
else:
geom = value
@@ -121,6 +142,49 @@ class GeometryField(GeoBackendField):
# Setup for lazy-instantiated GEOSGeometry object.
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
+ def get_db_prep_lookup(self, lookup_type, value):
+ """
+ Returns the spatial WHERE clause and associated parameters for the
+ given lookup type and value. The value will be prepared for database
+ lookup (e.g., spatial transformation SQL will be added if necessary).
+ """
+ if lookup_type in SpatialBackend.gis_terms:
+ # special case for isnull lookup
+ if lookup_type == 'isnull': return [], []
+
+ # Get the geometry with SRID; defaults SRID to that of the field
+ # if it is None.
+ geom = self.get_geometry(value)
+
+ # Getting the WHERE clause list and the associated params list. The params
+ # list is populated with the Adaptor wrapping the GEOSGeometry for the
+ # backend. The WHERE clause list contains the placeholder for the adaptor
+ # (e.g. any transformation SQL).
+ where = [self.get_placeholder(geom)]
+ params = [SpatialBackend.Adaptor(geom)]
+
+ if isinstance(value, (tuple, list)):
+ if lookup_type in SpatialBackend.distance_functions:
+ # Getting the distance parameter in the units of the field.
+ where += self.get_distance(value[1])
+ elif lookup_type in SpatialBackend.limited_where:
+ pass
+ else:
+ # Otherwise, making sure any other parameters are properly quoted.
+ where += map(gqn, value[1:])
+ return 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 isinstance(value, GEOSGeometry):
+ return SpatialBackend.Adaptor(value)
+ elif value is None:
+ return None
+ else:
+ raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
+
def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator."
return [WKTField]
diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py
index 3e909c1c3b..c9fa8decf3 100644
--- a/django/contrib/gis/db/models/query.py
+++ b/django/contrib/gis/db/models/query.py
@@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist
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, SpatialBackend
+from django.contrib.gis.db.backend import gqn, parse_lookup, SpatialBackend
from django.contrib.gis.geos import GEOSGeometry
# Shortcut booleans for determining the backend.
@@ -233,25 +233,20 @@ class GeoQuerySet(QuerySet):
return "%s.%s" % (qn(self.model._meta.db_table),
qn(field.column))
- def _geo_column(self, field_name):
+ def _geo_field(self, field_name=None):
"""
- Helper function that returns False when the given field name is not an
- instance of a GeographicField, otherwise, the database column for the
- geographic field is returned.
+ Returns the first Geometry field encountered; or specified via the
+ `field_name` keyword.
"""
- field = self.model._meta.get_field(field_name)
- if isinstance(field, GeometryField):
- return self._field_column(field)
- else:
- return False
-
- 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.')
-
+ fname = field.name
+ if field_name:
+ if field_name == field.name: return field
+ else:
+ return field
+ raise False
+
def distance(self, *args, **kwargs):
"""
Returns the distance from the given geographic field name to the
@@ -266,7 +261,7 @@ class GeoQuerySet(QuerySet):
# calculations from.
nargs = len(args)
if nargs == 1:
- field_name = self._get_geofield()
+ field_name = None
geom = args[0]
elif nargs == 2:
field_name, geom = args
@@ -274,29 +269,33 @@ class GeoQuerySet(QuerySet):
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
# Getting the quoted column.
- field_col = self._geo_column(field_name)
- if not field_col:
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('Distance output only available on GeometryFields.')
+ geo_col = self._field_column(geo_field)
- # 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)
+ # Using the field's get_db_prep_lookup() to get any needed
+ # transformation and distance SQL -- we pass in a 'dummy'
+ # `distance_lte` lookup type.
+ where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
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)}
+ tmp_params = [gqn(str(params[0]))]
+ tmp_params.extend(params[1:])
+ dsql = where[0] % tuple(tmp_params)
+ dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, dsql, tolerance)}
else:
- dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
+ dsql = where[0] % tuple(params)
+ if len(where) == 3:
+ # Call to distance_spheroid() requires the spheroid as well.
+ dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, dsql, where[1])
+ else:
+ dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, dsql)
+ dist_select = {'distance' : dist_sql}
return self.extra(select=dist_select)
def extent(self, field_name=None):
@@ -308,12 +307,10 @@ class GeoQuerySet(QuerySet):
if not EXTENT:
raise ImproperlyConfigured('Extent stored procedure not available.')
- if not field_name:
- field_name = self._get_geofield()
-
- field_col = self._geo_column(field_name)
- if not field_col:
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('Extent information only available on GeometryFields.')
+ geo_col = self._field_column(geo_field)
# Getting the SQL for the query.
try:
@@ -322,7 +319,7 @@ class GeoQuerySet(QuerySet):
return None
# Constructing the query that will select the extent.
- extent_sql = ('SELECT %s(%s)' % (EXTENT, field_col)) + sql
+ extent_sql = ('SELECT %s(%s)' % (EXTENT, geo_col)) + sql
# Getting a cursor, executing the query, and extracting the returned
# value from the extent function.
@@ -353,23 +350,21 @@ class GeoQuerySet(QuerySet):
# 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:
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('GML output only available on GeometryFields.')
-
+ geo_col = self._field_column(geo_field)
+
if oracle:
- gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
+ gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
elif postgis:
# PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh.
major, minor1, minor2 = SpatialBackend.version
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
- gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)}
+ gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, geo_col, precision)}
else:
- gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
+ gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)
@@ -385,48 +380,49 @@ class GeoQuerySet(QuerySet):
raise ImproperlyConfigured('AsKML() stored procedure not available.')
# Getting the geographic field.
- if not field_name:
- field_name = self._get_geofield()
-
- field_col = self._geo_column(field_name)
- if not field_col:
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('KML output only available on GeometryFields.')
-
+ geo_col = self._field_column(geo_field)
+
# Adding the AsKML function call to SELECT part of the SQL.
- return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
+ return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
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).
"""
- # 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()
+ TRANSFORM = SpatialBackend.transform
+ if not TRANSFORM:
+ raise ImproperlyConfigured('Transform stored procedure not available.')
- field = self.model._meta.get_field(field_name)
- if not isinstance(field, GeometryField):
+ # `field_name` is first for backwards compatibility; but we want to
+ # be able to take integer srid as first parameter.
+ if isinstance(field_name, (int, long)):
+ srid = field_name
+ field_name = None
+
+ # Getting the geographic field.
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
# 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))
+ geo_col = self._custom_select.get(geo_field.column, self._field_column(geo_field))
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
- TRANSFORM = SpatialBackend.transform
if oracle:
- custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
+ custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_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
+ (TRANSFORM, geo_col, srid, connection.ops.quote_name(geo_field.column))
+ self._custom_select[geo_field.column] = custom_sel
return self._clone()
def union(self, field_name=None, tolerance=0.0005):
@@ -441,12 +437,10 @@ class GeoQuerySet(QuerySet):
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:
+ geo_field = self._geo_field(field_name)
+ if not geo_field:
raise TypeError('Aggregate Union only available on GeometryFields.')
+ geo_col = self._field_column(geo_field)
# Getting the SQL for the query.
try:
@@ -458,10 +452,10 @@ class GeoQuerySet(QuerySet):
# on the geographic field column.
if oracle:
union_sql = 'SELECT %s' % self._geo_fmt
- union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
+ union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
union_sql += sql
else:
- union_sql = ('SELECT %s(%s)' % (UNION, field_col)) + sql
+ union_sql = ('SELECT %s(%s)' % (UNION, geo_col)) + sql
# Getting a cursor, executing the query.
cursor = connection.cursor()
diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py
index 233943dcbc..fc0de5fa58 100644
--- a/django/contrib/gis/models.py
+++ b/django/contrib/gis/models.py
@@ -21,7 +21,7 @@ class SpatialRefSysMixin(object):
# 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
@@ -71,7 +71,7 @@ class SpatialRefSysMixin(object):
@property
def spheroid(self):
- "Returns the spheroid for this spatial reference."
+ "Returns the spheroid name for this spatial reference."
return self.srs['spheroid']
@property
@@ -170,6 +170,34 @@ class SpatialRefSysMixin(object):
m = cls.units_regex.match(wkt)
return m.group('unit'), m.group('unit_name')
+ @classmethod
+ def get_spheroid(cls, wkt, string=True):
+ """
+ Class method used by GeometryField on initialization to
+ retrieve the `SPHEROID[..]` parameters from the given WKT.
+ """
+ if HAS_GDAL:
+ srs = SpatialReference(wkt)
+ sphere_params = srs.ellipsoid
+ sphere_name = srs['spheroid']
+ else:
+ m = cls.spheroid_regex.match(wkt)
+ if m:
+ sphere_params = (float(m.group('major')), float(m.group('flattening')))
+ sphere_name = m.group('name')
+ else:
+ return None
+
+ if not string:
+ return sphere_name, sphere_params
+ else:
+ # `string` parameter used to place in format acceptable by PostGIS
+ if len(sphere_params) == 3:
+ radius, flattening = sphere_params[0], sphere_params[2]
+ else:
+ radius, flattening = sphere_params
+ return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
+
def __unicode__(self):
"""
Returns the string representation. If GDAL is installed,
diff --git a/django/contrib/gis/tests/distapp/cities/cities.dbf b/django/contrib/gis/tests/distapp/cities/cities.dbf
deleted file mode 100644
index 660def3214..0000000000
--- a/django/contrib/gis/tests/distapp/cities/cities.dbf
+++ /dev/null
Binary files differ
diff --git a/django/contrib/gis/tests/distapp/cities/cities.prj b/django/contrib/gis/tests/distapp/cities/cities.prj
deleted file mode 100644
index c591448279..0000000000
--- a/django/contrib/gis/tests/distapp/cities/cities.prj
+++ /dev/null
@@ -1 +0,0 @@
-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]] \ No newline at end of file
diff --git a/django/contrib/gis/tests/distapp/cities/cities.shp b/django/contrib/gis/tests/distapp/cities/cities.shp
deleted file mode 100644
index 2b0b42f7f1..0000000000
--- a/django/contrib/gis/tests/distapp/cities/cities.shp
+++ /dev/null
Binary files differ
diff --git a/django/contrib/gis/tests/distapp/cities/cities.shx b/django/contrib/gis/tests/distapp/cities/cities.shx
deleted file mode 100644
index 9189d9ee7c..0000000000
--- a/django/contrib/gis/tests/distapp/cities/cities.shx
+++ /dev/null
Binary files differ
diff --git a/django/contrib/gis/tests/distapp/data.py b/django/contrib/gis/tests/distapp/data.py
new file mode 100644
index 0000000000..94b28abc4c
--- /dev/null
+++ b/django/contrib/gis/tests/distapp/data.py
@@ -0,0 +1,25 @@
+au_cities = (('Wollongong', 150.902, -34.4245),
+ ('Shellharbour', 150.87, -34.5789),
+ ('Thirroul', 150.924, -34.3147),
+ ('Mittagong', 150.449, -34.4509),
+ ('Batemans Bay', 150.175, -35.7082),
+ ('Canberra', 144.963, -37.8143),
+ ('Melbourne', 145.963, -37.8143),
+ ('Sydney', 151.26071, -33.887034),
+ ('Hobart', 147.33, -42.8827),
+ ('Adelaide', 138.6, -34.9258),
+ )
+
+stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
+ ('West University Place', 943547.922328, 4213623.65345),
+ ('Southside Place', 944704.643307, 4212768.87617),
+ ('Bellaire', 942595.669129, 4212686.72583),
+ ('Pearland', 959674.616506, 4197465.6526),
+ ('Galveston', 1008151.16007, 4170027.47655),
+ ('Sealy', 874859.286808, 4219186.8641),
+ ('San Antonio', 649173.910483, 4176413.27786),
+ ('Round Rock', 726846.03695, 4297160.99715),
+ ('Saint Hedwig', 677644.649952, 4175467.06744),
+ )
+
+
diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py
index 161fec9d56..836b769e61 100644
--- a/django/contrib/gis/tests/distapp/models.py
+++ b/django/contrib/gis/tests/distapp/models.py
@@ -1,20 +1,20 @@
from django.contrib.gis.db import models
-class City(models.Model):
+class SouthTexasCity(models.Model):
+ "City model on projected coordinate system for South Texas."
name = models.CharField(max_length=30)
point = models.PointField(srid=32140)
objects = models.GeoManager()
def __unicode__(self): return self.name
+class AustraliaCity(models.Model):
+ "City model for Australia, using WGS84."
+ name = models.CharField(max_length=30)
+ point = models.PointField()
+ 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',
-# }
diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py
index f407e50c7a..187d30e52b 100644
--- a/django/contrib/gis/tests/distapp/tests.py
+++ b/django/contrib/gis/tests/distapp/tests.py
@@ -1,53 +1,68 @@
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.geos import GEOSGeometry, Point
from django.contrib.gis.measure import D # alias for Distance
+from django.contrib.gis.db.models import GeoQ
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')
+from models import SouthTexasCity, AustraliaCity
+from data import au_cities, stx_cities
class DistanceTest(unittest.TestCase):
+ # A 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)
+ stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
+
+ def get_cities(self, qs):
+ cities = [c.name for c in qs]
+ cities.sort()
+ return cities
+
def test01_init(self):
- "LayerMapping initialization of distance models."
+ "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()
+ def load_cities(city_model, srid, data_tup):
+ for name, x, y in data_tup:
+ c = city_model(name=name, point=Point(x, y, srid=srid))
+ c.save()
- self.assertEqual(12, City.objects.count())
- #self.assertEqual(60, County.objects.count())
+ load_cities(SouthTexasCity, 32140, stx_cities)
+ load_cities(AustraliaCity, 4326, au_cities)
- # TODO: Complete tests for `dwithin` lookups.
- #def test02_dwithin(self):
- # "Testing the `dwithin` lookup type."
- # pass
+ self.assertEqual(10, SouthTexasCity.objects.count())
+ self.assertEqual(10, AustraliaCity.objects.count())
+
+ def test02_dwithin(self):
+ "Testing the `dwithin` lookup type."
+ pnt = self.stx_pnt
+ dists = [7000, D(km=7), D(mi=4.349)]
+ for dist in dists:
+ qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
+ cities = self.get_cities(qs)
+ self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
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)
+ # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
+ distances = [147075.069813, 139630.198056, 140888.552826,
+ 138809.684197, 158309.246259, 212183.594374,
+ 70870.188967, 165337.758878, 102128.654360,
+ 139196.085105]
+ dist1 = SouthTexasCity.objects.distance('point', lagrange)
+ dist2 = SouthTexasCity.objects.distance(lagrange)
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle.
if oracle: tol = 3
- else: tol = 7
+ else: tol = 5
for qs in [dist1, dist2]:
for i, c in enumerate(qs):
@@ -55,30 +70,50 @@ class DistanceTest(unittest.TestCase):
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
+ dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
+ 7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
+ ]
- for qs in [qs1, qs2, qs3, qs4]:
+ for dist in dists:
+ qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
for c in qs:
- self.assertEqual(2, qs.count())
- self.failIf(not c.name in ['Downtown Houston', 'Southside Place'])
+ cities = self.get_cities(qs)
+ self.assertEqual(cities, ['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'])
+ qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
+ cities = self.get_cities(qs)
+ self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
+
+ def test05_geodetic_distance(self):
+ "Testing distance lookups on geodetic coordinate systems."
+
+ if not oracle:
+ # Oracle doesn't have this limitation -- PostGIS only allows geodetic
+ # distance queries from Points to PointFields.
+ mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
+ self.assertRaises(TypeError,
+ AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
+
+ hobart = AustraliaCity.objects.get(name='Hobart')
+
+ # Getting all cities w/in 550 miles of Hobart.
+ qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
+ cities = self.get_cities(qs)
+ self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
+
+ # Cities that are either really close or really far from Wollongong --
+ # and using different units of distance.
+ wollongong = AustraliaCity.objects.get(name='Wollongong')
+ gq1 = GeoQ(point__distance_lte=(wollongong.point, D(yd=19500))) # Yards (~17km)
+ gq2 = GeoQ(point__distance_gte=(wollongong.point, D(nm=400))) # Nautical Miles
+ qs = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
+ cities = self.get_cities(qs)
+ self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
+
def suite():
s = unittest.TestSuite()