1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

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
This commit is contained in:
Justin Bronn 2008-02-11 02:25:01 +00:00
parent d06e33a54d
commit 21a019681c
23 changed files with 462 additions and 429 deletions

View File

@ -10,65 +10,100 @@
(3) The `parse_lookup` function, used for spatial SQL construction by (3) The `parse_lookup` function, used for spatial SQL construction by
the GeoQuerySet. the GeoQuerySet.
(4) The `create_spatial_db`, and `get_geo_where_clause` (4) The `create_spatial_db`, and `get_geo_where_clause`
routines (needed by `parse_lookup`). (needed by `parse_lookup`) functions.
(5) The `SpatialBackend` object, which contains information specific (5) The `SpatialBackend` object, which contains information specific
to the spatial backend. to the spatial backend.
""" """
from types import StringType, UnicodeType
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.models.query import field_choices, find_field, get_where_clause, \ from django.db.models.query import field_choices, find_field, get_where_clause, \
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
from django.utils.datastructures import SortedDict 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. # 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': if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules, from django.contrib.gis.db.backend.postgis.adaptor import \
# renaming as necessary. PostGISAdaptor as GeoAdaptor
from django.contrib.gis.db.backend.postgis import \ from django.contrib.gis.db.backend.postgis.field import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ PostGISField as GeoBackendField
create_spatial_db, get_geo_where_clause, \ from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \ 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 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) VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
SPATIAL_BACKEND = 'postgis' SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle': elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \ from django.contrib.gis.db.backend.oracle.adaptor import \
OracleSpatialField as GeoBackendField, \ OracleSpatialAdaptor as GeoAdaptor
ORACLE_SPATIAL_TERMS as GIS_TERMS, \ from django.contrib.gis.db.backend.oracle.field import \
create_spatial_db, get_geo_where_clause, \ OracleSpatialField as GeoBackendField
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION 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' SPATIAL_BACKEND = 'oracle'
LIMITED_WHERE = ['relate']
elif settings.DATABASE_ENGINE == 'mysql': elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.mysql import \ from django.contrib.gis.db.backend.mysql.adaptor import \
MySQLGeoField as GeoBackendField, \ MySQLAdaptor as GeoAdaptor
MYSQL_GIS_TERMS as GIS_TERMS, \ from django.contrib.gis.db.backend.mysql.field import \
create_spatial_db, get_geo_where_clause, \ MySQLGeoField as GeoBackendField
GEOM_SELECT 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' SPATIAL_BACKEND = 'mysql'
else: else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
class SpatialBackend(object): class SpatialBackend(object):
"A container for properties of the SpatialBackend." "A container for properties of the SpatialBackend."
# Stored procedure names used by the `GeoManager`.
as_kml = ASKML as_kml = ASKML
as_gml = ASGML as_gml = ASGML
distance = DISTANCE distance = DISTANCE
distance_spheroid = DISTANCE_SPHEROID
extent = EXTENT extent = EXTENT
name = SPATIAL_BACKEND name = SPATIAL_BACKEND
select = GEOM_SELECT select = GEOM_SELECT
transform = TRANSFORM transform = TRANSFORM
union = UNION union = UNION
# Version information, if defined.
version = VERSION 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 #### #### query.py overloaded functions ####
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
# counterparts to support constructing SQL for geographic queries. # counterparts to support constructing SQL for geographic queries.
# #
# Status: Synced with r5982. # Status: Synced with r7098.
# #
def parse_lookup(kwarg_items, opts): def parse_lookup(kwarg_items, opts):
# Helper function that handles converting API kwargs # 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 # If the field is a geometry field, then the WHERE clause will need to be obtained
# with the get_geo_where_clause() # with the get_geo_where_clause()
if hasattr(field, '_geom'): if hasattr(field, '_geom'):
# Getting the preparation SQL object from the field. # Getting additional SQL WHERE and params arrays associated with
geo_prep = field.get_db_prep_lookup(lookup_type, value) # the geographic field.
geo_where, geo_params = field.get_db_prep_lookup(lookup_type, value)
# Getting the adapted geometry from the field. # Getting the geographic WHERE clause.
gwc = get_geo_where_clause(lookup_type, current_table, column, value) gwc = get_geo_where_clause(lookup_type, current_table, field, value)
# Substituting in the the where parameters into the geographic where # Appending the geographic WHERE componnents and parameters onto
# clause, and extending the parameters. # the where and params arrays.
where.append(gwc % tuple(geo_prep.where)) where.append(gwc % tuple(geo_where))
params.extend(geo_prep.params) params.extend(geo_params)
else: else:
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type)) where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
params.extend(field.get_db_prep_lookup(lookup_type, value)) params.extend(field.get_db_prep_lookup(lookup_type, value))

View File

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

View File

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

View File

@ -1,16 +1,9 @@
import re
from types import StringType, UnicodeType
from django.db import connection from django.db import connection
from django.db.models.fields import Field # Django base Field class from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
from django.contrib.gis.db.backend.util import GeoFieldSQL
from django.contrib.gis.db.backend.mysql.query import MYSQL_GIS_TERMS, GEOM_FROM_TEXT
# Quotename & geographic quotename, respectively. # Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name qn = connection.ops.quote_name
def gqn(value):
if isinstance(value, UnicodeType): value = value.encode('ascii')
return "'%s'" % value
class MySQLGeoField(Field): class MySQLGeoField(Field):
""" """
@ -23,7 +16,7 @@ class MySQLGeoField(Field):
used an R-Tree index is created, otherwise a B-Tree index is created. used an R-Tree index is created, otherwise a B-Tree index is created.
Thus, for best spatial performance, you should use MyISAM tables Thus, for best spatial performance, you should use MyISAM tables
(which do not support transactions). For more information, see Ch. (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. # Getting the index name.
@ -51,42 +44,10 @@ class MySQLGeoField(Field):
"The OpenGIS name is returned for the MySQL database column type." "The OpenGIS name is returned for the MySQL database column type."
return self._geom 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): 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 return '%s(%%s)' % GEOM_FROM_TEXT

View File

@ -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 MySQL 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 from django.db import connection
qn = connection.ops.quote_name qn = connection.ops.quote_name
@ -34,10 +39,10 @@ MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
MYSQL_GIS_TERMS += MISC_TERMS MYSQL_GIS_TERMS += MISC_TERMS
MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable 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." "Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
# Getting the quoted field as `geo_col`. # 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 # See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False) lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)

View File

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

View File

@ -1,8 +1,6 @@
""" """
This object provides the database adaptor for Oracle geometries. This object provides the database adaptor for Oracle geometries.
""" """
from cx_Oracle import CLOB
class OracleSpatialAdaptor(object): class OracleSpatialAdaptor(object):
def __init__(self, geom): def __init__(self, geom):
"Initializes only on the geometry object." "Initializes only on the geometry object."
@ -11,11 +9,3 @@ class OracleSpatialAdaptor(object):
def __str__(self): def __str__(self):
"WKT is used for the substitution value of the geometry." "WKT is used for the substitution value of the geometry."
return self.wkt 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

View File

@ -1,18 +1,11 @@
import re
from types import StringType, UnicodeType
from django.db import connection from django.db import connection
from django.db.backends.util import truncate_name from django.db.backends.util import truncate_name
from django.db.models.fields import Field # Django base Field class 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 gqn
from django.contrib.gis.db.backend.util import GeoFieldSQL from django.contrib.gis.db.backend.oracle.query import TRANSFORM
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
# Quotename & geographic quotename, respectively. # Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name qn = connection.ops.quote_name
def gqn(value):
if isinstance(value, UnicodeType): value = value.encode('ascii')
return "'%s'" % value
class OracleSpatialField(Field): class OracleSpatialField(Field):
""" """
@ -95,64 +88,16 @@ class OracleSpatialField(Field):
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY." "The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
return '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): def get_placeholder(self, value):
""" """
Provides a proper substitution value for Geometries that are not in the Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the SRID of the field. Specifically, this routine will substitute in the
SDO_CS.TRANSFORM() function call. 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. # Adding Transform() to the SQL placeholder.
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid) return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
elif value is None:
return '%s'
else: else:
return 'SDO_GEOMETRY(%%s, %s)' % self._srid return 'SDO_GEOMETRY(%%s, %s)' % self._srid

View File

@ -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. 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 import re
from decimal import Decimal from decimal import Decimal
@ -25,8 +30,11 @@ GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
#### Classes used in constructing Oracle spatial SQL #### #### Classes used in constructing Oracle spatial SQL ####
class SDOOperation(SpatialFunction): class SDOOperation(SpatialFunction):
"Base class for SDO* Oracle operations." "Base class for SDO* Oracle operations."
def __init__(self, func, end_subst=") %s '%s'"): def __init__(self, func, **kwargs):
super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE') kwargs.setdefault('operator', '=')
kwargs.setdefault('result', 'TRUE')
kwargs.setdefault('end_subst', ") %s '%s'")
super(SDOOperation, self).__init__(func, **kwargs)
class SDODistance(SpatialFunction): class SDODistance(SpatialFunction):
"Class for Distance queries." "Class for Distance queries."
@ -55,12 +63,14 @@ class SDORelate(SpatialFunction):
#### Lookup type mapping dictionaries of Oracle spatial operations #### #### Lookup type mapping dictionaries of Oracle spatial operations ####
# Valid distance types and substitutions # Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int) dtypes = (Decimal, Distance, float, int, long)
DISTANCE_FUNCTIONS = { DISTANCE_FUNCTIONS = {
'distance_gt' : (SDODistance('>'), dtypes), 'distance_gt' : (SDODistance('>'), dtypes),
'distance_gte' : (SDODistance('>='), dtypes), 'distance_gte' : (SDODistance('>='), dtypes),
'distance_lt' : (SDODistance('<'), dtypes), 'distance_lt' : (SDODistance('<'), dtypes),
'distance_lte' : (SDODistance('<='), dtypes), 'distance_lte' : (SDODistance('<='), dtypes),
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
} }
ORACLE_GEOMETRY_FUNCTIONS = { ORACLE_GEOMETRY_FUNCTIONS = {
@ -68,7 +78,6 @@ ORACLE_GEOMETRY_FUNCTIONS = {
'coveredby' : SDOOperation('SDO_COVEREDBY'), 'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'), 'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'), '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()? 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'), 'equals' : SDOOperation('SDO_EQUAL'),
'exact' : 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 ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
#### The `get_geo_where_clause` function for Oracle #### #### 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." "Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
# Getting the quoted table name as `geo_col`. # 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 # See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) 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. # Otherwise, just call the `as_sql` method on the SDOOperation instance.
return sdo_op.as_sql(geo_col) return sdo_op.as_sql(geo_col)
else: 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: # the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
return lookup_info.as_sql(geo_col) return lookup_info.as_sql(geo_col)

View File

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

View File

@ -1,20 +1,11 @@
from types import UnicodeType
from django.db import connection from django.db import connection
from django.db.models.fields import Field # Django base Field class from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import GeoFieldSQL from django.contrib.gis.db.backend.util import gqn
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor from django.contrib.gis.db.backend.postgis.query import TRANSFORM
from django.contrib.gis.db.backend.postgis.query import \
DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively # Quotename & geographic quotename, respectively
qn = connection.ops.quote_name 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): class PostGISField(Field):
""" """
@ -92,59 +83,14 @@ class PostGISField(Field):
""" """
return None 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): def get_placeholder(self, value):
""" """
Provides a proper substitution value for Geometries that are not in the Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call. 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. # Adding Transform() to the SQL placeholder.
return '%s(%%s, %s)' % (TRANSFORM, self._srid) return '%s(%%s, %s)' % (TRANSFORM, self._srid)
else:
return '%s'

View File

@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1:
ASKML = get_func('AsKML') ASKML = get_func('AsKML')
ASGML = get_func('AsGML') ASGML = get_func('AsGML')
DISTANCE = get_func('Distance') DISTANCE = get_func('Distance')
DISTANCE_SPHEROID = get_func('distance_spheroid')
EXTENT = get_func('extent') EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText') GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB') GEOM_FROM_WKB = get_func('GeomFromWKB')
@ -74,8 +75,20 @@ class PostGISFunctionParam(PostGISFunction):
class PostGISDistance(PostGISFunction): class PostGISDistance(PostGISFunction):
"For PostGIS distance operations." "For PostGIS distance operations."
dist_func = 'Distance'
def __init__(self, operator): 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): class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls." "For PostGIS Relate(<geom>, <pattern>) calls."
@ -148,21 +161,24 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
} }
# Valid distance types and substitutions # 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_FUNCTIONS = {
'distance_gt' : (PostGISDistance('>'), dtypes), 'distance_gt' : (get_dist_ops('>'), dtypes),
'distance_gte' : (PostGISDistance('>='), dtypes), 'distance_gte' : (get_dist_ops('>='), dtypes),
'distance_lt' : (PostGISDistance('<'), dtypes), 'distance_lt' : (get_dist_ops('<'), dtypes),
'distance_lte' : (PostGISDistance('<='), dtypes), 'distance_lte' : (get_dist_ops('<='), dtypes),
} }
if GEOM_FUNC_PREFIX == 'ST_': if GEOM_FUNC_PREFIX == 'ST_':
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update( POSTGIS_GEOMETRY_FUNCTIONS.update(
{'dwithin' : (PostGISFunctionParam('DWithin'), dtypes), {'coveredby' : PostGISFunction('CoveredBy'),
'coveredby' : PostGISFunction('CoveredBy'),
'covers' : PostGISFunction('Covers'), 'covers' : PostGISFunction('Covers'),
}) })
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
# Distance functions are a part of PostGIS geometry functions. # Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_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 POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
#### The `get_geo_where_clause` function for PostGIS. #### #### 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." "Returns the SQL WHERE clause for use in PostGIS SQL construction."
# Getting the quoted field as `geo_col`. # 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: if lookup_type in POSTGIS_OPERATORS:
# See if a PostGIS operator matches the lookup type. # See if a PostGIS operator matches the lookup type.
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) 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 op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user # 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) raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(value) != 2: if len(value) != 2:
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type) 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 # For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter). # 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: else:
op = tmp op = tmp
# Calling the `as_sql` function on the operation instance. # Calling the `as_sql` function on the operation instance.

View File

@ -1,23 +1,16 @@
class GeoFieldSQL(object): from types import UnicodeType
"""
Container for passing values to `parse_lookup` from the various
backend geometry fields.
"""
def __init__(self, where=[], params=[]):
self.where = where
self.params = params
def __str__(self): def gqn(val):
return self.as_sql() """
The geographic quote name function; used for quoting tables and
def as_sql(self, quote=False): geometries (they use single rather than the double quotes of the
if not quote: backend quotename function).
return self.where[0] % tuple(self.params) """
if isinstance(val, basestring):
if isinstance(val, UnicodeType): val = val.encode('ascii')
return "'%s'" % val
else: else:
# Used for quoting WKT on certain backends. return str(val)
tmp_params = ["'%s'" % self.params[0]]
tmp_params.extend(self.params[1:])
return self.where[0] % tuple(tmp_params)
class SpatialOperation(object): class SpatialOperation(object):
""" """

View File

@ -1,7 +1,9 @@
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.db import connection 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.db.models.proxy import GeometryProxy
from django.contrib.gis.geos import GEOSException, GEOSGeometry from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
@ -14,7 +16,7 @@ except NotImplementedError:
SpatialRefSys = None SpatialRefSys = None
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. #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 base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name. # The OpenGIS Geometry name.
@ -38,21 +40,19 @@ class GeometryField(GeoBackendField):
The number of dimensions for this geometry. Defaults to 2. The number of dimensions for this geometry. Defaults to 2.
""" """
# Backward-compatibility notice, this will disappear in future revisions. # Setting the index flag with the value of the `spatial_index` keyword.
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 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 self._srid = srid
if SpatialRefSys: 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 # Why? `syncdb` fails to recognize installed geographic models when there's
# an ORM query instantiated within a model field. No matter, this works fine # an ORM query instantiated within a model field.
# too.
cur = connection.cursor() cur = connection.cursor()
qn = connection.ops.quote_name qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)' 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, 'srid' : srid,
} }
cur.execute(stmt) cur.execute(stmt)
row = cur.fetchone() srs_wkt = cur.fetchone()[0]
self._unit, self._unit_name = SpatialRefSys.get_units(row[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. # Setting the dimension of the geometry field.
self._dim = dim self._dim = dim
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
### Routines specific to GeometryField ### ### Routines specific to GeometryField ###
def get_distance(self, dist): 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): if isinstance(dist, Distance):
return getattr(dist, Distance.unit_attname(self._unit_name)) if self._unit_name in ('Decimal Degree', 'degree'):
elif isinstance(dist, (int, float, Decimal)): # 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. # 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): def get_geometry(self, value):
""" """
Retrieves the geometry, setting the default SRID from the given Retrieves the geometry, setting the default SRID from the given
lookup parameters. lookup parameters.
""" """
if isinstance(value, tuple): if isinstance(value, (tuple, list)):
geom = value[0] geom = value[0]
else: else:
geom = value geom = value
@ -121,6 +142,49 @@ class GeometryField(GeoBackendField):
# Setup for lazy-instantiated GEOSGeometry object. # Setup for lazy-instantiated GEOSGeometry object.
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self)) 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): def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator." "Using the WKTField (defined above) to be our manipulator."
return [WKTField] return [WKTField]

View File

@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.fields import GeometryField
# parse_lookup depends on the spatial database backend. # 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 from django.contrib.gis.geos import GEOSGeometry
# Shortcut booleans for determining the backend. # Shortcut booleans for determining the backend.
@ -233,24 +233,19 @@ class GeoQuerySet(QuerySet):
return "%s.%s" % (qn(self.model._meta.db_table), return "%s.%s" % (qn(self.model._meta.db_table),
qn(field.column)) 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 Returns the first Geometry field encountered; or specified via the
instance of a GeographicField, otherwise, the database column for the `field_name` keyword.
geographic field is returned.
""" """
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: for field in self.model._meta.fields:
if isinstance(field, GeometryField): if isinstance(field, GeometryField):
return field.name fname = field.name
raise Exception('No GeometryFields in the model.') if field_name:
if field_name == field.name: return field
else:
return field
raise False
def distance(self, *args, **kwargs): def distance(self, *args, **kwargs):
""" """
@ -266,7 +261,7 @@ class GeoQuerySet(QuerySet):
# calculations from. # calculations from.
nargs = len(args) nargs = len(args)
if nargs == 1: if nargs == 1:
field_name = self._get_geofield() field_name = None
geom = args[0] geom = args[0]
elif nargs == 2: elif nargs == 2:
field_name, geom = args field_name, geom = args
@ -274,29 +269,33 @@ class GeoQuerySet(QuerySet):
raise ValueError('Maximum two arguments allowed for `distance` aggregate.') raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
# Getting the quoted column. # Getting the quoted column.
field_col = self._geo_column(field_name) geo_field = self._geo_field(field_name)
if not field_col: if not geo_field:
raise TypeError('Distance output only available on GeometryFields.') 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 # Using the field's get_db_prep_lookup() to get any needed
# transformation SQL -- we pass in a 'dummy' `contains` lookup # transformation and distance SQL -- we pass in a 'dummy'
# type. # `distance_lte` lookup type.
geom_sql = geo_field.get_db_prep_lookup('contains', geom) where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
if oracle: if oracle:
# The `tolerance` keyword may be used for Oracle. # The `tolerance` keyword may be used for Oracle.
tolerance = kwargs.get('tolerance', 0.05) tolerance = kwargs.get('tolerance', 0.05)
# More legwork here because the OracleSpatialAdaptor doesn't do # More legwork here because the OracleSpatialAdaptor doesn't do
# quoting of the WKT. # quoting of the WKT.
params = ["'%s'" % geom_sql.params[0]] tmp_params = [gqn(str(params[0]))]
params.extend(geom_sql.params[1:]) tmp_params.extend(params[1:])
gsql = geom_sql.where[0] % tuple(params) dsql = where[0] % tuple(tmp_params)
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)} dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, dsql, tolerance)}
else: 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) return self.extra(select=dist_select)
def extent(self, field_name=None): def extent(self, field_name=None):
@ -308,12 +307,10 @@ class GeoQuerySet(QuerySet):
if not EXTENT: if not EXTENT:
raise ImproperlyConfigured('Extent stored procedure not available.') raise ImproperlyConfigured('Extent stored procedure not available.')
if not field_name: geo_field = self._geo_field(field_name)
field_name = self._get_geofield() if not geo_field:
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('Extent information only available on GeometryFields.') raise TypeError('Extent information only available on GeometryFields.')
geo_col = self._field_column(geo_field)
# Getting the SQL for the query. # Getting the SQL for the query.
try: try:
@ -322,7 +319,7 @@ class GeoQuerySet(QuerySet):
return None return None
# Constructing the query that will select the extent. # 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 # Getting a cursor, executing the query, and extracting the returned
# value from the extent function. # value from the extent function.
@ -353,23 +350,21 @@ class GeoQuerySet(QuerySet):
# If no field name explicitly given, get the first GeometryField from # If no field name explicitly given, get the first GeometryField from
# the model. # the model.
if not field_name: geo_field = self._geo_field(field_name)
field_name = self._get_geofield() if not geo_field:
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('GML output only available on GeometryFields.') raise TypeError('GML output only available on GeometryFields.')
geo_col = self._field_column(geo_field)
if oracle: if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)} gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
elif postgis: elif postgis:
# PostGIS AsGML() aggregate function parameter order depends on the # PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh. # version -- uggh.
major, minor1, minor2 = SpatialBackend.version major, minor1, minor2 = SpatialBackend.version
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): 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: 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. # Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select) return self.extra(select=gml_select)
@ -385,48 +380,49 @@ class GeoQuerySet(QuerySet):
raise ImproperlyConfigured('AsKML() stored procedure not available.') raise ImproperlyConfigured('AsKML() stored procedure not available.')
# Getting the geographic field. # Getting the geographic field.
if not field_name: geo_field = self._geo_field(field_name)
field_name = self._get_geofield() if not geo_field:
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('KML output only available on GeometryFields.') 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. # 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): def transform(self, field_name=None, srid=4326):
""" """
Transforms the given geometry field to the given SRID. If no SRID is Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84). provided, the transformation will default to using 4326 (WGS84).
""" """
# Getting the geographic field. TRANSFORM = SpatialBackend.transform
if not field_name: if not TRANSFORM:
field_name = self._get_geofield() raise ImproperlyConfigured('Transform stored procedure not available.')
elif isinstance(field_name, int):
srid = field_name
field_name = self._get_geofield()
field = self.model._meta.get_field(field_name) # `field_name` is first for backwards compatibility; but we want to
if not isinstance(field, GeometryField): # 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) raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
# Why cascading substitutions? Because spatial backends like # Why cascading substitutions? Because spatial backends like
# Oracle and MySQL already require a function call to convert to text, thus # 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. # when there's also a transformation we need to cascade the substitutions.
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' # 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 # Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database. # override the geometry column returned from the database.
TRANSFORM = SpatialBackend.transform
if oracle: if oracle:
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid) custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
self._ewkt = srid self._ewkt = srid
else: else:
custom_sel = '(%s(%s, %s)) AS %s' % \ custom_sel = '(%s(%s, %s)) AS %s' % \
(TRANSFORM, col, srid, connection.ops.quote_name(field.column)) (TRANSFORM, geo_col, srid, connection.ops.quote_name(geo_field.column))
self._custom_select[field.column] = custom_sel self._custom_select[geo_field.column] = custom_sel
return self._clone() return self._clone()
def union(self, field_name=None, tolerance=0.0005): def union(self, field_name=None, tolerance=0.0005):
@ -441,12 +437,10 @@ class GeoQuerySet(QuerySet):
raise ImproperlyConfigured('Union stored procedure not available.') raise ImproperlyConfigured('Union stored procedure not available.')
# Getting the geographic field column # Getting the geographic field column
if not field_name: geo_field = self._geo_field(field_name)
field_name = self._get_geofield() if not geo_field:
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('Aggregate Union only available on GeometryFields.') raise TypeError('Aggregate Union only available on GeometryFields.')
geo_col = self._field_column(geo_field)
# Getting the SQL for the query. # Getting the SQL for the query.
try: try:
@ -458,10 +452,10 @@ class GeoQuerySet(QuerySet):
# on the geographic field column. # on the geographic field column.
if oracle: if oracle:
union_sql = 'SELECT %s' % self._geo_fmt 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 union_sql += sql
else: 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. # Getting a cursor, executing the query.
cursor = connection.cursor() cursor = connection.cursor()

View File

@ -71,7 +71,7 @@ class SpatialRefSysMixin(object):
@property @property
def spheroid(self): def spheroid(self):
"Returns the spheroid for this spatial reference." "Returns the spheroid name for this spatial reference."
return self.srs['spheroid'] return self.srs['spheroid']
@property @property
@ -170,6 +170,34 @@ class SpatialRefSysMixin(object):
m = cls.units_regex.match(wkt) m = cls.units_regex.match(wkt)
return m.group('unit'), m.group('unit_name') 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): def __unicode__(self):
""" """
Returns the string representation. If GDAL is installed, Returns the string representation. If GDAL is installed,

View File

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

View File

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

View File

@ -1,20 +1,20 @@
from django.contrib.gis.db import models 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) name = models.CharField(max_length=30)
point = models.PointField(srid=32140) point = models.PointField(srid=32140)
objects = models.GeoManager() objects = models.GeoManager()
def __unicode__(self): return self.name 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): #class County(models.Model):
# name = models.CharField(max_length=30) # name = models.CharField(max_length=30)
# mpoly = models.MultiPolygonField(srid=32140) # mpoly = models.MultiPolygonField(srid=32140)
# objects = models.GeoManager() # objects = models.GeoManager()
city_mapping = {'name' : 'Name',
'point' : 'POINT',
}
#county_mapping = {'name' : 'Name',
# 'mpoly' : 'MULTIPOLYGON',
# }

View File

@ -1,53 +1,68 @@
import os, unittest import os, unittest
from decimal import Decimal 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.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.measure import D # alias for Distance
from django.contrib.gis.db.models import GeoQ
from django.contrib.gis.tests.utils import oracle from django.contrib.gis.tests.utils import oracle
shp_path = os.path.dirname(__file__) from models import SouthTexasCity, AustraliaCity
city_shp = os.path.join(shp_path, 'cities/cities.shp') from data import au_cities, stx_cities
#county_shp = os.path.join(shp_path, 'counties/counties.shp')
class DistanceTest(unittest.TestCase): 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): def test01_init(self):
"LayerMapping initialization of distance models." "Initialization of distance models."
city_lm = LayerMapping(City, city_shp, city_mapping, transform=False) def load_cities(city_model, srid, data_tup):
city_lm.save() for name, x, y in data_tup:
c = city_model(name=name, point=Point(x, y, srid=srid))
c.save()
# TODO: complete tests with distance from multipolygons. load_cities(SouthTexasCity, 32140, stx_cities)
#county_lm = LayerMapping(County, county_shp, county_mapping, transform=False) load_cities(AustraliaCity, 4326, au_cities)
#county_lm.save()
self.assertEqual(12, City.objects.count()) self.assertEqual(10, SouthTexasCity.objects.count())
#self.assertEqual(60, County.objects.count()) self.assertEqual(10, AustraliaCity.objects.count())
# TODO: Complete tests for `dwithin` lookups. def test02_dwithin(self):
#def test02_dwithin(self): "Testing the `dwithin` lookup type."
# "Testing the `dwithin` lookup type." pnt = self.stx_pnt
# pass 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): def test03_distance_aggregate(self):
"Testing the `distance` GeoQuerySet method." "Testing the `distance` GeoQuerySet method."
# The point for La Grange, TX # The point for La Grange, TX
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326) lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
# Got these from using the raw SQL statement: # 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; # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
distances = [147075.069813436, 139630.198056286, 140888.552826286, distances = [147075.069813, 139630.198056, 140888.552826,
138809.684197415, 158309.246259353, 212183.594374882, 138809.684197, 158309.246259, 212183.594374,
70870.1889675217, 319225.965633536, 165337.758878256, 70870.188967, 165337.758878, 102128.654360,
92630.7446925393, 102128.654360872, 139196.085105372] 139196.085105]
dist1 = City.objects.distance('point', lagrange) dist1 = SouthTexasCity.objects.distance('point', lagrange)
dist2 = City.objects.distance(lagrange) dist2 = SouthTexasCity.objects.distance(lagrange)
# Original query done on PostGIS, have to adjust AlmostEqual tolerance # Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle. # for Oracle.
if oracle: tol = 3 if oracle: tol = 3
else: tol = 7 else: tol = 5
for qs in [dist1, dist2]: for qs in [dist1, dist2]:
for i, c in enumerate(qs): for i, c in enumerate(qs):
@ -55,30 +70,50 @@ class DistanceTest(unittest.TestCase):
def test04_distance_lookups(self): def test04_distance_lookups(self):
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types." "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 # Only two cities (Houston and Southside Place) should be
# within 7km of the given point. # within 7km of the given point.
qs1 = City.objects.filter(point__distance_lte=(pnt, D(km=7))) # Query w/Distance instance. dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
qs2 = City.objects.filter(point__distance_lte=(pnt, 7000)) # Query w/int (units are assumed to be that of the field) 7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
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 dist in dists:
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
for c in qs: for c in qs:
self.assertEqual(2, qs.count()) cities = self.get_cities(qs)
self.failIf(not c.name in ['Downtown Houston', 'Southside Place']) self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole' # Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
# (thus, Houston and Southside place will be excluded) # (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))) qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
self.assertEqual(3, qs.count()) cities = self.get_cities(qs)
for c in qs: self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
self.failIf(not c.name in ['Pearland', 'Bellaire', '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(): def suite():
s = unittest.TestSuite() s = unittest.TestSuite()