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(, ) 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): - """ - Container for passing values to `parse_lookup` from the various - backend geometry fields. - """ - def __init__(self, where=[], params=[]): - self.where = where - self.params = params +from types import UnicodeType - 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) +def gqn(val): + """ + The geographic quote name function; used for quoting tables and + geometries (they use single rather than the double quotes of the + backend quotename function). + """ + 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.+)\",(?P\d+(\.\d+)?),(?P\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 Binary files a/django/contrib/gis/tests/distapp/cities/cities.dbf and /dev/null 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 Binary files a/django/contrib/gis/tests/distapp/cities/cities.shp and /dev/null 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 Binary files a/django/contrib/gis/tests/distapp/cities/cities.shx and /dev/null 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()