Permalink
Browse files

gis: Added distance querying capabilites via the `distance` manager m…

…ethod 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
  • Loading branch information...
1 parent cc0cc9f commit fae19f8cedf14c946febd10513bfbaf9377eaf64 @jbronn jbronn committed Dec 4, 2007
@@ -26,41 +26,33 @@
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.
@@ -117,7 +109,7 @@ def parse_lookup(kwarg_items, opts):
raise ValueError, "Cannot use None as a query value"
elif callable(value):
value = value()
-
+
joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
joins.update(joins2)
where.extend(where2)
@@ -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)
-
+ 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.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.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 @@ def get_db_prep_lookup(self, lookup_type, value):
# 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 Meta:
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 Meta:
@property
def wkt(self):
return self.wktext
+
+ @classmethod
+ def wkt_col(cls):
+ return 'wktext'
@@ -2,24 +2,99 @@
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
+# 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' : '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',
+ '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,36 +118,40 @@ 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':
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
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.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.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?
Oops, something went wrong.

0 comments on commit fae19f8

Please sign in to comment.