1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +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

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

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

View File

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

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.
"""
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

View File

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

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

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

View File

@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1:
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
DISTANCE = get_func('Distance')
DISTANCE_SPHEROID = get_func('distance_spheroid')
EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
@ -74,8 +75,20 @@ class PostGISFunctionParam(PostGISFunction):
class PostGISDistance(PostGISFunction):
"For PostGIS distance operations."
dist_func = 'Distance'
def __init__(self, operator):
super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s')
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISSphereDistance(PostGISFunction):
"For PostGIS spherical distance operations."
dist_func = 'distance_spheroid'
def __init__(self, operator):
# An extra parameter in `end_subst` is needed for the spheroid string.
super(PostGISSphereDistance, self).__init__(self.dist_func,
beg_subst='%s(%s, %%s, %%s',
end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls."
@ -148,21 +161,24 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
}
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int)
dtypes = (Decimal, Distance, float, int, long)
def get_dist_ops(operator):
"Returns operations for both regular and spherical distances."
return (PostGISDistance(operator), PostGISSphereDistance(operator))
DISTANCE_FUNCTIONS = {
'distance_gt' : (PostGISDistance('>'), dtypes),
'distance_gte' : (PostGISDistance('>='), dtypes),
'distance_lt' : (PostGISDistance('<'), dtypes),
'distance_lte' : (PostGISDistance('<='), dtypes),
'distance_gt' : (get_dist_ops('>'), dtypes),
'distance_gte' : (get_dist_ops('>='), dtypes),
'distance_lt' : (get_dist_ops('<'), dtypes),
'distance_lte' : (get_dist_ops('<='), dtypes),
}
if GEOM_FUNC_PREFIX == 'ST_':
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
{'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
'coveredby' : PostGISFunction('CoveredBy'),
{'coveredby' : PostGISFunction('CoveredBy'),
'covers' : PostGISFunction('Covers'),
})
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
# Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
@ -178,10 +194,10 @@ POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnul
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
#### The `get_geo_where_clause` function for PostGIS. ####
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
# Getting the quoted field as `geo_col`.
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
if lookup_type in POSTGIS_OPERATORS:
# See if a PostGIS operator matches the lookup type.
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
@ -198,7 +214,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple):
if not isinstance(value, (tuple, list)):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(value) != 2:
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
@ -209,7 +225,18 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
if lookup_type == 'relate': op = op(value[1])
if lookup_type == 'relate':
op = op(value[1])
elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
if field._unit_name == 'degree':
# Geodetic distances are only availble from Points to PointFields.
if field._geom != 'POINT':
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
if value[0].geom_typeid != 0:
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
op = op[1]
else:
op = op[0]
else:
op = tmp
# Calling the `as_sql` function on the operation instance.

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ class SpatialRefSysMixin(object):
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
# parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
# For pulling out the units on platforms w/o GDAL installed.
# TODO: Figure out how to pull out angular units of projected coordinate system and
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
@ -71,7 +71,7 @@ class SpatialRefSysMixin(object):
@property
def spheroid(self):
"Returns the spheroid for this spatial reference."
"Returns the spheroid name for this spatial reference."
return self.srs['spheroid']
@property
@ -170,6 +170,34 @@ class SpatialRefSysMixin(object):
m = cls.units_regex.match(wkt)
return m.group('unit'), m.group('unit_name')
@classmethod
def get_spheroid(cls, wkt, string=True):
"""
Class method used by GeometryField on initialization to
retrieve the `SPHEROID[..]` parameters from the given WKT.
"""
if HAS_GDAL:
srs = SpatialReference(wkt)
sphere_params = srs.ellipsoid
sphere_name = srs['spheroid']
else:
m = cls.spheroid_regex.match(wkt)
if m:
sphere_params = (float(m.group('major')), float(m.group('flattening')))
sphere_name = m.group('name')
else:
return None
if not string:
return sphere_name, sphere_params
else:
# `string` parameter used to place in format acceptable by PostGIS
if len(sphere_params) == 3:
radius, flattening = sphere_params[0], sphere_params[2]
else:
radius, flattening = sphere_params
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
def __unicode__(self):
"""
Returns the string representation. If GDAL is installed,

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
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',
# }

View File

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