mirror of
https://github.com/django/django.git
synced 2025-07-04 17:59:13 +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:
parent
cc0cc9fa08
commit
fae19f8ced
@ -26,41 +26,33 @@ from django.utils.datastructures import SortedDict
|
|||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
|
||||||
# These routines (needed by GeoManager), default to False.
|
# 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':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
# PostGIS is the spatial database, getting the rquired modules,
|
# PostGIS is the spatial database, getting the rquired modules,
|
||||||
# renaming as necessary.
|
# renaming as necessary.
|
||||||
from django.contrib.gis.db.backend.postgis import \
|
from django.contrib.gis.db.backend.postgis import \
|
||||||
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
|
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
|
||||||
create_spatial_db, get_geo_where_clause, gqn, \
|
create_spatial_db, get_geo_where_clause, \
|
||||||
ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
|
ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||||
SPATIAL_BACKEND = 'postgis'
|
SPATIAL_BACKEND = 'postgis'
|
||||||
elif settings.DATABASE_ENGINE == 'oracle':
|
elif settings.DATABASE_ENGINE == 'oracle':
|
||||||
from django.contrib.gis.db.backend.oracle import \
|
from django.contrib.gis.db.backend.oracle import \
|
||||||
OracleSpatialField as GeoBackendField, \
|
OracleSpatialField as GeoBackendField, \
|
||||||
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
||||||
create_spatial_db, get_geo_where_clause, gqn, \
|
create_spatial_db, get_geo_where_clause, \
|
||||||
ASGML, GEOM_SELECT, TRANSFORM, UNION
|
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||||
SPATIAL_BACKEND = 'oracle'
|
SPATIAL_BACKEND = 'oracle'
|
||||||
elif settings.DATABASE_ENGINE == 'mysql':
|
elif settings.DATABASE_ENGINE == 'mysql':
|
||||||
from django.contrib.gis.db.backend.mysql import \
|
from django.contrib.gis.db.backend.mysql import \
|
||||||
MySQLGeoField as GeoBackendField, \
|
MySQLGeoField as GeoBackendField, \
|
||||||
MYSQL_GIS_TERMS as GIS_TERMS, \
|
MYSQL_GIS_TERMS as GIS_TERMS, \
|
||||||
create_spatial_db, get_geo_where_clause, gqn, \
|
create_spatial_db, get_geo_where_clause, \
|
||||||
GEOM_SELECT
|
GEOM_SELECT
|
||||||
SPATIAL_BACKEND = 'mysql'
|
SPATIAL_BACKEND = 'mysql'
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
||||||
|
|
||||||
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 ####
|
#### query.py overloaded functions ####
|
||||||
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
||||||
# counterparts to support constructing SQL for geographic queries.
|
# counterparts to support constructing SQL for geographic queries.
|
||||||
@ -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
|
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
||||||
# with the get_geo_where_clause()
|
# with the get_geo_where_clause()
|
||||||
if hasattr(field, '_geom'):
|
if hasattr(field, '_geom'):
|
||||||
# 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.
|
# Getting the preparation SQL object from the field.
|
||||||
if multiple_args:
|
geo_prep = field.get_db_prep_lookup(lookup_type, value)
|
||||||
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.
|
# Getting the adapted geometry from the field.
|
||||||
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
||||||
|
|
||||||
# A GeoFieldSQL object is returned by `get_db_prep_lookup` --
|
# Substituting in the the where parameters into the geographic where
|
||||||
# getting the substitution list and the geographic parameters.
|
# clause, and extending the parameters.
|
||||||
subst_list = geo_prep.where
|
where.append(gwc % tuple(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)
|
|
||||||
params.extend(geo_prep.params)
|
params.extend(geo_prep.params)
|
||||||
else:
|
else:
|
||||||
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
||||||
|
@ -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.field import OracleSpatialField, gqn
|
||||||
from django.contrib.gis.db.backend.oracle.query import \
|
from django.contrib.gis.db.backend.oracle.query import \
|
||||||
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
|
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
|
||||||
ASGML, GEOM_SELECT, TRANSFORM, UNION
|
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||||
|
|
||||||
|
@ -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.geos import GEOSGeometry
|
||||||
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
|
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.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.
|
# Quotename & geographic quotename, respectively.
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
@ -21,12 +21,12 @@ class OracleSpatialField(Field):
|
|||||||
|
|
||||||
empty_strings_allowed = False
|
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
|
Oracle Spatial backend needs to have the extent -- for projected coordinate
|
||||||
systems _you must define the extent manually_, since the coordinates are
|
systems _you must define the extent manually_, since the coordinates are
|
||||||
for geodetic systems. The `tolerance` keyword specifies the tolerance
|
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.
|
# Oracle Spatial specific keyword arguments.
|
||||||
self._extent = extent
|
self._extent = extent
|
||||||
@ -104,32 +104,32 @@ class OracleSpatialField(Field):
|
|||||||
# special case for isnull lookup
|
# special case for isnull lookup
|
||||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||||
|
|
||||||
# When the input is not a GEOS geometry, attempt to construct one
|
# Get the geometry with SRID; defaults SRID to that
|
||||||
# from the given string input.
|
# of the field if it is None
|
||||||
if isinstance(value, GEOSGeometry):
|
geom = self.get_geometry(value)
|
||||||
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)
|
|
||||||
|
|
||||||
# The adaptor will be used by psycopg2 for quoting the WKT.
|
# The adaptor will be used by psycopg2 for quoting the WKT.
|
||||||
adapt = OracleSpatialAdaptor(value)
|
adapt = OracleSpatialAdaptor(geom)
|
||||||
if srid != self._srid:
|
|
||||||
|
if geom.srid != self._srid:
|
||||||
# Adding the necessary string substitutions and parameters
|
# Adding the necessary string substitutions and parameters
|
||||||
# to perform a geometry transformation.
|
# to perform a geometry transformation.
|
||||||
return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
|
where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
|
||||||
[adapt, self._srid])
|
params = [adapt, self._srid]
|
||||||
else:
|
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:
|
else:
|
||||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class GeometryColumns(models.Model):
|
|||||||
db_table = 'USER_SDO_GEOM_METADATA'
|
db_table = 'USER_SDO_GEOM_METADATA'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def table_name_col(self):
|
def table_name_col(cls):
|
||||||
return 'table_name'
|
return 'table_name'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
@ -43,3 +43,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
|||||||
@property
|
@property
|
||||||
def wkt(self):
|
def wkt(self):
|
||||||
return self.wktext
|
return self.wktext
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def wkt_col(cls):
|
||||||
|
return 'wktext'
|
||||||
|
@ -2,25 +2,100 @@
|
|||||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||||
routine for Oracle Spatial.
|
routine for Oracle Spatial.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
|
from decimal import Decimal
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
from django.contrib.gis.measure import Distance
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
|
|
||||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
# The GML, distance, transform, and union procedures.
|
||||||
'contains' : 'SDO_CONTAINS',
|
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||||
'coveredby' : 'SDO_COVEREDBY',
|
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
|
||||||
'covers' : 'SDO_COVERS',
|
TRANSFORM = 'SDO_CS.TRANSFORM'
|
||||||
'disjoint' : 'SDO_DISJOINT',
|
UNION = 'SDO_AGGR_UNION'
|
||||||
'dwithin' : ('SDO_WITHIN_DISTANCE', float),
|
|
||||||
'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
|
class SDOOperation(object):
|
||||||
'equals' : 'SDO_EQUAL',
|
"Base class for SDO* Oracle operations."
|
||||||
'exact' : 'SDO_EQUAL',
|
|
||||||
'overlaps' : 'SDO_OVERLAPS',
|
def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
|
||||||
'same_as' : 'SDO_EQUAL',
|
beg_subst='%s(%s%s, %%s'):
|
||||||
#'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
self.lookup = lookup
|
||||||
'touches' : 'SDO_TOUCH',
|
self.subst = subst
|
||||||
'within' : 'SDO_INSIDE',
|
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.
|
# This lookup type does not require a mapping.
|
||||||
MISC_TERMS = ['isnull']
|
MISC_TERMS = ['isnull']
|
||||||
|
|
||||||
@ -43,25 +118,33 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
|||||||
if isinstance(lookup_info, tuple):
|
if isinstance(lookup_info, tuple):
|
||||||
# First element of tuple is lookup type, second element is the type
|
# First element of tuple is lookup type, second element is the type
|
||||||
# of the expected argument (e.g., str, float)
|
# 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
|
# Ensuring that a tuple _value_ was passed in from the user
|
||||||
if not isinstance(value, tuple) or len(value) != 2:
|
if not isinstance(value, tuple):
|
||||||
raise TypeError('2-element tuple required for %s lookup type.' % lookup_type)
|
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.
|
# Ensuring the argument type matches what we expect.
|
||||||
if not isinstance(value[1], arg_type):
|
if not isinstance(value[1], arg_type):
|
||||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||||
|
|
||||||
if func == 'dwithin':
|
if lookup_type == 'relate':
|
||||||
# TODO: test and consider adding different distance options.
|
# The SDORelate class handles construction for these queries, and verifies
|
||||||
return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
|
# 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:
|
else:
|
||||||
return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
|
return sdo_op.as_sql(table_prefix, field_name)
|
||||||
else:
|
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'
|
# 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
|
# Handling 'isnull' lookup type
|
||||||
if lookup_type == 'isnull':
|
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))
|
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
|
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
|
||||||
# from WKT than SDO_GEOMETRY(...) strings ;)
|
# from WKT than SDO_GEOMETRY(...) strings ;)
|
||||||
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
||||||
|
@ -6,4 +6,4 @@ from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn
|
|||||||
from django.contrib.gis.db.backend.postgis.query import \
|
from django.contrib.gis.db.backend.postgis.query import \
|
||||||
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
|
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
|
||||||
|
@ -7,11 +7,11 @@ from psycopg2 import Binary
|
|||||||
from psycopg2.extensions import ISQLQuote
|
from psycopg2.extensions import ISQLQuote
|
||||||
|
|
||||||
class PostGISAdaptor(object):
|
class PostGISAdaptor(object):
|
||||||
def __init__(self, geom, srid):
|
def __init__(self, geom):
|
||||||
"Initializes on the geometry and the SRID."
|
"Initializes on the geometry and the SRID."
|
||||||
# Getting the WKB and the SRID
|
# Getting the WKB and the SRID
|
||||||
self.wkb = geom.wkb
|
self.wkb = geom.wkb
|
||||||
self.srid = srid
|
self.srid = geom.srid
|
||||||
|
|
||||||
def __conform__(self, proto):
|
def __conform__(self, proto):
|
||||||
# Does the given protocol conform to what Psycopg2 expects?
|
# Does the given protocol conform to what Psycopg2 expects?
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
from types import StringType, UnicodeType
|
from types import UnicodeType
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models.fields import Field # Django base Field class
|
from django.db.models.fields import Field # Django base Field class
|
||||||
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
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.util import get_srid, GeoFieldSQL
|
||||||
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
||||||
from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
|
from django.contrib.gis.db.backend.postgis.query import \
|
||||||
from psycopg2 import Binary
|
DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
|
||||||
|
|
||||||
# Quotename & geographic quotename, respectively
|
# Quotename & geographic quotename, respectively
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
def gqn(value):
|
def gqn(value):
|
||||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
if isinstance(value, basestring):
|
||||||
return "'%s'" % value
|
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
||||||
|
return "'%s'" % value
|
||||||
|
else:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
class PostGISField(Field):
|
class PostGISField(Field):
|
||||||
|
"""
|
||||||
|
The backend-specific geographic field for PostGIS.
|
||||||
|
"""
|
||||||
|
|
||||||
def _add_geom(self, style, db_table):
|
def _add_geom(self, style, db_table):
|
||||||
"""
|
"""
|
||||||
Constructs the addition of the geometry to the table using the
|
Constructs the addition of the geometry to the table using the
|
||||||
@ -92,53 +99,44 @@ class PostGISField(Field):
|
|||||||
"""
|
"""
|
||||||
if lookup_type in POSTGIS_TERMS:
|
if lookup_type in POSTGIS_TERMS:
|
||||||
# special case for isnull lookup
|
# special case for isnull lookup
|
||||||
if lookup_type == 'isnull':
|
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||||
return GeoFieldSQL([], [value])
|
|
||||||
|
|
||||||
# When the input is not a GEOS geometry, attempt to construct one
|
# Get the geometry with SRID; defaults SRID to
|
||||||
# from the given string input.
|
# that of the field if it is None.
|
||||||
if isinstance(value, GEOSGeometry):
|
geom = self.get_geometry(value)
|
||||||
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)
|
|
||||||
|
|
||||||
# The adaptor will be used by psycopg2 for quoting the WKB.
|
# 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
|
# Adding the necessary string substitutions and parameters
|
||||||
# to perform a geometry transformation.
|
# to perform a geometry transformation.
|
||||||
return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
|
where = ['%s(%%s,%%s)' % TRANSFORM]
|
||||||
[adapt, self._srid])
|
params = [adapt, self._srid]
|
||||||
else:
|
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:
|
else:
|
||||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||||
|
|
||||||
|
|
||||||
def get_db_prep_save(self, value):
|
def get_db_prep_save(self, value):
|
||||||
"Prepares the value for saving in the database."
|
"Prepares the value for saving in the database."
|
||||||
if not bool(value): return None
|
if not bool(value): return None
|
||||||
if isinstance(value, GEOSGeometry):
|
if isinstance(value, GEOSGeometry):
|
||||||
return PostGISAdaptor(value, value.srid)
|
return PostGISAdaptor(value)
|
||||||
else:
|
else:
|
||||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
|
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):
|
def get_placeholder(self, value):
|
||||||
"""
|
"""
|
||||||
Provides a proper substitution value for Geometries that are not in the
|
Provides a proper substitution value for Geometries that are not in the
|
||||||
|
@ -16,17 +16,17 @@ class GeometryColumns(models.Model):
|
|||||||
"""
|
"""
|
||||||
f_table_catalog = models.CharField(maxlength=256)
|
f_table_catalog = models.CharField(maxlength=256)
|
||||||
f_table_schema = 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)
|
f_geometry_column = models.CharField(maxlength=256)
|
||||||
coord_dimension = models.IntegerField()
|
coord_dimension = models.IntegerField()
|
||||||
srid = models.IntegerField()
|
srid = models.IntegerField(primary_key=True)
|
||||||
type = models.CharField(maxlength=30)
|
type = models.CharField(maxlength=30)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'geometry_columns'
|
db_table = 'geometry_columns'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def table_name_col(self):
|
def table_name_col(cls):
|
||||||
"Class method for returning the table name column for this model."
|
"Class method for returning the table name column for this model."
|
||||||
return 'f_table_name'
|
return 'f_table_name'
|
||||||
|
|
||||||
@ -52,3 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
|||||||
@property
|
@property
|
||||||
def wkt(self):
|
def wkt(self):
|
||||||
return self.srtext
|
return self.srtext
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def wkt_col(cls):
|
||||||
|
return 'srtext'
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
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 PostGIS.
|
routine for PostGIS.
|
||||||
"""
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
from django.db import connection
|
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 django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
|
||||||
from types import StringType, UnicodeType
|
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
|
|
||||||
# Getting the PostGIS version information
|
# Getting the PostGIS version information
|
||||||
@ -62,16 +63,38 @@ POSTGIS_OPERATORS = {
|
|||||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||||
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||||
# means that 'ST_' is prefixes geometry function names.
|
# 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 = ''
|
||||||
GEOM_FUNC_PREFIX = 'ST_'
|
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:
|
else:
|
||||||
GEOM_FUNC_PREFIX = ''
|
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
|
||||||
|
|
||||||
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
|
# 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
|
# first before calling the more computationally expensive GEOS routines (called
|
||||||
# "inline index magic"):
|
# "inline index magic"):
|
||||||
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
|
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
|
||||||
# 'covers'.
|
# 'covers'.
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS = {
|
POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||||
'equals' : 'Equals',
|
'equals' : 'Equals',
|
||||||
'disjoint' : 'Disjoint',
|
'disjoint' : 'Disjoint',
|
||||||
@ -81,25 +104,36 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
|
|||||||
'overlaps' : 'Overlaps',
|
'overlaps' : 'Overlaps',
|
||||||
'contains' : 'Contains',
|
'contains' : 'Contains',
|
||||||
'intersects' : 'Intersects',
|
'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_':
|
if GEOM_FUNC_PREFIX == 'ST_':
|
||||||
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
|
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
|
||||||
for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
||||||
if isinstance(func, tuple):
|
if isinstance(f, tuple):
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1])
|
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
|
||||||
else:
|
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+
|
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||||
{'dwithin' : ('ST_DWithin', float),
|
{'dwithin' : ('ST_DWithin', dtypes),
|
||||||
'coveredby' : 'ST_CoveredBy',
|
'coveredby' : 'ST_CoveredBy',
|
||||||
'covers' : 'ST_Covers',
|
'covers' : 'ST_Covers',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||||
|
|
||||||
# Any other lookup types that do not require a mapping.
|
# Any other lookup types that do not require a mapping.
|
||||||
MISC_TERMS = ['isnull']
|
MISC_TERMS = ['isnull']
|
||||||
|
|
||||||
@ -139,14 +173,20 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
|||||||
func, arg_type = lookup_info
|
func, arg_type = lookup_info
|
||||||
|
|
||||||
# Ensuring that a tuple _value_ was passed in from the user
|
# Ensuring that a tuple _value_ was passed in from the user
|
||||||
if not isinstance(value, tuple) or len(value) != 2:
|
if not isinstance(value, tuple):
|
||||||
raise TypeError('2-element tuple required for `%s` lookup type.' % lookup_type)
|
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.
|
# Ensuring the argument type matches what we expect.
|
||||||
if not isinstance(value[1], arg_type):
|
if not isinstance(value[1], arg_type):
|
||||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||||
|
|
||||||
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
|
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:
|
else:
|
||||||
# Returning the SQL necessary for the geometry function call. For example:
|
# Returning the SQL necessary for the geometry function call. For example:
|
||||||
# ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..))
|
# ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..))
|
||||||
@ -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))
|
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
|
# Custom selection not needed for PostGIS since GEOS geometries may be
|
||||||
# instantiated directly from the HEXEWKB returned by default. If
|
# instantiated directly from the HEXEWKB returned by default. If
|
||||||
# WKT is needed for some reason in the future, this value may be changed,
|
# WKT is needed for some reason in the future, this value may be changed,
|
||||||
|
@ -7,6 +7,9 @@ class GeoFieldSQL(object):
|
|||||||
self.where = where
|
self.where = where
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.where[0] % tuple(self.params)
|
||||||
|
|
||||||
def get_srid(field, geom):
|
def get_srid(field, geom):
|
||||||
"""
|
"""
|
||||||
Gets the SRID depending on the value of the SRID setting of the field
|
Gets the SRID depending on the value of the SRID setting of the field
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from django.conf import settings
|
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.backend import GeoBackendField # these depend on the spatial database backend.
|
||||||
from django.contrib.gis.db.models.proxy import GeometryProxy
|
from django.contrib.gis.db.models.proxy import GeometryProxy
|
||||||
|
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||||
|
from django.contrib.gis.measure import Distance
|
||||||
from django.contrib.gis.oldforms import WKTField
|
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.
|
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
|
||||||
class GeometryField(GeoBackendField):
|
class GeometryField(GeoBackendField):
|
||||||
@ -11,24 +20,101 @@ class GeometryField(GeoBackendField):
|
|||||||
# The OpenGIS Geometry name.
|
# The OpenGIS Geometry name.
|
||||||
_geom = 'GEOMETRY'
|
_geom = 'GEOMETRY'
|
||||||
|
|
||||||
def __init__(self, srid=4326, index=True, dim=2, **kwargs):
|
def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
|
||||||
"""The initialization function for geometry fields. Takes the following
|
"""
|
||||||
|
The initialization function for geometry fields. Takes the following
|
||||||
as keyword arguments:
|
as keyword arguments:
|
||||||
|
|
||||||
srid - The spatial reference system identifier. An OGC standard.
|
srid:
|
||||||
Defaults to 4326 (WGS84)
|
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:
|
||||||
Set this instead of 'db_index' for geographic fields since index
|
Indicates whether to create a spatial index. Defaults to True.
|
||||||
creation is different for geometry columns.
|
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
|
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
|
self._dim = dim
|
||||||
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
||||||
|
|
||||||
|
### Routines specific to GeometryField ###
|
||||||
|
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):
|
def contribute_to_class(self, cls, name):
|
||||||
super(GeometryField, self).contribute_to_class(cls, name)
|
super(GeometryField, self).contribute_to_class(cls, name)
|
||||||
|
|
||||||
|
@ -7,6 +7,9 @@ class GeoManager(Manager):
|
|||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
return GeoQuerySet(model=self.model)
|
return GeoQuerySet(model=self.model)
|
||||||
|
|
||||||
|
def distance(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().distance(*args, **kwargs)
|
||||||
|
|
||||||
def gml(self, *args, **kwargs):
|
def gml(self, *args, **kwargs):
|
||||||
return self.get_query_set().gml(*args, **kwargs)
|
return self.get_query_set().gml(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -6,9 +6,13 @@ from django.db.models.fields import FieldDoesNotExist
|
|||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
# parse_lookup depends on the spatial database backend.
|
# parse_lookup depends on the spatial database backend.
|
||||||
from django.contrib.gis.db.backend import parse_lookup, 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
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
|
||||||
|
# Flag indicating whether the backend is Oracle.
|
||||||
|
oracle = SPATIAL_BACKEND == 'oracle'
|
||||||
|
|
||||||
class GeoQ(Q):
|
class GeoQ(Q):
|
||||||
"Geographical query encapsulation object."
|
"Geographical query encapsulation object."
|
||||||
|
|
||||||
@ -28,11 +32,23 @@ class GeoQuerySet(QuerySet):
|
|||||||
|
|
||||||
# For replacement fields in the SELECT.
|
# For replacement fields in the SELECT.
|
||||||
self._custom_select = {}
|
self._custom_select = {}
|
||||||
|
self._ewkt = None
|
||||||
|
|
||||||
# If GEOM_SELECT is defined in the backend, then it will be used
|
# If GEOM_SELECT is defined in the backend, then it will be used
|
||||||
# for the selection format of the geometry column.
|
# for the selection format of the geometry column.
|
||||||
if GEOM_SELECT: self._geo_fmt = GEOM_SELECT
|
if GEOM_SELECT:
|
||||||
else: self._geo_fmt = '%s'
|
#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):
|
def _filter_or_exclude(self, mapper, *args, **kwargs):
|
||||||
# mapper is a callable used to transform Q objects,
|
# mapper is a callable used to transform Q objects,
|
||||||
@ -59,15 +75,23 @@ class GeoQuerySet(QuerySet):
|
|||||||
select = []
|
select = []
|
||||||
|
|
||||||
# This is the only component of this routine that is customized for the
|
# This is the only component of this routine that is customized for the
|
||||||
# GeoQuerySet. Specifically, this allows operations to be done on fields
|
# GeoQuerySet. Specifically, this allows operations to be done on fields
|
||||||
# in the SELECT, overriding their values -- this is different from using
|
# in the SELECT, overriding their values -- this is different from using
|
||||||
# QuerySet.extra(select=foo) because extra() adds an an _additional_
|
# QuerySet.extra(select=foo) because extra() adds an an _additional_
|
||||||
# field to be selected. Used in returning transformed geometries, and
|
# field to be selected. Used in returning transformed geometries, and
|
||||||
# handling the selection of native database geometry formats.
|
# handling the selection of native database geometry formats.
|
||||||
for f in opts.fields:
|
for f in opts.fields:
|
||||||
# Getting the selection format string.
|
# Getting the selection format string.
|
||||||
if hasattr(f, '_geom'): sel_fmt = self._geo_fmt
|
if hasattr(f, '_geom'):
|
||||||
else: sel_fmt = '%s'
|
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
|
# Getting the field selection substitution string
|
||||||
if f.column in self._custom_select:
|
if f.column in self._custom_select:
|
||||||
@ -147,7 +171,7 @@ class GeoQuerySet(QuerySet):
|
|||||||
sql.append("ORDER BY " + ", ".join(order_by))
|
sql.append("ORDER BY " + ", ".join(order_by))
|
||||||
|
|
||||||
# LIMIT and OFFSET clauses
|
# LIMIT and OFFSET clauses
|
||||||
if SPATIAL_BACKEND != 'oracle':
|
if not oracle:
|
||||||
if self._limit is not None:
|
if self._limit is not None:
|
||||||
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
|
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
|
||||||
else:
|
else:
|
||||||
@ -206,6 +230,7 @@ class GeoQuerySet(QuerySet):
|
|||||||
def _clone(self, klass=None, **kwargs):
|
def _clone(self, klass=None, **kwargs):
|
||||||
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
|
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
|
||||||
c._custom_select = self._custom_select
|
c._custom_select = self._custom_select
|
||||||
|
c._ewkt = self._ewkt
|
||||||
return c
|
return c
|
||||||
|
|
||||||
#### Methods specific to the GeoQuerySet ####
|
#### Methods specific to the GeoQuerySet ####
|
||||||
@ -227,7 +252,60 @@ class GeoQuerySet(QuerySet):
|
|||||||
else:
|
else:
|
||||||
return False
|
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
|
Returns GML representation of the given field in a `gml` attribute
|
||||||
on each element of the GeoQuerySet.
|
on each element of the GeoQuerySet.
|
||||||
@ -236,12 +314,16 @@ class GeoQuerySet(QuerySet):
|
|||||||
if not ASGML:
|
if not ASGML:
|
||||||
raise ImproperlyConfigured('AsGML() stored procedure not available.')
|
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)
|
field_col = self._geo_column(field_name)
|
||||||
if not field_col:
|
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)}
|
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
|
||||||
else:
|
else:
|
||||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
|
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.
|
# Adding GML function call to SELECT part of the SQL.
|
||||||
return self.extra(select=gml_select)
|
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`
|
Returns KML representation of the given field name in a `kml`
|
||||||
attribute on each element of the GeoQuerySet.
|
attribute on each element of the GeoQuerySet.
|
||||||
@ -258,7 +340,10 @@ class GeoQuerySet(QuerySet):
|
|||||||
if not ASKML:
|
if not ASKML:
|
||||||
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
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)
|
field_col = self._geo_column(field_name)
|
||||||
if not field_col:
|
if not field_col:
|
||||||
raise TypeError('KML output only available on GeometryFields.')
|
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.
|
# 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, 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
|
Transforms the given geometry field to the given SRID. If no SRID is
|
||||||
provided, the transformation will default to using 4326 (WGS84).
|
provided, the transformation will default to using 4326 (WGS84).
|
||||||
"""
|
"""
|
||||||
# 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)
|
field = self.model._meta.get_field(field_name)
|
||||||
if not isinstance(field, GeometryField):
|
if not isinstance(field, GeometryField):
|
||||||
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
|
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))
|
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
|
# Setting the key for the field's column with the custom SELECT SQL to
|
||||||
# override the geometry column returned from the database.
|
# override the geometry column returned from the database.
|
||||||
if SPATIAL_BACKEND == 'oracle':
|
if oracle:
|
||||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
|
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
|
||||||
|
self._ewkt = srid
|
||||||
else:
|
else:
|
||||||
custom_sel = '(%s(%s, %s)) AS %s' % \
|
custom_sel = '(%s(%s, %s)) AS %s' % \
|
||||||
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
|
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
|
||||||
self._custom_select[field.column] = custom_sel
|
self._custom_select[field.column] = custom_sel
|
||||||
return self._clone()
|
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
|
Performs an aggregate union on the given geometry field. Returns
|
||||||
None if the GeoQuerySet is empty. The `tolerance` keyword is for
|
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.')
|
raise ImproperlyConfigured('Union stored procedure not available.')
|
||||||
|
|
||||||
# Getting the geographic field column
|
# Getting the geographic field column
|
||||||
|
if not field_name:
|
||||||
|
field_name = self._get_geofield()
|
||||||
|
|
||||||
field_col = self._geo_column(field_name)
|
field_col = self._geo_column(field_name)
|
||||||
if not field_col:
|
if not field_col:
|
||||||
raise TypeError('Aggregate Union only available on GeometryFields.')
|
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
|
# Replacing the select with a call to the ST_Union stored procedure
|
||||||
# on the geographic field column.
|
# on the geographic field column.
|
||||||
if SPATIAL_BACKEND == 'oracle':
|
if oracle:
|
||||||
union_sql = 'SELECT %s' % self._geo_fmt
|
union_sql = 'SELECT %s' % self._geo_fmt
|
||||||
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
|
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
|
||||||
union_sql += sql
|
union_sql += sql
|
||||||
@ -323,7 +421,7 @@ class GeoQuerySet(QuerySet):
|
|||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute(union_sql, params)
|
cursor.execute(union_sql, params)
|
||||||
|
|
||||||
if SPATIAL_BACKEND == 'oracle':
|
if oracle:
|
||||||
# On Oracle have to read out WKT from CLOB first.
|
# On Oracle have to read out WKT from CLOB first.
|
||||||
clob = cursor.fetchone()[0]
|
clob = cursor.fetchone()[0]
|
||||||
if clob: u = clob.read()
|
if clob: u = clob.read()
|
||||||
|
@ -10,17 +10,24 @@ from django.contrib.gis.gdal import HAS_GDAL
|
|||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
from django.contrib.gis.gdal import SpatialReference
|
from django.contrib.gis.gdal import SpatialReference
|
||||||
|
|
||||||
# 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):
|
class SpatialRefSysMixin(object):
|
||||||
"""
|
"""
|
||||||
The SpatialRefSysMixin is a class used by the database-dependent
|
The SpatialRefSysMixin is a class used by the database-dependent
|
||||||
SpatialRefSys objects to reduce redundnant code.
|
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+),')
|
||||||
|
|
||||||
|
# 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
|
@property
|
||||||
def srs(self):
|
def srs(self):
|
||||||
"""
|
"""
|
||||||
@ -53,7 +60,7 @@ class SpatialRefSysMixin(object):
|
|||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
return self.srs.ellipsoid
|
return self.srs.ellipsoid
|
||||||
else:
|
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')))
|
if m: return (float(m.group('major')), float(m.group('flattening')))
|
||||||
else: return None
|
else: return None
|
||||||
|
|
||||||
@ -75,37 +82,93 @@ class SpatialRefSysMixin(object):
|
|||||||
@property
|
@property
|
||||||
def projected(self):
|
def projected(self):
|
||||||
"Is this Spatial Reference projected?"
|
"Is this Spatial Reference projected?"
|
||||||
return self.srs.projected
|
if HAS_GDAL:
|
||||||
|
return self.srs.projected
|
||||||
|
else:
|
||||||
|
return self.wkt.startswith('PROJCS')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local(self):
|
def local(self):
|
||||||
"Is this Spatial Reference local?"
|
"Is this Spatial Reference local?"
|
||||||
return self.srs.local
|
if HAS_GDAL:
|
||||||
|
return self.srs.local
|
||||||
|
else:
|
||||||
|
return self.wkt.startswith('LOCAL_CS')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def geographic(self):
|
def geographic(self):
|
||||||
"Is this Spatial Reference geographic?"
|
"Is this Spatial Reference geographic?"
|
||||||
return self.srs.geographic
|
if HAS_GDAL:
|
||||||
|
return self.srs.geographic
|
||||||
|
else:
|
||||||
|
return self.wkt.startswith('GEOGCS')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def linear_name(self):
|
def linear_name(self):
|
||||||
"Returns the linear units name."
|
"Returns the linear units name."
|
||||||
return self.srs.linear_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
|
@property
|
||||||
def linear_units(self):
|
def linear_units(self):
|
||||||
"Returns the linear units."
|
"Returns the linear units."
|
||||||
return self.srs.linear_units
|
if HAS_GDAL:
|
||||||
|
return self.srs.linear_units
|
||||||
@property
|
elif self.geographic:
|
||||||
def angular_units(self):
|
return None
|
||||||
"Returns the angular units."
|
else:
|
||||||
return self.srs.angular_units
|
m = self.units_regex.match(self.wkt)
|
||||||
|
return m.group('unit')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def angular_name(self):
|
def angular_name(self):
|
||||||
"Returns the name of the angular units."
|
"Returns the name of the angular units."
|
||||||
return self.srs.angular_name
|
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):
|
def __unicode__(self):
|
||||||
"""
|
"""
|
||||||
@ -115,7 +178,7 @@ class SpatialRefSysMixin(object):
|
|||||||
try:
|
try:
|
||||||
return unicode(self.srs)
|
return unicode(self.srs)
|
||||||
except:
|
except:
|
||||||
return unicode(self.srtext)
|
return unicode(self.wkt)
|
||||||
|
|
||||||
# The SpatialRefSys and GeometryColumns models
|
# The SpatialRefSys and GeometryColumns models
|
||||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
|
@ -3,9 +3,9 @@ from copy import copy
|
|||||||
from unittest import TestSuite, TextTestRunner
|
from unittest import TestSuite, TextTestRunner
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
try:
|
try:
|
||||||
from django.contrib.gis.tests.utils import mysql
|
from django.contrib.gis.tests.utils import mysql, oracle
|
||||||
except:
|
except:
|
||||||
mysql = False
|
mysql, oracle = (False, False)
|
||||||
|
|
||||||
# Tests that require use of a spatial database (e.g., creation of models)
|
# Tests that require use of a spatial database (e.g., creation of models)
|
||||||
test_models = ['geoapp']
|
test_models = ['geoapp']
|
||||||
@ -16,7 +16,12 @@ test_suite_names = [
|
|||||||
'test_measure',
|
'test_measure',
|
||||||
]
|
]
|
||||||
if HAS_GDAL:
|
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_suite_names += [
|
||||||
'test_gdal_driver',
|
'test_gdal_driver',
|
||||||
'test_gdal_ds',
|
'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`
|
(3) Start this database `pg_ctl -D /path/to/user/db start`
|
||||||
|
|
||||||
On Windows platforms simply use the pgAdmin III utility to add superuser
|
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.
|
Make sure your settings.py matches the settings of the user database.
|
||||||
For example, set the same port number (`DATABASE_PORT=5433`).
|
For example, set the same port number (`DATABASE_PORT=5433`).
|
||||||
|
0
django/contrib/gis/tests/distapp/__init__.py
Normal file
0
django/contrib/gis/tests/distapp/__init__.py
Normal file
BIN
django/contrib/gis/tests/distapp/cities/cities.dbf
Normal file
BIN
django/contrib/gis/tests/distapp/cities/cities.dbf
Normal file
Binary file not shown.
1
django/contrib/gis/tests/distapp/cities/cities.prj
Normal file
1
django/contrib/gis/tests/distapp/cities/cities.prj
Normal 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]]
|
BIN
django/contrib/gis/tests/distapp/cities/cities.shp
Normal file
BIN
django/contrib/gis/tests/distapp/cities/cities.shp
Normal file
Binary file not shown.
BIN
django/contrib/gis/tests/distapp/cities/cities.shx
Normal file
BIN
django/contrib/gis/tests/distapp/cities/cities.shx
Normal file
Binary file not shown.
20
django/contrib/gis/tests/distapp/models.py
Normal file
20
django/contrib/gis/tests/distapp/models.py
Normal 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',
|
||||||
|
# }
|
86
django/contrib/gis/tests/distapp/tests.py
Normal file
86
django/contrib/gis/tests/distapp/tests.py
Normal 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
|
@ -8,18 +8,22 @@ class Country(models.Model):
|
|||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
|
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
class City(models.Model):
|
class City(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
point = models.PointField()
|
point = models.PointField()
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
class State(models.Model):
|
class State(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here.
|
poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here.
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
class Feature(models.Model):
|
class Feature(models.Model):
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=20)
|
||||||
geom = models.GeometryField()
|
geom = models.GeometryField()
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
8
django/contrib/gis/tests/geoapp/sql/city.oracle.sql
Normal file
8
django/contrib/gis/tests/geoapp/sql/city.oracle.sql
Normal 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));
|
@ -2,12 +2,20 @@ import os, unittest
|
|||||||
from models import Country, City, State, Feature
|
from models import Country, City, State, Feature
|
||||||
from django.contrib.gis import gdal
|
from django.contrib.gis import gdal
|
||||||
from django.contrib.gis.geos import *
|
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
|
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):
|
class GeoModelTest(unittest.TestCase):
|
||||||
|
|
||||||
def test01_initial_sql(self):
|
def test01_initial_sql(self):
|
||||||
"Testing geographic initial SQL."
|
"Testing geographic initial SQL."
|
||||||
|
if DISABLE: return
|
||||||
if oracle:
|
if oracle:
|
||||||
# Oracle doesn't allow strings longer than 4000 characters
|
# Oracle doesn't allow strings longer than 4000 characters
|
||||||
# in SQL files, and I'm stumped on how to use Oracle BFILE's
|
# 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.
|
# Ensuring that data was loaded from initial SQL.
|
||||||
self.assertEqual(2, Country.objects.count())
|
self.assertEqual(2, Country.objects.count())
|
||||||
self.assertEqual(8, City.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):
|
def test02_proxy(self):
|
||||||
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
||||||
|
if DISABLE: return
|
||||||
#### Testing on a Point
|
#### Testing on a Point
|
||||||
pnt = Point(0, 0)
|
pnt = Point(0, 0)
|
||||||
nullcity = City(name='NullCity', point=pnt)
|
nullcity = City(name='NullCity', point=pnt)
|
||||||
@ -104,32 +117,41 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
@no_oracle # Oracle does not support KML.
|
@no_oracle # Oracle does not support KML.
|
||||||
def test03a_kml(self):
|
def test03a_kml(self):
|
||||||
"Testing KML output from the database using GeoManager.kml()."
|
"Testing KML output from the database using GeoManager.kml()."
|
||||||
|
if DISABLE: return
|
||||||
# Should throw a TypeError when trying to obtain KML from a
|
# Should throw a TypeError when trying to obtain KML from a
|
||||||
# non-geometry field.
|
# non-geometry field.
|
||||||
qs = City.objects.all()
|
qs = City.objects.all()
|
||||||
self.assertRaises(TypeError, qs.kml, 'name')
|
self.assertRaises(TypeError, qs.kml, 'name')
|
||||||
|
|
||||||
# Ensuring the KML is as expected.
|
# 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')
|
||||||
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
|
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):
|
def test03b_gml(self):
|
||||||
"Testing GML output from the database using GeoManager.gml()."
|
"Testing GML output from the database using GeoManager.gml()."
|
||||||
|
if DISABLE: return
|
||||||
# Should throw a TypeError when tyring to obtain GML from a
|
# Should throw a TypeError when tyring to obtain GML from a
|
||||||
# non-geometry field.
|
# non-geometry field.
|
||||||
qs = City.objects.all()
|
qs = City.objects.all()
|
||||||
self.assertRaises(TypeError, qs.gml, 'name')
|
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:
|
if oracle:
|
||||||
# No precision parameter for Oracle :-/
|
# No precision parameter for Oracle :-/
|
||||||
import re
|
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>')
|
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>')
|
||||||
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
|
for ptown in [ptown1, ptown2]:
|
||||||
|
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
|
||||||
else:
|
else:
|
||||||
self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
|
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):
|
def test04_transform(self):
|
||||||
"Testing the transform() GeoManager method."
|
"Testing the transform() GeoManager method."
|
||||||
|
if DISABLE: return
|
||||||
# Pre-transformed points for Houston and Pueblo.
|
# Pre-transformed points for Houston and Pueblo.
|
||||||
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
|
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
|
||||||
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
|
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.x, h.point.x, 8)
|
||||||
self.assertAlmostEqual(htown.y, h.point.y, 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')
|
||||||
self.assertEqual(2774, p.point.srid)
|
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
|
||||||
self.assertAlmostEqual(ptown.x, p.point.x, 7)
|
for p in [p1, p2]:
|
||||||
self.assertAlmostEqual(ptown.y, p.point.y, 7)
|
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):
|
def test10_contains_contained(self):
|
||||||
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
|
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
|
||||||
|
if DISABLE: return
|
||||||
# Getting Texas, yes we were a country -- once ;)
|
# Getting Texas, yes we were a country -- once ;)
|
||||||
texas = Country.objects.get(name='Texas')
|
texas = Country.objects.get(name='Texas')
|
||||||
|
|
||||||
@ -190,6 +230,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test11_lookup_insert_transform(self):
|
def test11_lookup_insert_transform(self):
|
||||||
"Testing automatic transform for lookups and inserts."
|
"Testing automatic transform for lookups and inserts."
|
||||||
|
if DISABLE: return
|
||||||
# San Antonio in 'WGS84' (SRID 4326)
|
# San Antonio in 'WGS84' (SRID 4326)
|
||||||
sa_4326 = 'POINT (-98.493183 29.424170)'
|
sa_4326 = 'POINT (-98.493183 29.424170)'
|
||||||
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
|
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.x, sa.point.x, 6)
|
||||||
self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 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):
|
def test12_null_geometries(self):
|
||||||
"Testing NULL geometry support, and the `isnull` lookup type."
|
"Testing NULL geometry support, and the `isnull` lookup type."
|
||||||
|
if DISABLE: return
|
||||||
# Querying for both NULL and Non-NULL values.
|
# Querying for both NULL and Non-NULL values.
|
||||||
nullqs = State.objects.filter(poly__isnull=True)
|
nullqs = State.objects.filter(poly__isnull=True)
|
||||||
validqs = State.objects.filter(poly__isnull=False)
|
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.
|
@no_oracle # No specific `left` or `right` operators in Oracle.
|
||||||
def test13_left_right(self):
|
def test13_left_right(self):
|
||||||
"Testing the 'left' and 'right' lookup types."
|
"Testing the 'left' and 'right' lookup types."
|
||||||
|
if DISABLE: return
|
||||||
# Left: A << B => true if xmax(A) < xmin(B)
|
# Left: A << B => true if xmax(A) < xmin(B)
|
||||||
# Right: A >> B => true if xmin(A) > xmax(B)
|
# Right: A >> B => true if xmin(A) > xmax(B)
|
||||||
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
|
# 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)
|
for c in qs: self.assertEqual(True, c.name in cities)
|
||||||
|
|
||||||
def test14_equals(self):
|
def test14_equals(self):
|
||||||
|
if DISABLE: return
|
||||||
"Testing the 'same_as' and 'equals' lookup types."
|
"Testing the 'same_as' and 'equals' lookup types."
|
||||||
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
||||||
c1 = City.objects.get(point=pnt)
|
c1 = City.objects.get(point=pnt)
|
||||||
@ -292,57 +339,73 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
c3 = City.objects.get(point__equals=pnt)
|
c3 = City.objects.get(point__equals=pnt)
|
||||||
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
||||||
|
|
||||||
@no_oracle # Oracle SDO_RELATE() uses a different system.
|
|
||||||
def test15_relate(self):
|
def test15_relate(self):
|
||||||
"Testing the 'relate' lookup type."
|
"Testing the 'relate' lookup type."
|
||||||
|
if DISABLE: return
|
||||||
# To make things more interesting, we will have our Texas reference point in
|
# To make things more interesting, we will have our Texas reference point in
|
||||||
# different SRIDs.
|
# 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)
|
pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
|
||||||
|
|
||||||
# Testing bad argument tuples that should return a TypeError
|
# Testing bad argument tuples that should return a TypeError or
|
||||||
bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')]
|
# a ValueError.
|
||||||
for args in bad_args:
|
bad_args = [((pnt1, 0), TypeError),
|
||||||
try:
|
((pnt2, 'T*T***FF*', 0), ValueError),
|
||||||
qs = Country.objects.filter(mpoly__relate=args)
|
((23, 'foo'), TypeError),
|
||||||
cnt = qs.count()
|
]
|
||||||
except TypeError:
|
for args, e in bad_args:
|
||||||
pass
|
qs = Country.objects.filter(mpoly__relate=args)
|
||||||
else:
|
self.assertRaises(e, qs.count)
|
||||||
self.fail('Expected a TypeError')
|
|
||||||
|
|
||||||
# 'T*T***FF*' => Contains()
|
# Relate works differently for the different backends.
|
||||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T*T***FF*')).name)
|
if postgis:
|
||||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T*T***FF*')).name)
|
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')
|
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()
|
# Testing intersection relation mask.
|
||||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T********')).name)
|
if not oracle:
|
||||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T********')).name)
|
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name)
|
||||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T********')).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):
|
def test16_createnull(self):
|
||||||
"Testing creating a model instance and the geometry being None"
|
"Testing creating a model instance and the geometry being None"
|
||||||
|
if DISABLE: return
|
||||||
c = City()
|
c = City()
|
||||||
self.assertEqual(c.point, None)
|
self.assertEqual(c.point, None)
|
||||||
|
|
||||||
def test17_union(self):
|
def test17_union(self):
|
||||||
"Testing the union() GeoManager method."
|
"Testing the union() GeoManager method."
|
||||||
|
if DISABLE: return
|
||||||
tx = Country.objects.get(name='Texas').mpoly
|
tx = Country.objects.get(name='Texas').mpoly
|
||||||
# Houston, Dallas, San Antonio
|
# Houston, Dallas, San Antonio
|
||||||
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
||||||
qs = City.objects.filter(point__within=tx)
|
qs = City.objects.filter(point__within=tx)
|
||||||
self.assertRaises(TypeError, qs.union, 'name')
|
self.assertRaises(TypeError, qs.union, 'name')
|
||||||
u = qs.union('point')
|
u1 = qs.union('point')
|
||||||
self.assertEqual(True, union.equals_exact(u, 10)) # Going up to 10 digits of precision.
|
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')
|
qs = City.objects.filter(name='NotACity')
|
||||||
self.assertEqual(None, qs.union('point'))
|
self.assertEqual(None, qs.union('point'))
|
||||||
|
|
||||||
def test18_geometryfield(self):
|
def test18_geometryfield(self):
|
||||||
"Testing GeometryField."
|
"Testing GeometryField."
|
||||||
|
if DISABLE: return
|
||||||
f1 = Feature(name='Point', geom=Point(1, 1))
|
f1 = Feature(name='Point', geom=Point(1, 1))
|
||||||
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
|
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))))
|
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user