1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

gis: Added distance querying capabilites via the distance manager method and the distance_[gt|gte|lt|lte] lookup types (works for both PostGIS and Oracle); improved Oracle query construction and fixed transform issues.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6886 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2007-12-04 17:44:37 +00:00
parent cc0cc9fa08
commit fae19f8ced
26 changed files with 786 additions and 269 deletions

View File

@ -26,41 +26,33 @@ from django.utils.datastructures import SortedDict
from django.contrib.gis.geos import GEOSGeometry
# These routines (needed by GeoManager), default to False.
ASGML, ASKML, TRANSFORM, UNION= (False, False, False, False)
ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules,
# renaming as necessary.
from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, gqn, \
ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
create_spatial_db, get_geo_where_clause, \
ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \
OracleSpatialField as GeoBackendField, \
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, gqn, \
ASGML, GEOM_SELECT, TRANSFORM, UNION
create_spatial_db, get_geo_where_clause, \
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'oracle'
elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.mysql import \
MySQLGeoField as GeoBackendField, \
MYSQL_GIS_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, gqn, \
create_spatial_db, get_geo_where_clause, \
GEOM_SELECT
SPATIAL_BACKEND = 'mysql'
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
def geo_quotename(value):
"""
Returns the quotation used on a given Geometry value using the geometry
quoting from the backend (the `gqn` function).
"""
if isinstance(value, (StringType, UnicodeType)): return gqn(value)
else: return str(value)
#### query.py overloaded functions ####
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
# counterparts to support constructing SQL for geographic queries.
@ -287,28 +279,15 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
# If the field is a geometry field, then the WHERE clause will need to be obtained
# with the get_geo_where_clause()
if hasattr(field, '_geom'):
# Do we have multiple arguments, e.g., `relate`, `dwithin` lookup types
# need more than argument.
multiple_args = isinstance(value, tuple)
# Getting the preparation SQL object from the field.
if multiple_args:
geo_prep = field.get_db_prep_lookup(lookup_type, value[0])
else:
geo_prep = field.get_db_prep_lookup(lookup_type, value)
# Getting the adapted geometry from the field.
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
# A GeoFieldSQL object is returned by `get_db_prep_lookup` --
# getting the substitution list and the geographic parameters.
subst_list = geo_prep.where
if multiple_args: subst_list += map(geo_quotename, value[1:])
gwc = gwc % tuple(subst_list)
# Finally, appending onto the WHERE clause, and extending with
# the additional parameters.
where.append(gwc)
# Substituting in the the where parameters into the geographic where
# clause, and extending the parameters.
where.append(gwc % tuple(geo_prep.where))
params.extend(geo_prep.params)
else:
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))

View File

@ -10,5 +10,5 @@ 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, GEOM_SELECT, TRANSFORM, UNION
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION

View File

@ -6,7 +6,7 @@ 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 get_srid, GeoFieldSQL
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, TRANSFORM
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
@ -21,12 +21,12 @@ class OracleSpatialField(Field):
empty_strings_allowed = False
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.00005, **kwargs):
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
"""
Oracle Spatial backend needs to have the extent -- for projected coordinate
systems _you must define the extent manually_, since the coordinates are
for geodetic systems. The `tolerance` keyword specifies the tolerance
for error (in meters).
for error (in meters), and defaults to 0.05 (5 centimeters).
"""
# Oracle Spatial specific keyword arguments.
self._extent = extent
@ -104,32 +104,32 @@ class OracleSpatialField(Field):
# special case for isnull lookup
if lookup_type == 'isnull': return GeoFieldSQL([], [])
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(value, GEOSGeometry):
pass
elif isinstance(value, (StringType, UnicodeType)):
try:
value = GEOSGeometry(value)
except GEOSException:
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
else:
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
# Getting the SRID of the geometry, or defaulting to that of the field if
# it is None.
srid = get_srid(self, value)
# Get the geometry with SRID; defaults SRID to that
# of the field if it is None
geom = self.get_geometry(value)
# The adaptor will be used by psycopg2 for quoting the WKT.
adapt = OracleSpatialAdaptor(value)
if srid != self._srid:
adapt = OracleSpatialAdaptor(geom)
if geom.srid != self._srid:
# Adding the necessary string substitutions and parameters
# to perform a geometry transformation.
return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
[adapt, self._srid])
where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
params = [adapt, self._srid]
else:
return GeoFieldSQL(['SDO_GEOMETRY(%%s, %s)' % srid], [adapt])
where = ['SDO_GEOMETRY(%%s, %s)' % geom.srid]
params = [adapt]
if isinstance(value, tuple):
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
# Getting the distance parameter in the units of the field
where += [self.get_distance(value[1])]
elif lookup_type == 'relate':
# No extra where parameters for SDO_RELATE queries.
pass
else:
where += map(gqn, value[1:])
return GeoFieldSQL(where, params)
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)

View File

@ -20,7 +20,7 @@ class GeometryColumns(models.Model):
db_table = 'USER_SDO_GEOM_METADATA'
@classmethod
def table_name_col(self):
def table_name_col(cls):
return 'table_name'
def __unicode__(self):
@ -43,3 +43,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
@property
def wkt(self):
return self.wktext
@classmethod
def wkt_col(cls):
return 'wktext'

View File

@ -2,25 +2,100 @@
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for Oracle Spatial.
"""
import re
from decimal import Decimal
from django.db import connection
from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
ORACLE_GEOMETRY_FUNCTIONS = {
'contains' : 'SDO_CONTAINS',
'coveredby' : 'SDO_COVEREDBY',
'covers' : 'SDO_COVERS',
'disjoint' : 'SDO_DISJOINT',
'dwithin' : ('SDO_WITHIN_DISTANCE', float),
'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
'equals' : 'SDO_EQUAL',
'exact' : 'SDO_EQUAL',
'overlaps' : 'SDO_OVERLAPS',
'same_as' : 'SDO_EQUAL',
#'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
'touches' : 'SDO_TOUCH',
'within' : 'SDO_INSIDE',
# The GML, distance, transform, and union procedures.
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
TRANSFORM = 'SDO_CS.TRANSFORM'
UNION = 'SDO_AGGR_UNION'
class SDOOperation(object):
"Base class for SDO* Oracle operations."
def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
beg_subst='%s(%s%s, %%s'):
self.lookup = lookup
self.subst = subst
self.operator = operator
self.result = result
self.beg_subst = beg_subst
self.end_subst = ') %s %s' % (self.operator, self.result)
@property
def sql_subst(self):
return ''.join([self.beg_subst, self.subst, self.end_subst])
def as_sql(self, table, field):
return self.sql_subst % self.params(table, field)
def params(self, table, field):
return (self.lookup, table, field)
class SDODistance(SDOOperation):
"Class for Distance queries."
def __init__(self, op, tolerance=0.05):
super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
self.tolerance = tolerance
def params(self, table, field):
return (self.lookup, table, field, self.tolerance)
class SDOGeomRelate(SDOOperation):
"Class for using SDO_GEOM.RELATE."
def __init__(self, mask, tolerance=0.05):
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'",
subst=", %%s, %s", result="'%s'" % mask)
self.mask = mask
self.tolerance = tolerance
def params(self, table, field):
return (self.lookup, table, field, self.mask, self.tolerance)
class SDORelate(SDOOperation):
"Class for using SDO_RELATE."
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
def __init__(self, mask, **kwargs):
super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs)
if not self.mask_regex.match(mask):
raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
self.mask = mask
def params(self, table, field):
return (self.lookup, table, field, self.mask)
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int)
DISTANCE_FUNCTIONS = {
'distance_gt' : (SDODistance('>'), dtypes),
'distance_gte' : (SDODistance('>='), dtypes),
'distance_lt' : (SDODistance('<'), dtypes),
'distance_lte' : (SDODistance('<='), dtypes),
}
ORACLE_GEOMETRY_FUNCTIONS = {
'contains' : SDOOperation('SDO_CONTAINS'),
'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'),
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'),
'exact' : SDOOperation('SDO_EQUAL'),
'overlaps' : SDOOperation('SDO_OVERLAPS'),
'same_as' : SDOOperation('SDO_EQUAL'),
'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
'touches' : SDOOperation('SDO_TOUCH'),
'within' : SDOOperation('SDO_INSIDE'),
}
ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# This lookup type does not require a mapping.
MISC_TERMS = ['isnull']
@ -43,25 +118,33 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type
# of the expected argument (e.g., str, float)
func, arg_type = lookup_info
sdo_op, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple) or len(value) != 2:
raise TypeError('2-element tuple required for %s lookup type.' % lookup_type)
if not isinstance(value, tuple):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(value) != 2:
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if func == 'dwithin':
# TODO: test and consider adding different distance options.
return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
if lookup_type == 'relate':
# The SDORelate class handles construction for these queries, and verifies
# the mask argument.
return sdo_op(value[1]).as_sql(table_prefix, field_name)
elif lookup_type in DISTANCE_FUNCTIONS:
op = DISTANCE_FUNCTIONS[lookup_type][0]
return op.as_sql(table_prefix, field_name)
# return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
else:
return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
return sdo_op.as_sql(table_prefix, field_name)
else:
# Returning the SQL necessary for the geometry function call. For example:
# Lookup info is a SDOOperation instance, whos `as_sql` method returns
# the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
return "%s(%s, %%s) = 'TRUE'" % (lookup_info, table_prefix + field_name)
return lookup_info.as_sql(table_prefix, field_name)
# Handling 'isnull' lookup type
if lookup_type == 'isnull':
@ -69,10 +152,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
UNION = 'SDO_AGGR_UNION'
TRANSFORM = 'SDO_CS.TRANSFORM'
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
# from WKT than SDO_GEOMETRY(...) strings ;)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'

View File

@ -6,4 +6,4 @@ 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, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT

View File

@ -7,11 +7,11 @@ from psycopg2 import Binary
from psycopg2.extensions import ISQLQuote
class PostGISAdaptor(object):
def __init__(self, geom, srid):
def __init__(self, geom):
"Initializes on the geometry and the SRID."
# Getting the WKB and the SRID
self.wkb = geom.wkb
self.srid = srid
self.srid = geom.srid
def __conform__(self, proto):
# Does the given protocol conform to what Psycopg2 expects?

View File

@ -1,19 +1,26 @@
from types import StringType, UnicodeType
from types import UnicodeType
from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry, GEOSException
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
from psycopg2 import Binary
from django.contrib.gis.db.backend.postgis.query import \
DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively
qn = connection.ops.quote_name
def gqn(value):
if isinstance(value, basestring):
if isinstance(value, UnicodeType): value = value.encode('ascii')
return "'%s'" % value
else:
return str(value)
class PostGISField(Field):
"""
The backend-specific geographic field for PostGIS.
"""
def _add_geom(self, style, db_table):
"""
Constructs the addition of the geometry to the table using the
@ -92,53 +99,44 @@ class PostGISField(Field):
"""
if lookup_type in POSTGIS_TERMS:
# special case for isnull lookup
if lookup_type == 'isnull':
return GeoFieldSQL([], [value])
if lookup_type == 'isnull': return GeoFieldSQL([], [])
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(value, GEOSGeometry):
pass
elif isinstance(value, (StringType, UnicodeType)):
try:
value = GEOSGeometry(value)
except GEOSException:
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
else:
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
# Getting the SRID of the geometry, or defaulting to that of the field if
# it is None.
srid = get_srid(self, value)
# Get the geometry with SRID; defaults SRID to
# that of the field if it is None.
geom = self.get_geometry(value)
# The adaptor will be used by psycopg2 for quoting the WKB.
adapt = PostGISAdaptor(value, srid)
adapt = PostGISAdaptor(geom)
if srid != self._srid:
if geom.srid != self._srid:
# Adding the necessary string substitutions and parameters
# to perform a geometry transformation.
return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
[adapt, self._srid])
where = ['%s(%%s,%%s)' % TRANSFORM]
params = [adapt, self._srid]
else:
return GeoFieldSQL(['%s'], [adapt])
# Otherwise, the adaptor will take care of everything.
where = ['%s']
params = [adapt]
if isinstance(value, tuple):
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
# Getting the distance parameter in the units of the field.
where += [self.get_distance(value[1])]
else:
where += map(gqn, value[1:])
return GeoFieldSQL(where, params)
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if not bool(value): return None
if isinstance(value, GEOSGeometry):
return PostGISAdaptor(value, value.srid)
return PostGISAdaptor(value)
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
def get_internal_type(self):
"""
Returns NoField because a stored procedure is used by PostGIS to create
the Geometry Fields.
"""
return 'NoField'
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the

View File

@ -16,17 +16,17 @@ class GeometryColumns(models.Model):
"""
f_table_catalog = models.CharField(maxlength=256)
f_table_schema = models.CharField(maxlength=256)
f_table_name = models.CharField(maxlength=256, primary_key=True)
f_table_name = models.CharField(maxlength=256)
f_geometry_column = models.CharField(maxlength=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField()
srid = models.IntegerField(primary_key=True)
type = models.CharField(maxlength=30)
class Meta:
db_table = 'geometry_columns'
@classmethod
def table_name_col(self):
def table_name_col(cls):
"Class method for returning the table name column for this model."
return 'f_table_name'
@ -52,3 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
@property
def wkt(self):
return self.srtext
@classmethod
def wkt_col(cls):
return 'srtext'

View File

@ -2,9 +2,10 @@
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for PostGIS.
"""
from decimal import Decimal
from django.db import connection
from django.contrib.gis.measure import Distance
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
from types import StringType, UnicodeType
qn = connection.ops.quote_name
# Getting the PostGIS version information
@ -62,10 +63,32 @@ POSTGIS_OPERATORS = {
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
# means that 'ST_' is prefixes geometry function names.
if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
GEOM_FUNC_PREFIX = 'ST_'
else:
GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
GEOM_FUNC_PREFIX = 'ST_'
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
# Functions used by the GeoManager & GeoQuerySet
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
DISTANCE = get_func('Distance')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
TRANSFORM = get_func('Transform')
# Special cases for union and KML methods.
if MINOR_VERSION1 < 3:
UNION = 'GeomUnion'
else:
UNION = 'ST_Union'
if MINOR_VERSION1 == 1:
ASKML = False
else:
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
# first before calling the more computationally expensive GEOS routines (called
@ -81,25 +104,36 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
'overlaps' : 'Overlaps',
'contains' : 'Contains',
'intersects' : 'Intersects',
'relate' : ('Relate', str),
'relate' : ('Relate', basestring),
}
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int)
DISTANCE_FUNCTIONS = {
'distance_gt' : ('>', dtypes),
'distance_gte' : ('>=', dtypes),
'distance_lt' : ('<', dtypes),
'distance_lte' : ('<=', dtypes),
}
if GEOM_FUNC_PREFIX == 'ST_':
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
if isinstance(func, tuple):
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1])
for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
if isinstance(f, tuple):
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
else:
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
{'dwithin' : ('ST_DWithin', float),
{'dwithin' : ('ST_DWithin', dtypes),
'coveredby' : 'ST_CoveredBy',
'covers' : 'ST_Covers',
}
)
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# Any other lookup types that do not require a mapping.
MISC_TERMS = ['isnull']
@ -139,13 +173,19 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
func, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple) or len(value) != 2:
raise TypeError('2-element tuple required for `%s` lookup type.' % lookup_type)
if not isinstance(value, tuple):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(value) != 2:
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if lookup_type in DISTANCE_FUNCTIONS:
op = DISTANCE_FUNCTIONS[lookup_type][0]
return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
else:
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
else:
# Returning the SQL necessary for the geometry function call. For example:
@ -158,33 +198,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
# Functions that we define manually.
if MAJOR_VERSION == 1:
if MINOR_VERSION1 == 3:
# PostGIS versions 1.3.x
ASKML = 'ST_AsKML'
ASGML = 'ST_AsGML'
GEOM_FROM_TEXT = 'ST_GeomFromText'
GEOM_FROM_WKB = 'ST_GeomFromWKB'
UNION = 'ST_Union'
TRANSFORM = 'ST_Transform'
elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1:
# PostGIS versions 1.2.x
ASKML = 'AsKML'
ASGML = 'AsGML'
GEOM_FROM_TEXT = 'GeomFromText'
GEOM_FROM_WKB = 'GeomFromWKB'
UNION = 'GeomUnion'
TRANSFORM = 'Transform'
elif MINOR_VERSION1 == 1 and MINOR_VERSION2 >= 0:
# PostGIS versions 1.1.x
ASKML = False
ASGML = 'AsGML'
GEOM_FROM_TEXT = 'GeomFromText'
GEOM_FROM_WKB = 'GeomFromWKB'
TRANSFORM = 'Transform'
UNION = 'GeomUnion'
# Custom selection not needed for PostGIS since GEOS geometries may be
# instantiated directly from the HEXEWKB returned by default. If
# WKT is needed for some reason in the future, this value may be changed,

View File

@ -7,6 +7,9 @@ class GeoFieldSQL(object):
self.where = where
self.params = params
def __str__(self):
return self.where[0] % tuple(self.params)
def get_srid(field, geom):
"""
Gets the SRID depending on the value of the SRID setting of the field

View File

@ -1,8 +1,17 @@
from decimal import Decimal
from django.conf import settings
from django.db import connection
from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend.
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.measure import Distance
from django.contrib.gis.oldforms import WKTField
from django.contrib.gis.geos import GEOSGeometry
# Attempting to get the spatial reference system.
try:
from django.contrib.gis.models import SpatialRefSys
except NotImplementedError:
SpatialRefSys = None
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
class GeometryField(GeoBackendField):
@ -11,24 +20,101 @@ class GeometryField(GeoBackendField):
# The OpenGIS Geometry name.
_geom = 'GEOMETRY'
def __init__(self, srid=4326, index=True, dim=2, **kwargs):
"""The initialization function for geometry fields. Takes the following
def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
"""
The initialization function for geometry fields. Takes the following
as keyword arguments:
srid - The spatial reference system identifier. An OGC standard.
Defaults to 4326 (WGS84)
srid:
The spatial reference system identifier, an OGC standard.
Defaults to 4326 (WGS84).
index - Indicates whether to create a GiST index. Defaults to True.
spatial_index:
Indicates whether to create a spatial index. Defaults to True.
Set this instead of 'db_index' for geographic fields since index
creation is different for geometry columns.
dim - The number of dimensions for this geometry. Defaults to 2.
dim:
The number of dimensions for this geometry. Defaults to 2.
"""
self._index = index
# Backward-compatibility notice, this will disappear in future revisions.
if 'index' in kwargs:
from warnings import warn
warn('The `index` keyword has been deprecated, please use the `spatial_index` keyword instead.')
self._index = kwargs['index']
else:
self._index = spatial_index
# Setting the SRID and getting the units.
self._srid = srid
if SpatialRefSys:
# This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
# Why? `syncdb` fails to recognize installed geographic models when there's
# an ORM query instantiated within a model field. No matter, this works fine
# too.
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'),
'srid' : srid,
}
cur.execute(stmt)
row = cur.fetchone()
self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
# Setting the dimension of the geometry field.
self._dim = dim
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
### Routines specific to GeometryField ###
def get_distance(self, dist):
if isinstance(dist, Distance):
return getattr(dist, Distance.unit_attname(self._unit_name))
elif isinstance(dist, (int, float, Decimal)):
# Assuming the distance is in the units of the field.
return dist
def get_geometry(self, value):
"""
Retrieves the geometry, setting the default SRID from the given
lookup parameters.
"""
if isinstance(value, tuple):
geom = value[0]
else:
geom = value
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(geom, GEOSGeometry):
pass
elif isinstance(geom, basestring):
try:
geom = GEOSGeometry(geom)
except GEOSException:
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
else:
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
# Assigning the SRID value.
geom.srid = self.get_srid(geom)
return geom
def get_srid(self, geom):
"""
Has logic for retrieving the default SRID taking into account
the SRID of the field.
"""
if geom.srid is None or (geom.srid == -1 and self._srid != -1):
return self._srid
else:
return geom.srid
### Routines overloaded from Field ###
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)

View File

@ -7,6 +7,9 @@ class GeoManager(Manager):
def get_query_set(self):
return GeoQuerySet(model=self.model)
def distance(self, *args, **kwargs):
return self.get_query_set().distance(*args, **kwargs)
def gml(self, *args, **kwargs):
return self.get_query_set().gml(*args, **kwargs)

View File

@ -6,9 +6,13 @@ 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, ASGML, ASKML, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
from django.contrib.gis.db.backend import parse_lookup, \
ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
from django.contrib.gis.geos import GEOSGeometry
# Flag indicating whether the backend is Oracle.
oracle = SPATIAL_BACKEND == 'oracle'
class GeoQ(Q):
"Geographical query encapsulation object."
@ -28,11 +32,23 @@ class GeoQuerySet(QuerySet):
# For replacement fields in the SELECT.
self._custom_select = {}
self._ewkt = None
# If GEOM_SELECT is defined in the backend, then it will be used
# for the selection format of the geometry column.
if GEOM_SELECT: self._geo_fmt = GEOM_SELECT
else: self._geo_fmt = '%s'
if GEOM_SELECT:
#if oracle and hasattr(self, '_ewkt'):
# Transformed geometries in Oracle use EWKT so that the SRID
# on the transformed lazy geometries is set correctly).
#print '-=' * 20
#print self._ewkt, GEOM_SELECT
#self._geo_fmt = "'SRID=%d;'||%s" % (self._ewkt, GEOM_SELECT)
#self._geo_fmt = GEOM_SELECT
#else:
#print '-=' * 20
self._geo_fmt = GEOM_SELECT
else:
self._geo_fmt = '%s'
def _filter_or_exclude(self, mapper, *args, **kwargs):
# mapper is a callable used to transform Q objects,
@ -66,8 +82,16 @@ class GeoQuerySet(QuerySet):
# handling the selection of native database geometry formats.
for f in opts.fields:
# Getting the selection format string.
if hasattr(f, '_geom'): sel_fmt = self._geo_fmt
else: sel_fmt = '%s'
if hasattr(f, '_geom'):
sel_fmt = self._geo_fmt
# If an SRID needs to specified other than what is in the field
# (like when `transform` is called), make sure to explicitly set
# the SRID by returning EWKT.
if self._ewkt and oracle:
sel_fmt = "'SRID=%d;'||%s" % (self._ewkt, sel_fmt)
else:
sel_fmt = '%s'
# Getting the field selection substitution string
if f.column in self._custom_select:
@ -147,7 +171,7 @@ class GeoQuerySet(QuerySet):
sql.append("ORDER BY " + ", ".join(order_by))
# LIMIT and OFFSET clauses
if SPATIAL_BACKEND != 'oracle':
if not oracle:
if self._limit is not None:
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
else:
@ -206,6 +230,7 @@ class GeoQuerySet(QuerySet):
def _clone(self, klass=None, **kwargs):
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
c._custom_select = self._custom_select
c._ewkt = self._ewkt
return c
#### Methods specific to the GeoQuerySet ####
@ -227,7 +252,60 @@ class GeoQuerySet(QuerySet):
else:
return False
def gml(self, field_name, precision=8, version=2):
def _get_geofield(self):
"Returns the name of the first Geometry field encountered."
for field in self.model._meta.fields:
if isinstance(field, GeometryField):
return field.name
raise Exception('No GeometryFields in the model.')
def distance(self, *args, **kwargs):
"""
Returns the distance from the given geographic field name to the
given geometry in a `distance` attribute on each element of the
GeoQuerySet.
"""
if not DISTANCE:
raise ImproperlyConfigured('Distance() stored proecedure not available.')
# Getting the geometry field and GEOSGeometry object to base distance
# calculations from.
nargs = len(args)
if nargs == 1:
field_name = self._get_geofield()
geom = args[0]
elif nargs == 2:
field_name, geom = args
else:
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
# Getting the quoted column.
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('Distance output only available on GeometryFields.')
# Getting the geographic field instance.
geo_field = self.model._meta.get_field(field_name)
# Using the field's get_db_prep_lookup() to get any needed
# transformation SQL -- we pass in a 'dummy' `contains` lookup
# type.
geom_sql = geo_field.get_db_prep_lookup('contains', geom)
if oracle:
# The `tolerance` keyword may be used for Oracle.
tolerance = kwargs.get('tolerance', 0.05)
# More legwork here because the OracleSpatialAdaptor doesn't do
# quoting of the WKT.
params = ["'%s'" % geom_sql.params[0]]
params.extend(geom_sql.params[1:])
gsql = geom_sql.where[0] % tuple(params)
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
else:
dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
return self.extra(select=dist_select)
def gml(self, field_name=None, precision=8, version=2):
"""
Returns GML representation of the given field in a `gml` attribute
on each element of the GeoQuerySet.
@ -236,12 +314,16 @@ class GeoQuerySet(QuerySet):
if not ASGML:
raise ImproperlyConfigured('AsGML() stored procedure not available.')
# Is the given field name a geographic field?
# If no field name explicitly given, get the first GeometryField from
# the model.
if not field_name:
field_name = self._get_geofield()
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('GML output only available on GeometryFields')
raise TypeError('GML output only available on GeometryFields.')
if SPATIAL_BACKEND == 'oracle':
if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
@ -249,7 +331,7 @@ class GeoQuerySet(QuerySet):
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)
def kml(self, field_name, precision=8):
def kml(self, field_name=None, precision=8):
"""
Returns KML representation of the given field name in a `kml`
attribute on each element of the GeoQuerySet.
@ -258,7 +340,10 @@ class GeoQuerySet(QuerySet):
if not ASKML:
raise ImproperlyConfigured('AsKML() stored procedure not available.')
# Is the given field name a geographic field?
# Getting the geographic field.
if not field_name:
field_name = self._get_geofield()
field_col = self._geo_column(field_name)
if not field_col:
raise TypeError('KML output only available on GeometryFields.')
@ -266,30 +351,40 @@ class GeoQuerySet(QuerySet):
# Adding the AsKML function call to SELECT part of the SQL.
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
def transform(self, field_name, srid=4326):
def transform(self, field_name=None, srid=4326):
"""
Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84).
"""
# Is the given field name a geographic field?
# Getting the geographic field.
if not field_name:
field_name = self._get_geofield()
elif isinstance(field_name, int):
srid = field_name
field_name = self._get_geofield()
field = self.model._meta.get_field(field_name)
if not isinstance(field, GeometryField):
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
# If there's already custom select SQL.
# Why cascading substitutions? Because spatial backends like
# Oracle and MySQL already require a function call to convert to text, thus
# when there's also a transformation we need to cascade the substitutions.
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
col = self._custom_select.get(field.column, self._field_column(field))
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
if SPATIAL_BACKEND == 'oracle':
if oracle:
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
self._ewkt = srid
else:
custom_sel = '(%s(%s, %s)) AS %s' % \
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
self._custom_select[field.column] = custom_sel
return self._clone()
def union(self, field_name, tolerance=0.0005):
def union(self, field_name=None, tolerance=0.0005):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty. The `tolerance` keyword is for
@ -300,6 +395,9 @@ 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:
raise TypeError('Aggregate Union only available on GeometryFields.')
@ -312,7 +410,7 @@ class GeoQuerySet(QuerySet):
# Replacing the select with a call to the ST_Union stored procedure
# on the geographic field column.
if SPATIAL_BACKEND == 'oracle':
if oracle:
union_sql = 'SELECT %s' % self._geo_fmt
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
union_sql += sql
@ -323,7 +421,7 @@ class GeoQuerySet(QuerySet):
cursor = connection.cursor()
cursor.execute(union_sql, params)
if SPATIAL_BACKEND == 'oracle':
if oracle:
# On Oracle have to read out WKT from CLOB first.
clob = cursor.fetchone()[0]
if clob: u = clob.read()

View File

@ -10,17 +10,24 @@ from django.contrib.gis.gdal import HAS_GDAL
if HAS_GDAL:
from django.contrib.gis.gdal import SpatialReference
class SpatialRefSysMixin(object):
"""
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
# For pulling out the spheroid from the spatial reference string. This
# regular expression is used only if the user does not have GDAL installed.
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
# parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
class SpatialRefSysMixin(object):
"""
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
# For pulling out the units on platforms w/o GDAL installed.
# TODO: Figure out how to pull out angular units of projected coordinate system and
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
# distance queries.
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
@property
def srs(self):
"""
@ -53,7 +60,7 @@ class SpatialRefSysMixin(object):
if HAS_GDAL:
return self.srs.ellipsoid
else:
m = spheroid_regex.match(self.wkt)
m = self.spheroid_regex.match(self.wkt)
if m: return (float(m.group('major')), float(m.group('flattening')))
else: return None
@ -75,37 +82,93 @@ class SpatialRefSysMixin(object):
@property
def projected(self):
"Is this Spatial Reference projected?"
if HAS_GDAL:
return self.srs.projected
else:
return self.wkt.startswith('PROJCS')
@property
def local(self):
"Is this Spatial Reference local?"
if HAS_GDAL:
return self.srs.local
else:
return self.wkt.startswith('LOCAL_CS')
@property
def geographic(self):
"Is this Spatial Reference geographic?"
if HAS_GDAL:
return self.srs.geographic
else:
return self.wkt.startswith('GEOGCS')
@property
def linear_name(self):
"Returns the linear units name."
if HAS_GDAL:
return self.srs.linear_name
elif self.geographic:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit_name')
@property
def linear_units(self):
"Returns the linear units."
if HAS_GDAL:
return self.srs.linear_units
@property
def angular_units(self):
"Returns the angular units."
return self.srs.angular_units
elif self.geographic:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit')
@property
def angular_name(self):
"Returns the name of the angular units."
if HAS_GDAL:
return self.srs.angular_name
elif self.projected:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit_name')
@property
def angular_units(self):
"Returns the angular units."
if HAS_GDAL:
return self.srs.angular_units
elif self.projected:
return None
else:
m = self.units_regex.match(self.wkt)
return m.group('unit')
@property
def units(self):
"Returns a tuple of the units and the name."
if self.projected or self.local:
return (self.linear_units, self.linear_name)
elif self.geographic:
return (self.angular_units, self.angular_name)
else:
return (None, None)
@classmethod
def get_units(cls, wkt):
"""
Class method used by GeometryField on initialization to
retrive the units on the given WKT, without having to use
any of the database fields.
"""
if HAS_GDAL:
return SpatialReference(wkt).units
else:
m = cls.units_regex.match(wkt)
return m.group('unit'), m.group('unit_name')
def __unicode__(self):
"""
@ -115,7 +178,7 @@ class SpatialRefSysMixin(object):
try:
return unicode(self.srs)
except:
return unicode(self.srtext)
return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':

View File

@ -3,9 +3,9 @@ from copy import copy
from unittest import TestSuite, TextTestRunner
from django.contrib.gis.gdal import HAS_GDAL
try:
from django.contrib.gis.tests.utils import mysql
from django.contrib.gis.tests.utils import mysql, oracle
except:
mysql = False
mysql, oracle = (False, False)
# Tests that require use of a spatial database (e.g., creation of models)
test_models = ['geoapp']
@ -16,7 +16,12 @@ test_suite_names = [
'test_measure',
]
if HAS_GDAL:
test_models += ['layermap']
if oracle:
# TODO: There is a problem with the `syncdb` SQL for the LayerMapping
# tests on Oracle.
test_models += ['distapp']
else:
test_models += ['distapp', 'layermap']
test_suite_names += [
'test_gdal_driver',
'test_gdal_ds',
@ -54,7 +59,7 @@ def run_tests(module_list, verbosity=1, interactive=True):
(3) Start this database `pg_ctl -D /path/to/user/db start`
On Windows platforms simply use the pgAdmin III utility to add superuser
priviliges to your database user.
privileges to your database user.
Make sure your settings.py matches the settings of the user database.
For example, set the same port number (`DATABASE_PORT=5433`).

Binary file not shown.

View File

@ -0,0 +1 @@
PROJCS["NAD83 / Texas South Central",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",30.28333333333334],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["Meter",1]]

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -0,0 +1,86 @@
import os, unittest
from decimal import Decimal
from models import *
from django.contrib.gis.utils import LayerMapping
from django.contrib.gis.gdal import DataSource
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.measure import D # alias for Distance
from django.contrib.gis.tests.utils import oracle
shp_path = os.path.dirname(__file__)
city_shp = os.path.join(shp_path, 'cities/cities.shp')
#county_shp = os.path.join(shp_path, 'counties/counties.shp')
class DistanceTest(unittest.TestCase):
def test01_init(self):
"LayerMapping initialization of distance models."
city_lm = LayerMapping(City, city_shp, city_mapping, transform=False)
city_lm.save()
# TODO: complete tests with distance from multipolygons.
#county_lm = LayerMapping(County, county_shp, county_mapping, transform=False)
#county_lm.save()
self.assertEqual(12, City.objects.count())
#self.assertEqual(60, County.objects.count())
# TODO: Complete tests for `dwithin` lookups.
#def test02_dwithin(self):
# "Testing the `dwithin` lookup type."
# pass
def test03_distance_aggregate(self):
"Testing the `distance` GeoQuerySet method."
# The point for La Grange, TX
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
# Got these from using the raw SQL statement:
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_city;
distances = [147075.069813436, 139630.198056286, 140888.552826286,
138809.684197415, 158309.246259353, 212183.594374882,
70870.1889675217, 319225.965633536, 165337.758878256,
92630.7446925393, 102128.654360872, 139196.085105372]
dist1 = City.objects.distance('point', lagrange)
dist2 = City.objects.distance(lagrange)
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle.
if oracle: tol = 3
else: tol = 7
for qs in [dist1, dist2]:
for i, c in enumerate(qs):
self.assertAlmostEqual(distances[i], c.distance, tol)
def test04_distance_lookups(self):
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
# The point we are testing distances with -- using a WGS84
# coordinate that'll be implicitly transormed to that to
# the coordinate system of the field, EPSG:32140 (Texas South Central
# w/units in meters)
pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
# Only two cities (Houston and Southside Place) should be
# within 7km of the given point.
qs1 = City.objects.filter(point__distance_lte=(pnt, D(km=7))) # Query w/Distance instance.
qs2 = City.objects.filter(point__distance_lte=(pnt, 7000)) # Query w/int (units are assumed to be that of the field)
qs3 = City.objects.filter(point__distance_lte=(pnt, 7000.0)) # Query w/float
qs4 = City.objects.filter(point__distance_lte=(pnt, Decimal(7000))) # Query w/Decimal
for qs in [qs1, qs2, qs3, qs4]:
for c in qs:
self.assertEqual(2, qs.count())
self.failIf(not c.name in ['Downtown Houston', 'Southside Place'])
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
# (thus, Houston and Southside place will be excluded)
qs = City.objects.filter(point__distance_gte=(pnt, D(km=7))).filter(point__distance_lte=(pnt, D(km=20)))
self.assertEqual(3, qs.count())
for c in qs:
self.failIf(not c.name in ['Pearland', 'Bellaire', 'West University Place'])
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(DistanceTest))
return s

View File

@ -8,18 +8,22 @@ class Country(models.Model):
name = models.CharField(max_length=30)
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
objects = models.GeoManager()
def __unicode__(self): return self.name
class City(models.Model):
name = models.CharField(max_length=30)
point = models.PointField()
objects = models.GeoManager()
def __unicode__(self): return self.name
class State(models.Model):
name = models.CharField(max_length=30)
poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here.
objects = models.GeoManager()
def __unicode__(self): return self.name
class Feature(models.Model):
name = models.CharField(max_length=20)
geom = models.GeometryField()
objects = models.GeoManager()
def __unicode__(self): return self.name

View File

@ -0,0 +1,8 @@
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Houston', SDO_GEOMETRY('POINT (-95.363151 29.763374)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Dallas', SDO_GEOMETRY('POINT (-96.801611 32.782057)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Oklahoma City', SDO_GEOMETRY('POINT (-97.521157 34.464642)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Wellington', SDO_GEOMETRY('POINT (174.783117 -41.315268)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Pueblo', SDO_GEOMETRY('POINT (-104.609252 38.255001)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Lawrence', SDO_GEOMETRY('POINT (-95.235060 38.971823)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Chicago', SDO_GEOMETRY('POINT (-87.650175 41.850385)', 4326));
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Victoria', SDO_GEOMETRY('POINT (-123.305196 48.462611)', 4326));

View File

@ -2,12 +2,20 @@ import os, unittest
from models import Country, City, State, Feature
from django.contrib.gis import gdal
from django.contrib.gis.geos import *
from django.contrib.gis.measure import Distance
from django.contrib.gis.tests.utils import no_oracle, no_postgis, oracle, postgis
# TODO: Some tests depend on the success/failure of previous tests, these should
# be decoupled. This flag is an artifact of this problem, and makes debugging easier;
# specifically, the DISABLE flag will disables all tests, allowing problem tests to
# be examined individually.
DISABLE = False
class GeoModelTest(unittest.TestCase):
def test01_initial_sql(self):
"Testing geographic initial SQL."
if DISABLE: return
if oracle:
# Oracle doesn't allow strings longer than 4000 characters
# in SQL files, and I'm stumped on how to use Oracle BFILE's
@ -31,10 +39,15 @@ class GeoModelTest(unittest.TestCase):
# Ensuring that data was loaded from initial SQL.
self.assertEqual(2, Country.objects.count())
self.assertEqual(8, City.objects.count())
self.assertEqual(3, State.objects.count())
# Oracle cannot handle NULL geometry values w/certain queries.
if oracle: n_state = 2
else: n_state = 3
self.assertEqual(n_state, State.objects.count())
def test02_proxy(self):
"Testing Lazy-Geometry support (using the GeometryProxy)."
if DISABLE: return
#### Testing on a Point
pnt = Point(0, 0)
nullcity = City(name='NullCity', point=pnt)
@ -104,32 +117,41 @@ class GeoModelTest(unittest.TestCase):
@no_oracle # Oracle does not support KML.
def test03a_kml(self):
"Testing KML output from the database using GeoManager.kml()."
if DISABLE: return
# Should throw a TypeError when trying to obtain KML from a
# non-geometry field.
qs = City.objects.all()
self.assertRaises(TypeError, qs.kml, 'name')
# Ensuring the KML is as expected.
ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
for ptown in [ptown1, ptown2]:
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
def test03b_gml(self):
"Testing GML output from the database using GeoManager.gml()."
if DISABLE: return
# Should throw a TypeError when tyring to obtain GML from a
# non-geometry field.
qs = City.objects.all()
self.assertRaises(TypeError, qs.gml, 'name')
ptown = City.objects.gml('point', precision=9).get(name='Pueblo')
ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
if oracle:
# No precision parameter for Oracle :-/
import re
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925199\d+,38.25500\d+ </gml:coordinates></gml:Point>')
for ptown in [ptown1, ptown2]:
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
else:
for ptown in [ptown1, ptown2]:
self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
def test04_transform(self):
"Testing the transform() GeoManager method."
if DISABLE: return
# Pre-transformed points for Houston and Pueblo.
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
@ -142,13 +164,31 @@ class GeoModelTest(unittest.TestCase):
self.assertAlmostEqual(htown.x, h.point.x, 8)
self.assertAlmostEqual(htown.y, h.point.y, 8)
p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
for p in [p1, p2]:
self.assertEqual(2774, p.point.srid)
self.assertAlmostEqual(ptown.x, p.point.x, 7)
self.assertAlmostEqual(ptown.y, p.point.y, 7)
def test09_disjoint(self):
"Testing the `disjoint` lookup type."
if DISABLE: return
ptown = City.objects.get(name='Pueblo')
qs1 = City.objects.filter(point__disjoint=ptown.point)
self.assertEqual(7, qs1.count())
if not postgis:
# TODO: Do NULL columns bork queries on PostGIS? The following
# error is encountered:
# psycopg2.ProgrammingError: invalid memory alloc request size 4294957297
qs2 = State.objects.filter(poly__disjoint=ptown.point)
self.assertEqual(1, qs2.count())
self.assertEqual('Kansas', qs2[0].name)
def test10_contains_contained(self):
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
if DISABLE: return
# Getting Texas, yes we were a country -- once ;)
texas = Country.objects.get(name='Texas')
@ -190,6 +230,7 @@ class GeoModelTest(unittest.TestCase):
def test11_lookup_insert_transform(self):
"Testing automatic transform for lookups and inserts."
if DISABLE: return
# San Antonio in 'WGS84' (SRID 4326)
sa_4326 = 'POINT (-98.493183 29.424170)'
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
@ -225,8 +266,12 @@ class GeoModelTest(unittest.TestCase):
self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6)
self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6)
# Oracle does not support NULL geometries in its spatial index for
# some routines (e.g., SDO_GEOM.RELATE).
@no_oracle
def test12_null_geometries(self):
"Testing NULL geometry support, and the `isnull` lookup type."
if DISABLE: return
# Querying for both NULL and Non-NULL values.
nullqs = State.objects.filter(poly__isnull=True)
validqs = State.objects.filter(poly__isnull=False)
@ -250,6 +295,7 @@ class GeoModelTest(unittest.TestCase):
@no_oracle # No specific `left` or `right` operators in Oracle.
def test13_left_right(self):
"Testing the 'left' and 'right' lookup types."
if DISABLE: return
# Left: A << B => true if xmax(A) < xmin(B)
# Right: A >> B => true if xmin(A) > xmax(B)
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
@ -285,6 +331,7 @@ class GeoModelTest(unittest.TestCase):
for c in qs: self.assertEqual(True, c.name in cities)
def test14_equals(self):
if DISABLE: return
"Testing the 'same_as' and 'equals' lookup types."
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
c1 = City.objects.get(point=pnt)
@ -292,57 +339,73 @@ class GeoModelTest(unittest.TestCase):
c3 = City.objects.get(point__equals=pnt)
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
@no_oracle # Oracle SDO_RELATE() uses a different system.
def test15_relate(self):
"Testing the 'relate' lookup type."
if DISABLE: return
# To make things more interesting, we will have our Texas reference point in
# different SRIDs.
pnt1 = fromstr('POINT (649287.0363174345111474 4177429.4494686722755432)', srid=2847)
pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847)
pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
# Testing bad argument tuples that should return a TypeError
bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')]
for args in bad_args:
try:
# Testing bad argument tuples that should return a TypeError or
# a ValueError.
bad_args = [((pnt1, 0), TypeError),
((pnt2, 'T*T***FF*', 0), ValueError),
((23, 'foo'), TypeError),
]
for args, e in bad_args:
qs = Country.objects.filter(mpoly__relate=args)
cnt = qs.count()
except TypeError:
pass
else:
self.fail('Expected a TypeError')
self.assertRaises(e, qs.count)
# 'T*T***FF*' => Contains()
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T*T***FF*')).name)
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T*T***FF*')).name)
# Relate works differently for the different backends.
if postgis:
contains_mask = 'T*T***FF*'
within_mask = 'T*F**F***'
intersects_mask = 'T********'
elif oracle:
contains_mask = 'contains'
within_mask = 'inside'
# TODO: This is not quite the same as the PostGIS mask above
intersects_mask = 'overlapbdyintersect'
# 'T*F**F***' => Within()
# Testing contains relation mask.
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, contains_mask)).name)
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, contains_mask)).name)
# Testing within relation mask.
ks = State.objects.get(name='Kansas')
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T*F**F***')).name)
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).name)
# 'T********' => Intersects()
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T********')).name)
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T********')).name)
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T********')).name)
# Testing intersection relation mask.
if not oracle:
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name)
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name)
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name)
def test16_createnull(self):
"Testing creating a model instance and the geometry being None"
if DISABLE: return
c = City()
self.assertEqual(c.point, None)
def test17_union(self):
"Testing the union() GeoManager method."
if DISABLE: return
tx = Country.objects.get(name='Texas').mpoly
# Houston, Dallas, San Antonio
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
qs = City.objects.filter(point__within=tx)
self.assertRaises(TypeError, qs.union, 'name')
u = qs.union('point')
self.assertEqual(True, union.equals_exact(u, 10)) # Going up to 10 digits of precision.
u1 = qs.union('point')
u2 = qs.union()
self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
self.assertEqual(True, union.equals_exact(u2, 10))
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.union('point'))
def test18_geometryfield(self):
"Testing GeometryField."
if DISABLE: return
f1 = Feature(name='Point', geom=Point(1, 1))
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))