mirror of
https://github.com/django/django.git
synced 2025-07-04 09:49:12 +00:00
gis: Added distance querying capabilites via the distance
manager method and the distance_[gt|gte|lt|lte]
lookup types (works for both PostGIS and Oracle); improved Oracle query construction and fixed transform
issues.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6886 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
cc0cc9fa08
commit
fae19f8ced
@ -26,41 +26,33 @@ from django.utils.datastructures import SortedDict
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
# These routines (needed by GeoManager), default to False.
|
||||
ASGML, ASKML, TRANSFORM, UNION= (False, False, False, False)
|
||||
ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
|
||||
|
||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||
# PostGIS is the spatial database, getting the rquired modules,
|
||||
# renaming as necessary.
|
||||
from django.contrib.gis.db.backend.postgis import \
|
||||
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, gqn, \
|
||||
ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||
SPATIAL_BACKEND = 'postgis'
|
||||
elif settings.DATABASE_ENGINE == 'oracle':
|
||||
from django.contrib.gis.db.backend.oracle import \
|
||||
OracleSpatialField as GeoBackendField, \
|
||||
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, gqn, \
|
||||
ASGML, GEOM_SELECT, TRANSFORM, UNION
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||
SPATIAL_BACKEND = 'oracle'
|
||||
elif settings.DATABASE_ENGINE == 'mysql':
|
||||
from django.contrib.gis.db.backend.mysql import \
|
||||
MySQLGeoField as GeoBackendField, \
|
||||
MYSQL_GIS_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, gqn, \
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
GEOM_SELECT
|
||||
SPATIAL_BACKEND = 'mysql'
|
||||
else:
|
||||
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
||||
|
||||
def geo_quotename(value):
|
||||
"""
|
||||
Returns the quotation used on a given Geometry value using the geometry
|
||||
quoting from the backend (the `gqn` function).
|
||||
"""
|
||||
if isinstance(value, (StringType, UnicodeType)): return gqn(value)
|
||||
else: return str(value)
|
||||
|
||||
#### query.py overloaded functions ####
|
||||
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
||||
# counterparts to support constructing SQL for geographic queries.
|
||||
@ -287,28 +279,15 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
|
||||
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
||||
# with the get_geo_where_clause()
|
||||
if hasattr(field, '_geom'):
|
||||
# Do we have multiple arguments, e.g., `relate`, `dwithin` lookup types
|
||||
# need more than argument.
|
||||
multiple_args = isinstance(value, tuple)
|
||||
|
||||
# Getting the preparation SQL object from the field.
|
||||
if multiple_args:
|
||||
geo_prep = field.get_db_prep_lookup(lookup_type, value[0])
|
||||
else:
|
||||
geo_prep = field.get_db_prep_lookup(lookup_type, value)
|
||||
|
||||
# Getting the adapted geometry from the field.
|
||||
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
|
||||
|
||||
# A GeoFieldSQL object is returned by `get_db_prep_lookup` --
|
||||
# getting the substitution list and the geographic parameters.
|
||||
subst_list = geo_prep.where
|
||||
if multiple_args: subst_list += map(geo_quotename, value[1:])
|
||||
gwc = gwc % tuple(subst_list)
|
||||
|
||||
# Finally, appending onto the WHERE clause, and extending with
|
||||
# the additional parameters.
|
||||
where.append(gwc)
|
||||
# Substituting in the the where parameters into the geographic where
|
||||
# clause, and extending the parameters.
|
||||
where.append(gwc % tuple(geo_prep.where))
|
||||
params.extend(geo_prep.params)
|
||||
else:
|
||||
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
||||
|
@ -10,5 +10,5 @@ from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
|
||||
from django.contrib.gis.db.backend.oracle.query import \
|
||||
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
|
||||
ASGML, GEOM_SELECT, TRANSFORM, UNION
|
||||
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||
|
||||
|
@ -6,7 +6,7 @@ from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
|
||||
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
|
||||
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, TRANSFORM
|
||||
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively.
|
||||
qn = connection.ops.quote_name
|
||||
@ -21,12 +21,12 @@ class OracleSpatialField(Field):
|
||||
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.00005, **kwargs):
|
||||
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
|
||||
"""
|
||||
Oracle Spatial backend needs to have the extent -- for projected coordinate
|
||||
systems _you must define the extent manually_, since the coordinates are
|
||||
for geodetic systems. The `tolerance` keyword specifies the tolerance
|
||||
for error (in meters).
|
||||
for error (in meters), and defaults to 0.05 (5 centimeters).
|
||||
"""
|
||||
# Oracle Spatial specific keyword arguments.
|
||||
self._extent = extent
|
||||
@ -104,32 +104,32 @@ class OracleSpatialField(Field):
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||
|
||||
# When the input is not a GEOS geometry, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(value, GEOSGeometry):
|
||||
pass
|
||||
elif isinstance(value, (StringType, UnicodeType)):
|
||||
try:
|
||||
value = GEOSGeometry(value)
|
||||
except GEOSException:
|
||||
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
|
||||
else:
|
||||
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
|
||||
|
||||
# Getting the SRID of the geometry, or defaulting to that of the field if
|
||||
# it is None.
|
||||
srid = get_srid(self, value)
|
||||
# Get the geometry with SRID; defaults SRID to that
|
||||
# of the field if it is None
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# The adaptor will be used by psycopg2 for quoting the WKT.
|
||||
adapt = OracleSpatialAdaptor(value)
|
||||
if srid != self._srid:
|
||||
adapt = OracleSpatialAdaptor(geom)
|
||||
|
||||
if geom.srid != self._srid:
|
||||
# Adding the necessary string substitutions and parameters
|
||||
# to perform a geometry transformation.
|
||||
return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
|
||||
[adapt, self._srid])
|
||||
where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
|
||||
params = [adapt, self._srid]
|
||||
else:
|
||||
return GeoFieldSQL(['SDO_GEOMETRY(%%s, %s)' % srid], [adapt])
|
||||
where = ['SDO_GEOMETRY(%%s, %s)' % geom.srid]
|
||||
params = [adapt]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
|
||||
# Getting the distance parameter in the units of the field
|
||||
where += [self.get_distance(value[1])]
|
||||
elif lookup_type == 'relate':
|
||||
# No extra where parameters for SDO_RELATE queries.
|
||||
pass
|
||||
else:
|
||||
where += map(gqn, value[1:])
|
||||
return GeoFieldSQL(where, params)
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
|
@ -20,7 +20,7 @@ class GeometryColumns(models.Model):
|
||||
db_table = 'USER_SDO_GEOM_METADATA'
|
||||
|
||||
@classmethod
|
||||
def table_name_col(self):
|
||||
def table_name_col(cls):
|
||||
return 'table_name'
|
||||
|
||||
def __unicode__(self):
|
||||
@ -43,3 +43,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.wktext
|
||||
|
||||
@classmethod
|
||||
def wkt_col(cls):
|
||||
return 'wktext'
|
||||
|
@ -2,25 +2,100 @@
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
routine for Oracle Spatial.
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from django.db import connection
|
||||
from django.contrib.gis.measure import Distance
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
||||
'contains' : 'SDO_CONTAINS',
|
||||
'coveredby' : 'SDO_COVEREDBY',
|
||||
'covers' : 'SDO_COVERS',
|
||||
'disjoint' : 'SDO_DISJOINT',
|
||||
'dwithin' : ('SDO_WITHIN_DISTANCE', float),
|
||||
'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
|
||||
'equals' : 'SDO_EQUAL',
|
||||
'exact' : 'SDO_EQUAL',
|
||||
'overlaps' : 'SDO_OVERLAPS',
|
||||
'same_as' : 'SDO_EQUAL',
|
||||
#'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
||||
'touches' : 'SDO_TOUCH',
|
||||
'within' : 'SDO_INSIDE',
|
||||
# The GML, distance, transform, and union procedures.
|
||||
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
|
||||
TRANSFORM = 'SDO_CS.TRANSFORM'
|
||||
UNION = 'SDO_AGGR_UNION'
|
||||
|
||||
class SDOOperation(object):
|
||||
"Base class for SDO* Oracle operations."
|
||||
|
||||
def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
|
||||
beg_subst='%s(%s%s, %%s'):
|
||||
self.lookup = lookup
|
||||
self.subst = subst
|
||||
self.operator = operator
|
||||
self.result = result
|
||||
self.beg_subst = beg_subst
|
||||
self.end_subst = ') %s %s' % (self.operator, self.result)
|
||||
|
||||
@property
|
||||
def sql_subst(self):
|
||||
return ''.join([self.beg_subst, self.subst, self.end_subst])
|
||||
|
||||
def as_sql(self, table, field):
|
||||
return self.sql_subst % self.params(table, field)
|
||||
|
||||
def params(self, table, field):
|
||||
return (self.lookup, table, field)
|
||||
|
||||
class SDODistance(SDOOperation):
|
||||
"Class for Distance queries."
|
||||
def __init__(self, op, tolerance=0.05):
|
||||
super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
|
||||
self.tolerance = tolerance
|
||||
|
||||
def params(self, table, field):
|
||||
return (self.lookup, table, field, self.tolerance)
|
||||
|
||||
class SDOGeomRelate(SDOOperation):
|
||||
"Class for using SDO_GEOM.RELATE."
|
||||
def __init__(self, mask, tolerance=0.05):
|
||||
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'",
|
||||
subst=", %%s, %s", result="'%s'" % mask)
|
||||
self.mask = mask
|
||||
self.tolerance = tolerance
|
||||
|
||||
def params(self, table, field):
|
||||
return (self.lookup, table, field, self.mask, self.tolerance)
|
||||
|
||||
class SDORelate(SDOOperation):
|
||||
"Class for using SDO_RELATE."
|
||||
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
|
||||
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
|
||||
|
||||
def __init__(self, mask, **kwargs):
|
||||
super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs)
|
||||
if not self.mask_regex.match(mask):
|
||||
raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
|
||||
self.mask = mask
|
||||
|
||||
def params(self, table, field):
|
||||
return (self.lookup, table, field, self.mask)
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int)
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (SDODistance('>'), dtypes),
|
||||
'distance_gte' : (SDODistance('>='), dtypes),
|
||||
'distance_lt' : (SDODistance('<'), dtypes),
|
||||
'distance_lte' : (SDODistance('<='), dtypes),
|
||||
}
|
||||
|
||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
||||
'contains' : SDOOperation('SDO_CONTAINS'),
|
||||
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
||||
'covers' : SDOOperation('SDO_COVERS'),
|
||||
'disjoint' : SDOGeomRelate('DISJOINT'),
|
||||
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
|
||||
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
|
||||
'equals' : SDOOperation('SDO_EQUAL'),
|
||||
'exact' : SDOOperation('SDO_EQUAL'),
|
||||
'overlaps' : SDOOperation('SDO_OVERLAPS'),
|
||||
'same_as' : SDOOperation('SDO_EQUAL'),
|
||||
'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
||||
'touches' : SDOOperation('SDO_TOUCH'),
|
||||
'within' : SDOOperation('SDO_INSIDE'),
|
||||
}
|
||||
ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
|
||||
# This lookup type does not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
@ -43,25 +118,33 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
if isinstance(lookup_info, tuple):
|
||||
# First element of tuple is lookup type, second element is the type
|
||||
# of the expected argument (e.g., str, float)
|
||||
func, arg_type = lookup_info
|
||||
sdo_op, arg_type = lookup_info
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, tuple) or len(value) != 2:
|
||||
raise TypeError('2-element tuple required for %s lookup type.' % lookup_type)
|
||||
if not isinstance(value, tuple):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
if len(value) != 2:
|
||||
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(value[1], arg_type):
|
||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||
|
||||
if func == 'dwithin':
|
||||
# TODO: test and consider adding different distance options.
|
||||
return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
|
||||
if lookup_type == 'relate':
|
||||
# The SDORelate class handles construction for these queries, and verifies
|
||||
# the mask argument.
|
||||
return sdo_op(value[1]).as_sql(table_prefix, field_name)
|
||||
elif lookup_type in DISTANCE_FUNCTIONS:
|
||||
op = DISTANCE_FUNCTIONS[lookup_type][0]
|
||||
return op.as_sql(table_prefix, field_name)
|
||||
# return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
|
||||
else:
|
||||
return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
|
||||
return sdo_op.as_sql(table_prefix, field_name)
|
||||
else:
|
||||
# Returning the SQL necessary for the geometry function call. For example:
|
||||
# Lookup info is a SDOOperation instance, whos `as_sql` method returns
|
||||
# the SQL necessary for the geometry function call. For example:
|
||||
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
||||
return "%s(%s, %%s) = 'TRUE'" % (lookup_info, table_prefix + field_name)
|
||||
return lookup_info.as_sql(table_prefix, field_name)
|
||||
|
||||
# Handling 'isnull' lookup type
|
||||
if lookup_type == 'isnull':
|
||||
@ -69,10 +152,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
||||
|
||||
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||
UNION = 'SDO_AGGR_UNION'
|
||||
TRANSFORM = 'SDO_CS.TRANSFORM'
|
||||
|
||||
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
|
||||
# from WKT than SDO_GEOMETRY(...) strings ;)
|
||||
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
||||
|
@ -6,4 +6,4 @@ from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
|
||||
ASKML, ASGML, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
|
||||
ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
|
||||
|
@ -7,11 +7,11 @@ from psycopg2 import Binary
|
||||
from psycopg2.extensions import ISQLQuote
|
||||
|
||||
class PostGISAdaptor(object):
|
||||
def __init__(self, geom, srid):
|
||||
def __init__(self, geom):
|
||||
"Initializes on the geometry and the SRID."
|
||||
# Getting the WKB and the SRID
|
||||
self.wkb = geom.wkb
|
||||
self.srid = srid
|
||||
self.srid = geom.srid
|
||||
|
||||
def __conform__(self, proto):
|
||||
# Does the given protocol conform to what Psycopg2 expects?
|
||||
|
@ -1,19 +1,26 @@
|
||||
from types import StringType, UnicodeType
|
||||
from types import UnicodeType
|
||||
from django.db import connection
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
||||
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
|
||||
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
||||
from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
|
||||
from psycopg2 import Binary
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively
|
||||
qn = connection.ops.quote_name
|
||||
def gqn(value):
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
||||
return "'%s'" % value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
class PostGISField(Field):
|
||||
"""
|
||||
The backend-specific geographic field for PostGIS.
|
||||
"""
|
||||
|
||||
def _add_geom(self, style, db_table):
|
||||
"""
|
||||
Constructs the addition of the geometry to the table using the
|
||||
@ -92,53 +99,44 @@ class PostGISField(Field):
|
||||
"""
|
||||
if lookup_type in POSTGIS_TERMS:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull':
|
||||
return GeoFieldSQL([], [value])
|
||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||
|
||||
# When the input is not a GEOS geometry, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(value, GEOSGeometry):
|
||||
pass
|
||||
elif isinstance(value, (StringType, UnicodeType)):
|
||||
try:
|
||||
value = GEOSGeometry(value)
|
||||
except GEOSException:
|
||||
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
|
||||
else:
|
||||
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
|
||||
|
||||
# Getting the SRID of the geometry, or defaulting to that of the field if
|
||||
# it is None.
|
||||
srid = get_srid(self, value)
|
||||
# Get the geometry with SRID; defaults SRID to
|
||||
# that of the field if it is None.
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# The adaptor will be used by psycopg2 for quoting the WKB.
|
||||
adapt = PostGISAdaptor(value, srid)
|
||||
adapt = PostGISAdaptor(geom)
|
||||
|
||||
if srid != self._srid:
|
||||
if geom.srid != self._srid:
|
||||
# Adding the necessary string substitutions and parameters
|
||||
# to perform a geometry transformation.
|
||||
return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
|
||||
[adapt, self._srid])
|
||||
where = ['%s(%%s,%%s)' % TRANSFORM]
|
||||
params = [adapt, self._srid]
|
||||
else:
|
||||
return GeoFieldSQL(['%s'], [adapt])
|
||||
# Otherwise, the adaptor will take care of everything.
|
||||
where = ['%s']
|
||||
params = [adapt]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
|
||||
# Getting the distance parameter in the units of the field.
|
||||
where += [self.get_distance(value[1])]
|
||||
else:
|
||||
where += map(gqn, value[1:])
|
||||
return GeoFieldSQL(where, params)
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"Prepares the value for saving in the database."
|
||||
if not bool(value): return None
|
||||
if isinstance(value, GEOSGeometry):
|
||||
return PostGISAdaptor(value, value.srid)
|
||||
return PostGISAdaptor(value)
|
||||
else:
|
||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
|
||||
|
||||
def get_internal_type(self):
|
||||
"""
|
||||
Returns NoField because a stored procedure is used by PostGIS to create
|
||||
the Geometry Fields.
|
||||
"""
|
||||
return 'NoField'
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
|
@ -16,17 +16,17 @@ class GeometryColumns(models.Model):
|
||||
"""
|
||||
f_table_catalog = models.CharField(maxlength=256)
|
||||
f_table_schema = models.CharField(maxlength=256)
|
||||
f_table_name = models.CharField(maxlength=256, primary_key=True)
|
||||
f_table_name = models.CharField(maxlength=256)
|
||||
f_geometry_column = models.CharField(maxlength=256)
|
||||
coord_dimension = models.IntegerField()
|
||||
srid = models.IntegerField()
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
type = models.CharField(maxlength=30)
|
||||
|
||||
class Meta:
|
||||
db_table = 'geometry_columns'
|
||||
|
||||
@classmethod
|
||||
def table_name_col(self):
|
||||
def table_name_col(cls):
|
||||
"Class method for returning the table name column for this model."
|
||||
return 'f_table_name'
|
||||
|
||||
@ -52,3 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
@property
|
||||
def wkt(self):
|
||||
return self.srtext
|
||||
|
||||
@classmethod
|
||||
def wkt_col(cls):
|
||||
return 'srtext'
|
||||
|
@ -2,9 +2,10 @@
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
routine for PostGIS.
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db import connection
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
|
||||
from types import StringType, UnicodeType
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
# Getting the PostGIS version information
|
||||
@ -62,10 +63,32 @@ POSTGIS_OPERATORS = {
|
||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||
# means that 'ST_' is prefixes geometry function names.
|
||||
if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
|
||||
GEOM_FUNC_PREFIX = 'ST_'
|
||||
else:
|
||||
GEOM_FUNC_PREFIX = ''
|
||||
if MAJOR_VERSION >= 1:
|
||||
if (MINOR_VERSION1 > 2 or
|
||||
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
|
||||
GEOM_FUNC_PREFIX = 'ST_'
|
||||
|
||||
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
|
||||
|
||||
# Functions used by the GeoManager & GeoQuerySet
|
||||
ASKML = get_func('AsKML')
|
||||
ASGML = get_func('AsGML')
|
||||
DISTANCE = get_func('Distance')
|
||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||
TRANSFORM = get_func('Transform')
|
||||
|
||||
# Special cases for union and KML methods.
|
||||
if MINOR_VERSION1 < 3:
|
||||
UNION = 'GeomUnion'
|
||||
else:
|
||||
UNION = 'ST_Union'
|
||||
|
||||
if MINOR_VERSION1 == 1:
|
||||
ASKML = False
|
||||
else:
|
||||
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
|
||||
|
||||
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
|
||||
# first before calling the more computationally expensive GEOS routines (called
|
||||
@ -81,25 +104,36 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||
'overlaps' : 'Overlaps',
|
||||
'contains' : 'Contains',
|
||||
'intersects' : 'Intersects',
|
||||
'relate' : ('Relate', str),
|
||||
'relate' : ('Relate', basestring),
|
||||
}
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int)
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : ('>', dtypes),
|
||||
'distance_gte' : ('>=', dtypes),
|
||||
'distance_lt' : ('<', dtypes),
|
||||
'distance_lte' : ('<=', dtypes),
|
||||
}
|
||||
|
||||
if GEOM_FUNC_PREFIX == 'ST_':
|
||||
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
|
||||
for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
||||
if isinstance(func, tuple):
|
||||
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1])
|
||||
for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
|
||||
if isinstance(f, tuple):
|
||||
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
|
||||
else:
|
||||
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
|
||||
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
|
||||
|
||||
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||
{'dwithin' : ('ST_DWithin', float),
|
||||
{'dwithin' : ('ST_DWithin', dtypes),
|
||||
'coveredby' : 'ST_CoveredBy',
|
||||
'covers' : 'ST_Covers',
|
||||
}
|
||||
)
|
||||
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
|
||||
# Any other lookup types that do not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
@ -139,13 +173,19 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
func, arg_type = lookup_info
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, tuple) or len(value) != 2:
|
||||
raise TypeError('2-element tuple required for `%s` lookup type.' % lookup_type)
|
||||
if not isinstance(value, tuple):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
if len(value) != 2:
|
||||
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(value[1], arg_type):
|
||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||
|
||||
if lookup_type in DISTANCE_FUNCTIONS:
|
||||
op = DISTANCE_FUNCTIONS[lookup_type][0]
|
||||
return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
|
||||
else:
|
||||
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
|
||||
else:
|
||||
# Returning the SQL necessary for the geometry function call. For example:
|
||||
@ -158,33 +198,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
||||
|
||||
# Functions that we define manually.
|
||||
if MAJOR_VERSION == 1:
|
||||
if MINOR_VERSION1 == 3:
|
||||
# PostGIS versions 1.3.x
|
||||
ASKML = 'ST_AsKML'
|
||||
ASGML = 'ST_AsGML'
|
||||
GEOM_FROM_TEXT = 'ST_GeomFromText'
|
||||
GEOM_FROM_WKB = 'ST_GeomFromWKB'
|
||||
UNION = 'ST_Union'
|
||||
TRANSFORM = 'ST_Transform'
|
||||
elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1:
|
||||
# PostGIS versions 1.2.x
|
||||
ASKML = 'AsKML'
|
||||
ASGML = 'AsGML'
|
||||
GEOM_FROM_TEXT = 'GeomFromText'
|
||||
GEOM_FROM_WKB = 'GeomFromWKB'
|
||||
UNION = 'GeomUnion'
|
||||
TRANSFORM = 'Transform'
|
||||
elif MINOR_VERSION1 == 1 and MINOR_VERSION2 >= 0:
|
||||
# PostGIS versions 1.1.x
|
||||
ASKML = False
|
||||
ASGML = 'AsGML'
|
||||
GEOM_FROM_TEXT = 'GeomFromText'
|
||||
GEOM_FROM_WKB = 'GeomFromWKB'
|
||||
TRANSFORM = 'Transform'
|
||||
UNION = 'GeomUnion'
|
||||
|
||||
# Custom selection not needed for PostGIS since GEOS geometries may be
|
||||
# instantiated directly from the HEXEWKB returned by default. If
|
||||
# WKT is needed for some reason in the future, this value may be changed,
|
||||
|
@ -7,6 +7,9 @@ class GeoFieldSQL(object):
|
||||
self.where = where
|
||||
self.params = params
|
||||
|
||||
def __str__(self):
|
||||
return self.where[0] % tuple(self.params)
|
||||
|
||||
def get_srid(field, geom):
|
||||
"""
|
||||
Gets the SRID depending on the value of the SRID setting of the field
|
||||
|
@ -1,8 +1,17 @@
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend.
|
||||
from django.contrib.gis.db.models.proxy import GeometryProxy
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.oldforms import WKTField
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
# Attempting to get the spatial reference system.
|
||||
try:
|
||||
from django.contrib.gis.models import SpatialRefSys
|
||||
except NotImplementedError:
|
||||
SpatialRefSys = None
|
||||
|
||||
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
|
||||
class GeometryField(GeoBackendField):
|
||||
@ -11,24 +20,101 @@ class GeometryField(GeoBackendField):
|
||||
# The OpenGIS Geometry name.
|
||||
_geom = 'GEOMETRY'
|
||||
|
||||
def __init__(self, srid=4326, index=True, dim=2, **kwargs):
|
||||
"""The initialization function for geometry fields. Takes the following
|
||||
def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
|
||||
"""
|
||||
The initialization function for geometry fields. Takes the following
|
||||
as keyword arguments:
|
||||
|
||||
srid - The spatial reference system identifier. An OGC standard.
|
||||
Defaults to 4326 (WGS84)
|
||||
srid:
|
||||
The spatial reference system identifier, an OGC standard.
|
||||
Defaults to 4326 (WGS84).
|
||||
|
||||
index - Indicates whether to create a GiST index. Defaults to True.
|
||||
spatial_index:
|
||||
Indicates whether to create a spatial index. Defaults to True.
|
||||
Set this instead of 'db_index' for geographic fields since index
|
||||
creation is different for geometry columns.
|
||||
|
||||
dim - The number of dimensions for this geometry. Defaults to 2.
|
||||
dim:
|
||||
The number of dimensions for this geometry. Defaults to 2.
|
||||
"""
|
||||
self._index = index
|
||||
|
||||
# Backward-compatibility notice, this will disappear in future revisions.
|
||||
if 'index' in kwargs:
|
||||
from warnings import warn
|
||||
warn('The `index` keyword has been deprecated, please use the `spatial_index` keyword instead.')
|
||||
self._index = kwargs['index']
|
||||
else:
|
||||
self._index = spatial_index
|
||||
|
||||
# Setting the SRID and getting the units.
|
||||
self._srid = srid
|
||||
if SpatialRefSys:
|
||||
# This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
|
||||
# Why? `syncdb` fails to recognize installed geographic models when there's
|
||||
# an ORM query instantiated within a model field. No matter, this works fine
|
||||
# too.
|
||||
cur = connection.cursor()
|
||||
qn = connection.ops.quote_name
|
||||
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
|
||||
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
|
||||
'wkt_col' : qn(SpatialRefSys.wkt_col()),
|
||||
'srid_col' : qn('srid'),
|
||||
'srid' : srid,
|
||||
}
|
||||
cur.execute(stmt)
|
||||
row = cur.fetchone()
|
||||
self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
|
||||
|
||||
# Setting the dimension of the geometry field.
|
||||
self._dim = dim
|
||||
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
||||
|
||||
### Routines specific to GeometryField ###
|
||||
def get_distance(self, dist):
|
||||
if isinstance(dist, Distance):
|
||||
return getattr(dist, Distance.unit_attname(self._unit_name))
|
||||
elif isinstance(dist, (int, float, Decimal)):
|
||||
# Assuming the distance is in the units of the field.
|
||||
return dist
|
||||
|
||||
def get_geometry(self, value):
|
||||
"""
|
||||
Retrieves the geometry, setting the default SRID from the given
|
||||
lookup parameters.
|
||||
"""
|
||||
if isinstance(value, tuple):
|
||||
geom = value[0]
|
||||
else:
|
||||
geom = value
|
||||
|
||||
# When the input is not a GEOS geometry, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(geom, GEOSGeometry):
|
||||
pass
|
||||
elif isinstance(geom, basestring):
|
||||
try:
|
||||
geom = GEOSGeometry(geom)
|
||||
except GEOSException:
|
||||
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
|
||||
else:
|
||||
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
|
||||
|
||||
# Assigning the SRID value.
|
||||
geom.srid = self.get_srid(geom)
|
||||
|
||||
return geom
|
||||
|
||||
def get_srid(self, geom):
|
||||
"""
|
||||
Has logic for retrieving the default SRID taking into account
|
||||
the SRID of the field.
|
||||
"""
|
||||
if geom.srid is None or (geom.srid == -1 and self._srid != -1):
|
||||
return self._srid
|
||||
else:
|
||||
return geom.srid
|
||||
|
||||
### Routines overloaded from Field ###
|
||||
def contribute_to_class(self, cls, name):
|
||||
super(GeometryField, self).contribute_to_class(cls, name)
|
||||
|
||||
|
@ -7,6 +7,9 @@ class GeoManager(Manager):
|
||||
def get_query_set(self):
|
||||
return GeoQuerySet(model=self.model)
|
||||
|
||||
def distance(self, *args, **kwargs):
|
||||
return self.get_query_set().distance(*args, **kwargs)
|
||||
|
||||
def gml(self, *args, **kwargs):
|
||||
return self.get_query_set().gml(*args, **kwargs)
|
||||
|
||||
|
@ -6,9 +6,13 @@ from django.db.models.fields import FieldDoesNotExist
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
# parse_lookup depends on the spatial database backend.
|
||||
from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
|
||||
from django.contrib.gis.db.backend import parse_lookup, \
|
||||
ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
# Flag indicating whether the backend is Oracle.
|
||||
oracle = SPATIAL_BACKEND == 'oracle'
|
||||
|
||||
class GeoQ(Q):
|
||||
"Geographical query encapsulation object."
|
||||
|
||||
@ -28,11 +32,23 @@ class GeoQuerySet(QuerySet):
|
||||
|
||||
# For replacement fields in the SELECT.
|
||||
self._custom_select = {}
|
||||
self._ewkt = None
|
||||
|
||||
# If GEOM_SELECT is defined in the backend, then it will be used
|
||||
# for the selection format of the geometry column.
|
||||
if GEOM_SELECT: self._geo_fmt = GEOM_SELECT
|
||||
else: self._geo_fmt = '%s'
|
||||
if GEOM_SELECT:
|
||||
#if oracle and hasattr(self, '_ewkt'):
|
||||
# Transformed geometries in Oracle use EWKT so that the SRID
|
||||
# on the transformed lazy geometries is set correctly).
|
||||
#print '-=' * 20
|
||||
#print self._ewkt, GEOM_SELECT
|
||||
#self._geo_fmt = "'SRID=%d;'||%s" % (self._ewkt, GEOM_SELECT)
|
||||
#self._geo_fmt = GEOM_SELECT
|
||||
#else:
|
||||
#print '-=' * 20
|
||||
self._geo_fmt = GEOM_SELECT
|
||||
else:
|
||||
self._geo_fmt = '%s'
|
||||
|
||||
def _filter_or_exclude(self, mapper, *args, **kwargs):
|
||||
# mapper is a callable used to transform Q objects,
|
||||
@ -66,8 +82,16 @@ class GeoQuerySet(QuerySet):
|
||||
# handling the selection of native database geometry formats.
|
||||
for f in opts.fields:
|
||||
# Getting the selection format string.
|
||||
if hasattr(f, '_geom'): sel_fmt = self._geo_fmt
|
||||
else: sel_fmt = '%s'
|
||||
if hasattr(f, '_geom'):
|
||||
sel_fmt = self._geo_fmt
|
||||
|
||||
# If an SRID needs to specified other than what is in the field
|
||||
# (like when `transform` is called), make sure to explicitly set
|
||||
# the SRID by returning EWKT.
|
||||
if self._ewkt and oracle:
|
||||
sel_fmt = "'SRID=%d;'||%s" % (self._ewkt, sel_fmt)
|
||||
else:
|
||||
sel_fmt = '%s'
|
||||
|
||||
# Getting the field selection substitution string
|
||||
if f.column in self._custom_select:
|
||||
@ -147,7 +171,7 @@ class GeoQuerySet(QuerySet):
|
||||
sql.append("ORDER BY " + ", ".join(order_by))
|
||||
|
||||
# LIMIT and OFFSET clauses
|
||||
if SPATIAL_BACKEND != 'oracle':
|
||||
if not oracle:
|
||||
if self._limit is not None:
|
||||
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
|
||||
else:
|
||||
@ -206,6 +230,7 @@ class GeoQuerySet(QuerySet):
|
||||
def _clone(self, klass=None, **kwargs):
|
||||
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
|
||||
c._custom_select = self._custom_select
|
||||
c._ewkt = self._ewkt
|
||||
return c
|
||||
|
||||
#### Methods specific to the GeoQuerySet ####
|
||||
@ -227,7 +252,60 @@ class GeoQuerySet(QuerySet):
|
||||
else:
|
||||
return False
|
||||
|
||||
def gml(self, field_name, precision=8, version=2):
|
||||
def _get_geofield(self):
|
||||
"Returns the name of the first Geometry field encountered."
|
||||
for field in self.model._meta.fields:
|
||||
if isinstance(field, GeometryField):
|
||||
return field.name
|
||||
raise Exception('No GeometryFields in the model.')
|
||||
|
||||
def distance(self, *args, **kwargs):
|
||||
"""
|
||||
Returns the distance from the given geographic field name to the
|
||||
given geometry in a `distance` attribute on each element of the
|
||||
GeoQuerySet.
|
||||
"""
|
||||
if not DISTANCE:
|
||||
raise ImproperlyConfigured('Distance() stored proecedure not available.')
|
||||
|
||||
# Getting the geometry field and GEOSGeometry object to base distance
|
||||
# calculations from.
|
||||
nargs = len(args)
|
||||
if nargs == 1:
|
||||
field_name = self._get_geofield()
|
||||
geom = args[0]
|
||||
elif nargs == 2:
|
||||
field_name, geom = args
|
||||
else:
|
||||
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
|
||||
|
||||
# Getting the quoted column.
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
raise TypeError('Distance output only available on GeometryFields.')
|
||||
|
||||
# Getting the geographic field instance.
|
||||
geo_field = self.model._meta.get_field(field_name)
|
||||
|
||||
# Using the field's get_db_prep_lookup() to get any needed
|
||||
# transformation SQL -- we pass in a 'dummy' `contains` lookup
|
||||
# type.
|
||||
geom_sql = geo_field.get_db_prep_lookup('contains', geom)
|
||||
if oracle:
|
||||
# The `tolerance` keyword may be used for Oracle.
|
||||
tolerance = kwargs.get('tolerance', 0.05)
|
||||
|
||||
# More legwork here because the OracleSpatialAdaptor doesn't do
|
||||
# quoting of the WKT.
|
||||
params = ["'%s'" % geom_sql.params[0]]
|
||||
params.extend(geom_sql.params[1:])
|
||||
gsql = geom_sql.where[0] % tuple(params)
|
||||
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
|
||||
else:
|
||||
dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
|
||||
return self.extra(select=dist_select)
|
||||
|
||||
def gml(self, field_name=None, precision=8, version=2):
|
||||
"""
|
||||
Returns GML representation of the given field in a `gml` attribute
|
||||
on each element of the GeoQuerySet.
|
||||
@ -236,12 +314,16 @@ class GeoQuerySet(QuerySet):
|
||||
if not ASGML:
|
||||
raise ImproperlyConfigured('AsGML() stored procedure not available.')
|
||||
|
||||
# Is the given field name a geographic field?
|
||||
# If no field name explicitly given, get the first GeometryField from
|
||||
# the model.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
raise TypeError('GML output only available on GeometryFields')
|
||||
raise TypeError('GML output only available on GeometryFields.')
|
||||
|
||||
if SPATIAL_BACKEND == 'oracle':
|
||||
if oracle:
|
||||
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
|
||||
else:
|
||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
|
||||
@ -249,7 +331,7 @@ class GeoQuerySet(QuerySet):
|
||||
# Adding GML function call to SELECT part of the SQL.
|
||||
return self.extra(select=gml_select)
|
||||
|
||||
def kml(self, field_name, precision=8):
|
||||
def kml(self, field_name=None, precision=8):
|
||||
"""
|
||||
Returns KML representation of the given field name in a `kml`
|
||||
attribute on each element of the GeoQuerySet.
|
||||
@ -258,7 +340,10 @@ class GeoQuerySet(QuerySet):
|
||||
if not ASKML:
|
||||
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
||||
|
||||
# Is the given field name a geographic field?
|
||||
# Getting the geographic field.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
raise TypeError('KML output only available on GeometryFields.')
|
||||
@ -266,30 +351,40 @@ class GeoQuerySet(QuerySet):
|
||||
# Adding the AsKML function call to SELECT part of the SQL.
|
||||
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
|
||||
|
||||
def transform(self, field_name, srid=4326):
|
||||
def transform(self, field_name=None, srid=4326):
|
||||
"""
|
||||
Transforms the given geometry field to the given SRID. If no SRID is
|
||||
provided, the transformation will default to using 4326 (WGS84).
|
||||
"""
|
||||
# Is the given field name a geographic field?
|
||||
# Getting the geographic field.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
elif isinstance(field_name, int):
|
||||
srid = field_name
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field = self.model._meta.get_field(field_name)
|
||||
if not isinstance(field, GeometryField):
|
||||
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
|
||||
|
||||
# If there's already custom select SQL.
|
||||
# Why cascading substitutions? Because spatial backends like
|
||||
# Oracle and MySQL already require a function call to convert to text, thus
|
||||
# when there's also a transformation we need to cascade the substitutions.
|
||||
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
|
||||
col = self._custom_select.get(field.column, self._field_column(field))
|
||||
|
||||
# Setting the key for the field's column with the custom SELECT SQL to
|
||||
# override the geometry column returned from the database.
|
||||
if SPATIAL_BACKEND == 'oracle':
|
||||
if oracle:
|
||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
|
||||
self._ewkt = srid
|
||||
else:
|
||||
custom_sel = '(%s(%s, %s)) AS %s' % \
|
||||
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
|
||||
self._custom_select[field.column] = custom_sel
|
||||
return self._clone()
|
||||
|
||||
def union(self, field_name, tolerance=0.0005):
|
||||
def union(self, field_name=None, tolerance=0.0005):
|
||||
"""
|
||||
Performs an aggregate union on the given geometry field. Returns
|
||||
None if the GeoQuerySet is empty. The `tolerance` keyword is for
|
||||
@ -300,6 +395,9 @@ class GeoQuerySet(QuerySet):
|
||||
raise ImproperlyConfigured('Union stored procedure not available.')
|
||||
|
||||
# Getting the geographic field column
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
raise TypeError('Aggregate Union only available on GeometryFields.')
|
||||
@ -312,7 +410,7 @@ class GeoQuerySet(QuerySet):
|
||||
|
||||
# Replacing the select with a call to the ST_Union stored procedure
|
||||
# on the geographic field column.
|
||||
if SPATIAL_BACKEND == 'oracle':
|
||||
if oracle:
|
||||
union_sql = 'SELECT %s' % self._geo_fmt
|
||||
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
|
||||
union_sql += sql
|
||||
@ -323,7 +421,7 @@ class GeoQuerySet(QuerySet):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(union_sql, params)
|
||||
|
||||
if SPATIAL_BACKEND == 'oracle':
|
||||
if oracle:
|
||||
# On Oracle have to read out WKT from CLOB first.
|
||||
clob = cursor.fetchone()[0]
|
||||
if clob: u = clob.read()
|
||||
|
@ -10,17 +10,24 @@ from django.contrib.gis.gdal import HAS_GDAL
|
||||
if HAS_GDAL:
|
||||
from django.contrib.gis.gdal import SpatialReference
|
||||
|
||||
class SpatialRefSysMixin(object):
|
||||
"""
|
||||
The SpatialRefSysMixin is a class used by the database-dependent
|
||||
SpatialRefSys objects to reduce redundnant code.
|
||||
"""
|
||||
|
||||
# For pulling out the spheroid from the spatial reference string. This
|
||||
# regular expression is used only if the user does not have GDAL installed.
|
||||
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
|
||||
# parameter.
|
||||
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
|
||||
|
||||
class SpatialRefSysMixin(object):
|
||||
"""
|
||||
The SpatialRefSysMixin is a class used by the database-dependent
|
||||
SpatialRefSys objects to reduce redundnant code.
|
||||
"""
|
||||
# For pulling out the units on platforms w/o GDAL installed.
|
||||
# TODO: Figure out how to pull out angular units of projected coordinate system and
|
||||
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
|
||||
# distance queries.
|
||||
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"""
|
||||
@ -53,7 +60,7 @@ class SpatialRefSysMixin(object):
|
||||
if HAS_GDAL:
|
||||
return self.srs.ellipsoid
|
||||
else:
|
||||
m = spheroid_regex.match(self.wkt)
|
||||
m = self.spheroid_regex.match(self.wkt)
|
||||
if m: return (float(m.group('major')), float(m.group('flattening')))
|
||||
else: return None
|
||||
|
||||
@ -75,37 +82,93 @@ class SpatialRefSysMixin(object):
|
||||
@property
|
||||
def projected(self):
|
||||
"Is this Spatial Reference projected?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.projected
|
||||
else:
|
||||
return self.wkt.startswith('PROJCS')
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
"Is this Spatial Reference local?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.local
|
||||
else:
|
||||
return self.wkt.startswith('LOCAL_CS')
|
||||
|
||||
@property
|
||||
def geographic(self):
|
||||
"Is this Spatial Reference geographic?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.geographic
|
||||
else:
|
||||
return self.wkt.startswith('GEOGCS')
|
||||
|
||||
@property
|
||||
def linear_name(self):
|
||||
"Returns the linear units name."
|
||||
if HAS_GDAL:
|
||||
return self.srs.linear_name
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
|
||||
@property
|
||||
def linear_units(self):
|
||||
"Returns the linear units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.linear_units
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Returns the angular units."
|
||||
return self.srs.angular_units
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
|
||||
@property
|
||||
def angular_name(self):
|
||||
"Returns the name of the angular units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.angular_name
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Returns the angular units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.angular_units
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"Returns a tuple of the units and the name."
|
||||
if self.projected or self.local:
|
||||
return (self.linear_units, self.linear_name)
|
||||
elif self.geographic:
|
||||
return (self.angular_units, self.angular_name)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
@classmethod
|
||||
def get_units(cls, wkt):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrive the units on the given WKT, without having to use
|
||||
any of the database fields.
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
return SpatialReference(wkt).units
|
||||
else:
|
||||
m = cls.units_regex.match(wkt)
|
||||
return m.group('unit'), m.group('unit_name')
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
@ -115,7 +178,7 @@ class SpatialRefSysMixin(object):
|
||||
try:
|
||||
return unicode(self.srs)
|
||||
except:
|
||||
return unicode(self.srtext)
|
||||
return unicode(self.wkt)
|
||||
|
||||
# The SpatialRefSys and GeometryColumns models
|
||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||
|
@ -3,9 +3,9 @@ from copy import copy
|
||||
from unittest import TestSuite, TextTestRunner
|
||||
from django.contrib.gis.gdal import HAS_GDAL
|
||||
try:
|
||||
from django.contrib.gis.tests.utils import mysql
|
||||
from django.contrib.gis.tests.utils import mysql, oracle
|
||||
except:
|
||||
mysql = False
|
||||
mysql, oracle = (False, False)
|
||||
|
||||
# Tests that require use of a spatial database (e.g., creation of models)
|
||||
test_models = ['geoapp']
|
||||
@ -16,7 +16,12 @@ test_suite_names = [
|
||||
'test_measure',
|
||||
]
|
||||
if HAS_GDAL:
|
||||
test_models += ['layermap']
|
||||
if oracle:
|
||||
# TODO: There is a problem with the `syncdb` SQL for the LayerMapping
|
||||
# tests on Oracle.
|
||||
test_models += ['distapp']
|
||||
else:
|
||||
test_models += ['distapp', 'layermap']
|
||||
test_suite_names += [
|
||||
'test_gdal_driver',
|
||||
'test_gdal_ds',
|
||||
@ -54,7 +59,7 @@ def run_tests(module_list, verbosity=1, interactive=True):
|
||||
(3) Start this database `pg_ctl -D /path/to/user/db start`
|
||||
|
||||
On Windows platforms simply use the pgAdmin III utility to add superuser
|
||||
priviliges to your database user.
|
||||
privileges to your database user.
|
||||
|
||||
Make sure your settings.py matches the settings of the user database.
|
||||
For example, set the same port number (`DATABASE_PORT=5433`).
|
||||
|
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)
|
||||
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
||||
class City(models.Model):
|
||||
name = models.CharField(max_length=30)
|
||||
point = models.PointField()
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
||||
class State(models.Model):
|
||||
name = models.CharField(max_length=30)
|
||||
poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here.
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
||||
class Feature(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
geom = models.GeometryField()
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
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 django.contrib.gis import gdal
|
||||
from django.contrib.gis.geos import *
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.tests.utils import no_oracle, no_postgis, oracle, postgis
|
||||
|
||||
# TODO: Some tests depend on the success/failure of previous tests, these should
|
||||
# be decoupled. This flag is an artifact of this problem, and makes debugging easier;
|
||||
# specifically, the DISABLE flag will disables all tests, allowing problem tests to
|
||||
# be examined individually.
|
||||
DISABLE = False
|
||||
|
||||
class GeoModelTest(unittest.TestCase):
|
||||
|
||||
def test01_initial_sql(self):
|
||||
"Testing geographic initial SQL."
|
||||
if DISABLE: return
|
||||
if oracle:
|
||||
# Oracle doesn't allow strings longer than 4000 characters
|
||||
# in SQL files, and I'm stumped on how to use Oracle BFILE's
|
||||
@ -31,10 +39,15 @@ class GeoModelTest(unittest.TestCase):
|
||||
# Ensuring that data was loaded from initial SQL.
|
||||
self.assertEqual(2, Country.objects.count())
|
||||
self.assertEqual(8, City.objects.count())
|
||||
self.assertEqual(3, State.objects.count())
|
||||
|
||||
# Oracle cannot handle NULL geometry values w/certain queries.
|
||||
if oracle: n_state = 2
|
||||
else: n_state = 3
|
||||
self.assertEqual(n_state, State.objects.count())
|
||||
|
||||
def test02_proxy(self):
|
||||
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
||||
if DISABLE: return
|
||||
#### Testing on a Point
|
||||
pnt = Point(0, 0)
|
||||
nullcity = City(name='NullCity', point=pnt)
|
||||
@ -104,32 +117,41 @@ class GeoModelTest(unittest.TestCase):
|
||||
@no_oracle # Oracle does not support KML.
|
||||
def test03a_kml(self):
|
||||
"Testing KML output from the database using GeoManager.kml()."
|
||||
if DISABLE: return
|
||||
# Should throw a TypeError when trying to obtain KML from a
|
||||
# non-geometry field.
|
||||
qs = City.objects.all()
|
||||
self.assertRaises(TypeError, qs.kml, 'name')
|
||||
|
||||
# Ensuring the KML is as expected.
|
||||
ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
|
||||
ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
|
||||
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
|
||||
for ptown in [ptown1, ptown2]:
|
||||
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml)
|
||||
|
||||
def test03b_gml(self):
|
||||
"Testing GML output from the database using GeoManager.gml()."
|
||||
if DISABLE: return
|
||||
# Should throw a TypeError when tyring to obtain GML from a
|
||||
# non-geometry field.
|
||||
qs = City.objects.all()
|
||||
self.assertRaises(TypeError, qs.gml, 'name')
|
||||
ptown = City.objects.gml('point', precision=9).get(name='Pueblo')
|
||||
ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
|
||||
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
|
||||
|
||||
if oracle:
|
||||
# No precision parameter for Oracle :-/
|
||||
import re
|
||||
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925199\d+,38.25500\d+ </gml:coordinates></gml:Point>')
|
||||
for ptown in [ptown1, ptown2]:
|
||||
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
|
||||
else:
|
||||
for ptown in [ptown1, ptown2]:
|
||||
self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
|
||||
|
||||
def test04_transform(self):
|
||||
"Testing the transform() GeoManager method."
|
||||
if DISABLE: return
|
||||
# Pre-transformed points for Houston and Pueblo.
|
||||
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
|
||||
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
|
||||
@ -142,13 +164,31 @@ class GeoModelTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(htown.x, h.point.x, 8)
|
||||
self.assertAlmostEqual(htown.y, h.point.y, 8)
|
||||
|
||||
p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
|
||||
p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
|
||||
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
|
||||
for p in [p1, p2]:
|
||||
self.assertEqual(2774, p.point.srid)
|
||||
self.assertAlmostEqual(ptown.x, p.point.x, 7)
|
||||
self.assertAlmostEqual(ptown.y, p.point.y, 7)
|
||||
|
||||
def test09_disjoint(self):
|
||||
"Testing the `disjoint` lookup type."
|
||||
if DISABLE: return
|
||||
ptown = City.objects.get(name='Pueblo')
|
||||
qs1 = City.objects.filter(point__disjoint=ptown.point)
|
||||
self.assertEqual(7, qs1.count())
|
||||
|
||||
if not postgis:
|
||||
# TODO: Do NULL columns bork queries on PostGIS? The following
|
||||
# error is encountered:
|
||||
# psycopg2.ProgrammingError: invalid memory alloc request size 4294957297
|
||||
qs2 = State.objects.filter(poly__disjoint=ptown.point)
|
||||
self.assertEqual(1, qs2.count())
|
||||
self.assertEqual('Kansas', qs2[0].name)
|
||||
|
||||
def test10_contains_contained(self):
|
||||
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
|
||||
if DISABLE: return
|
||||
# Getting Texas, yes we were a country -- once ;)
|
||||
texas = Country.objects.get(name='Texas')
|
||||
|
||||
@ -190,6 +230,7 @@ class GeoModelTest(unittest.TestCase):
|
||||
|
||||
def test11_lookup_insert_transform(self):
|
||||
"Testing automatic transform for lookups and inserts."
|
||||
if DISABLE: return
|
||||
# San Antonio in 'WGS84' (SRID 4326)
|
||||
sa_4326 = 'POINT (-98.493183 29.424170)'
|
||||
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
|
||||
@ -225,8 +266,12 @@ class GeoModelTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6)
|
||||
self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6)
|
||||
|
||||
# Oracle does not support NULL geometries in its spatial index for
|
||||
# some routines (e.g., SDO_GEOM.RELATE).
|
||||
@no_oracle
|
||||
def test12_null_geometries(self):
|
||||
"Testing NULL geometry support, and the `isnull` lookup type."
|
||||
if DISABLE: return
|
||||
# Querying for both NULL and Non-NULL values.
|
||||
nullqs = State.objects.filter(poly__isnull=True)
|
||||
validqs = State.objects.filter(poly__isnull=False)
|
||||
@ -250,6 +295,7 @@ class GeoModelTest(unittest.TestCase):
|
||||
@no_oracle # No specific `left` or `right` operators in Oracle.
|
||||
def test13_left_right(self):
|
||||
"Testing the 'left' and 'right' lookup types."
|
||||
if DISABLE: return
|
||||
# Left: A << B => true if xmax(A) < xmin(B)
|
||||
# Right: A >> B => true if xmin(A) > xmax(B)
|
||||
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
|
||||
@ -285,6 +331,7 @@ class GeoModelTest(unittest.TestCase):
|
||||
for c in qs: self.assertEqual(True, c.name in cities)
|
||||
|
||||
def test14_equals(self):
|
||||
if DISABLE: return
|
||||
"Testing the 'same_as' and 'equals' lookup types."
|
||||
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
||||
c1 = City.objects.get(point=pnt)
|
||||
@ -292,57 +339,73 @@ class GeoModelTest(unittest.TestCase):
|
||||
c3 = City.objects.get(point__equals=pnt)
|
||||
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
||||
|
||||
@no_oracle # Oracle SDO_RELATE() uses a different system.
|
||||
def test15_relate(self):
|
||||
"Testing the 'relate' lookup type."
|
||||
if DISABLE: return
|
||||
# To make things more interesting, we will have our Texas reference point in
|
||||
# different SRIDs.
|
||||
pnt1 = fromstr('POINT (649287.0363174345111474 4177429.4494686722755432)', srid=2847)
|
||||
pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847)
|
||||
pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
|
||||
|
||||
# Testing bad argument tuples that should return a TypeError
|
||||
bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')]
|
||||
for args in bad_args:
|
||||
try:
|
||||
# Testing bad argument tuples that should return a TypeError or
|
||||
# a ValueError.
|
||||
bad_args = [((pnt1, 0), TypeError),
|
||||
((pnt2, 'T*T***FF*', 0), ValueError),
|
||||
((23, 'foo'), TypeError),
|
||||
]
|
||||
for args, e in bad_args:
|
||||
qs = Country.objects.filter(mpoly__relate=args)
|
||||
cnt = qs.count()
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail('Expected a TypeError')
|
||||
self.assertRaises(e, qs.count)
|
||||
|
||||
# 'T*T***FF*' => Contains()
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T*T***FF*')).name)
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T*T***FF*')).name)
|
||||
# Relate works differently for the different backends.
|
||||
if postgis:
|
||||
contains_mask = 'T*T***FF*'
|
||||
within_mask = 'T*F**F***'
|
||||
intersects_mask = 'T********'
|
||||
elif oracle:
|
||||
contains_mask = 'contains'
|
||||
within_mask = 'inside'
|
||||
# TODO: This is not quite the same as the PostGIS mask above
|
||||
intersects_mask = 'overlapbdyintersect'
|
||||
|
||||
# 'T*F**F***' => Within()
|
||||
# Testing contains relation mask.
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, contains_mask)).name)
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, contains_mask)).name)
|
||||
|
||||
# Testing within relation mask.
|
||||
ks = State.objects.get(name='Kansas')
|
||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T*F**F***')).name)
|
||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).name)
|
||||
|
||||
# 'T********' => Intersects()
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T********')).name)
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T********')).name)
|
||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T********')).name)
|
||||
# Testing intersection relation mask.
|
||||
if not oracle:
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name)
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name)
|
||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name)
|
||||
|
||||
def test16_createnull(self):
|
||||
"Testing creating a model instance and the geometry being None"
|
||||
if DISABLE: return
|
||||
c = City()
|
||||
self.assertEqual(c.point, None)
|
||||
|
||||
def test17_union(self):
|
||||
"Testing the union() GeoManager method."
|
||||
if DISABLE: return
|
||||
tx = Country.objects.get(name='Texas').mpoly
|
||||
# Houston, Dallas, San Antonio
|
||||
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
||||
qs = City.objects.filter(point__within=tx)
|
||||
self.assertRaises(TypeError, qs.union, 'name')
|
||||
u = qs.union('point')
|
||||
self.assertEqual(True, union.equals_exact(u, 10)) # Going up to 10 digits of precision.
|
||||
u1 = qs.union('point')
|
||||
u2 = qs.union()
|
||||
self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
|
||||
self.assertEqual(True, union.equals_exact(u2, 10))
|
||||
qs = City.objects.filter(name='NotACity')
|
||||
self.assertEqual(None, qs.union('point'))
|
||||
|
||||
def test18_geometryfield(self):
|
||||
"Testing GeometryField."
|
||||
if DISABLE: return
|
||||
f1 = Feature(name='Point', geom=Point(1, 1))
|
||||
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
|
||||
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
|
||||
|
Loading…
x
Reference in New Issue
Block a user