Converted GIS lookups to use the new Lookup API

Thanks Tim Graham, Anssi Kääriäinen and Marc Tamlyn for the
reviews.
This commit is contained in:
Claude Paroz 2014-05-22 21:51:30 +02:00
parent 4ef9618e12
commit 2bd1bbc424
11 changed files with 410 additions and 487 deletions

View File

@ -42,19 +42,19 @@ class BaseSpatialFeatures(object):
@property @property
def supports_bbcontains_lookup(self): def supports_bbcontains_lookup(self):
return 'bbcontains' in self.connection.ops.gis_terms return 'bbcontains' in self.connection.ops.gis_operators
@property @property
def supports_contained_lookup(self): def supports_contained_lookup(self):
return 'contained' in self.connection.ops.gis_terms return 'contained' in self.connection.ops.gis_operators
@property @property
def supports_dwithin_lookup(self): def supports_dwithin_lookup(self):
return 'dwithin' in self.connection.ops.distance_functions return 'dwithin' in self.connection.ops.gis_operators
@property @property
def supports_relate_lookup(self): def supports_relate_lookup(self):
return 'relate' in self.connection.ops.gis_terms return 'relate' in self.connection.ops.gis_operators
# For each of those methods, the class will have a property named # For each of those methods, the class will have a property named
# `has_<name>_method` (defined in __init__) which accesses connection.ops # `has_<name>_method` (defined in __init__) which accesses connection.ops
@ -97,12 +97,6 @@ class BaseSpatialOperations(object):
instantiated by each spatial database backend with the features instantiated by each spatial database backend with the features
it has. it has.
""" """
distance_functions = {}
geometry_functions = {}
geometry_operators = {}
geography_operators = {}
geography_functions = {}
gis_terms = set()
truncate_params = {} truncate_params = {}
# Quick booleans for the type of this spatial backend, and # Quick booleans for the type of this spatial backend, and
@ -215,9 +209,6 @@ class BaseSpatialOperations(object):
def spatial_aggregate_sql(self, agg): def spatial_aggregate_sql(self, agg):
raise NotImplementedError('Aggregate support not implemented for this spatial backend.') raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_lookup_sql() method')
# Routines for getting the OGC-compliant models. # Routines for getting the OGC-compliant models.
def geometry_columns(self): def geometry_columns(self):
raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method') raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method')

View File

@ -2,6 +2,7 @@ from django.db.backends.mysql.base import DatabaseOperations
from django.contrib.gis.db.backends.adapter import WKTAdapter from django.contrib.gis.db.backends.adapter import WKTAdapter
from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.utils import SpatialOperator
class MySQLOperations(DatabaseOperations, BaseSpatialOperations): class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
@ -16,27 +17,25 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
Adapter = WKTAdapter Adapter = WKTAdapter
Adaptor = Adapter # Backwards-compatibility alias. Adaptor = Adapter # Backwards-compatibility alias.
geometry_functions = { gis_operators = {
'bbcontains': 'MBRContains', # For consistency w/PostGIS API 'bbcontains': SpatialOperator(func='MBRContains'), # For consistency w/PostGIS API
'bboverlaps': 'MBROverlaps', # .. .. 'bboverlaps': SpatialOperator(func='MBROverlaps'), # .. ..
'contained': 'MBRWithin', # .. .. 'contained': SpatialOperator(func='MBRWithin'), # .. ..
'contains': 'MBRContains', 'contains': SpatialOperator(func='MBRContains'),
'disjoint': 'MBRDisjoint', 'disjoint': SpatialOperator(func='MBRDisjoint'),
'equals': 'MBREqual', 'equals': SpatialOperator(func='MBREqual'),
'exact': 'MBREqual', 'exact': SpatialOperator(func='MBREqual'),
'intersects': 'MBRIntersects', 'intersects': SpatialOperator(func='MBRIntersects'),
'overlaps': 'MBROverlaps', 'overlaps': SpatialOperator(func='MBROverlaps'),
'same_as': 'MBREqual', 'same_as': SpatialOperator(func='MBREqual'),
'touches': 'MBRTouches', 'touches': SpatialOperator(func='MBRTouches'),
'within': 'MBRWithin', 'within': SpatialOperator(func='MBRWithin'),
} }
gis_terms = set(geometry_functions) | {'isnull'}
def geo_db_type(self, f): def geo_db_type(self, f):
return f.geom_type return f.geom_type
def get_geom_placeholder(self, value, srid): def get_geom_placeholder(self, f, value):
""" """
The placeholder here has to include MySQL's WKT constructor. Because The placeholder here has to include MySQL's WKT constructor. Because
MySQL does not support spatial transformations, there is no need to MySQL does not support spatial transformations, there is no need to
@ -47,19 +46,3 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
else: else:
placeholder = '%s(%%s)' % self.from_text placeholder = '%s(%%s)' % self.from_text
return placeholder return placeholder
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
geo_col, db_type = lvalue
lookup_info = self.geometry_functions.get(lookup_type, False)
if lookup_info:
sql = "%s(%s, %s)" % (lookup_info, geo_col,
self.get_geom_placeholder(value, field.srid))
return sql, []
# TODO: Is this really necessary? MySQL can't handle NULL geometries
# in its spatial indexes anyways.
if lookup_type == 'isnull':
return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))

View File

@ -244,6 +244,12 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
else: else:
return 'SDO_GEOMETRY(%%s, %s)' % f.srid return 'SDO_GEOMETRY(%%s, %s)' % f.srid
def check_relate_argument(self, arg):
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
if not self.mask_regex.match(arg):
raise ValueError('Invalid SDO_RELATE mask: "%s"' % (self.relate_func, arg))
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction." "Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
geo_col, db_type = lvalue geo_col, db_type = lvalue

View File

@ -1,73 +1,46 @@
import re import re
from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.utils import SpatialOperation, SpatialFunction
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql_psycopg2.base import DatabaseOperations from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
from django.db.utils import ProgrammingError from django.db.utils import ProgrammingError
from django.utils import six
from django.utils.functional import cached_property from django.utils.functional import cached_property
from .models import PostGISGeometryColumns, PostGISSpatialRefSys from .models import PostGISGeometryColumns, PostGISSpatialRefSys
#### Classes used in constructing PostGIS spatial SQL #### class PostGISOperator(SpatialOperator):
class PostGISOperator(SpatialOperation): def __init__(self, geography=False, **kwargs):
"For PostGIS operators (e.g. `&&`, `~`)." # Only a subset of the operators and functions are available
def __init__(self, operator): # for the geography type.
super(PostGISOperator, self).__init__(operator=operator) self.geography = geography
super(PostGISOperator, self).__init__(**kwargs)
def as_sql(self, connection, lookup, *args):
if lookup.lhs.source.geography and not self.geography:
raise ValueError('PostGIS geography does not support the "%s" '
'function/operator.' % (self.func or self.op,))
return super(PostGISOperator, self).as_sql(connection, lookup, *args)
class PostGISFunction(SpatialFunction): class PostGISDistanceOperator(PostGISOperator):
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
def __init__(self, prefix, function, **kwargs):
super(PostGISFunction, self).__init__(prefix + function, **kwargs)
def as_sql(self, connection, lookup, template_params, sql_params):
class PostGISFunctionParam(PostGISFunction): if not lookup.lhs.source.geography and lookup.lhs.source.geodetic(connection):
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)." sql_template = self.sql_template
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)' if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid':
template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'})
sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s) %(op)s %%s'
class PostGISDistance(PostGISFunction): else:
"For PostGIS distance operations." template_params.update({'op': self.op, 'func': 'ST_Distance_Sphere'})
dist_func = 'Distance' return sql_template % template_params, sql_params
sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s' return super(PostGISDistanceOperator, self).as_sql(connection, lookup, template_params, sql_params)
def __init__(self, prefix, operator):
super(PostGISDistance, self).__init__(prefix, self.dist_func,
operator=operator)
class PostGISSpheroidDistance(PostGISFunction):
"For PostGIS spherical distance operations (using the spheroid)."
dist_func = 'distance_spheroid'
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
def __init__(self, prefix, operator):
# An extra parameter in `end_subst` is needed for the spheroid string.
super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
operator=operator)
class PostGISSphereDistance(PostGISDistance):
"For PostGIS spherical distance operations."
dist_func = 'distance_sphere'
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls."
pattern_regex = re.compile(r'^[012TF\*]{9}$')
def __init__(self, prefix, pattern):
if not self.pattern_regex.match(pattern):
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
super(PostGISRelate, self).__init__(prefix, 'Relate')
class PostGISOperations(DatabaseOperations, BaseSpatialOperations): class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
@ -82,104 +55,43 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
Adapter = PostGISAdapter Adapter = PostGISAdapter
Adaptor = Adapter # Backwards-compatibility alias. Adaptor = Adapter # Backwards-compatibility alias.
gis_operators = {
'bbcontains': PostGISOperator(op='~'),
'bboverlaps': PostGISOperator(op='&&', geography=True),
'contained': PostGISOperator(op='@'),
'contains': PostGISOperator(func='ST_Contains'),
'overlaps_left': PostGISOperator(op='&<'),
'overlaps_right': PostGISOperator(op='&>'),
'overlaps_below': PostGISOperator(op='&<|'),
'overlaps_above': PostGISOperator(op='|&>'),
'left': PostGISOperator(op='<<'),
'right': PostGISOperator(op='>>'),
'strictly_below': PostGISOperator(op='<<|'),
'stricly_above': PostGISOperator(op='|>>'),
'same_as': PostGISOperator(op='~='),
'exact': PostGISOperator(op='~='), # alias of same_as
'contains_properly': PostGISOperator(func='ST_ContainsProperly'),
'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True),
'covers': PostGISOperator(func='ST_Covers', geography=True),
'crosses': PostGISOperator(func='ST_Crosses)'),
'disjoint': PostGISOperator(func='ST_Disjoint'),
'equals': PostGISOperator(func='ST_Equals'),
'intersects': PostGISOperator(func='ST_Intersects', geography=True),
'overlaps': PostGISOperator(func='ST_Overlaps'),
'relate': PostGISOperator(func='ST_Relate'),
'touches': PostGISOperator(func='ST_Touches'),
'within': PostGISOperator(func='ST_Within'),
'dwithin': PostGISOperator(func='ST_DWithin', geography=True),
'distance_gt': PostGISDistanceOperator(func='ST_Distance', op='>', geography=True),
'distance_gte': PostGISDistanceOperator(func='ST_Distance', op='>=', geography=True),
'distance_lt': PostGISDistanceOperator(func='ST_Distance', op='<', geography=True),
'distance_lte': PostGISDistanceOperator(func='ST_Distance', op='<=', geography=True),
}
def __init__(self, connection): def __init__(self, connection):
super(PostGISOperations, self).__init__(connection) super(PostGISOperations, self).__init__(connection)
prefix = self.geom_func_prefix prefix = self.geom_func_prefix
# PostGIS-specific operators. The commented descriptions of these
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
self.geometry_operators = {
# The "&<" operator returns true if A's bounding box overlaps or
# is to the left of B's bounding box.
'overlaps_left': PostGISOperator('&<'),
# The "&>" operator returns true if A's bounding box overlaps or
# is to the right of B's bounding box.
'overlaps_right': PostGISOperator('&>'),
# The "<<" operator returns true if A's bounding box is strictly
# to the left of B's bounding box.
'left': PostGISOperator('<<'),
# The ">>" operator returns true if A's bounding box is strictly
# to the right of B's bounding box.
'right': PostGISOperator('>>'),
# The "&<|" operator returns true if A's bounding box overlaps or
# is below B's bounding box.
'overlaps_below': PostGISOperator('&<|'),
# The "|&>" operator returns true if A's bounding box overlaps or
# is above B's bounding box.
'overlaps_above': PostGISOperator('|&>'),
# The "<<|" operator returns true if A's bounding box is strictly
# below B's bounding box.
'strictly_below': PostGISOperator('<<|'),
# The "|>>" operator returns true if A's bounding box is strictly
# above B's bounding box.
'strictly_above': PostGISOperator('|>>'),
# The "~=" operator is the "same as" operator. It tests actual
# geometric equality of two features. So if A and B are the same feature,
# vertex-by-vertex, the operator returns true.
'same_as': PostGISOperator('~='),
'exact': PostGISOperator('~='),
# The "@" operator returns true if A's bounding box is completely contained
# by B's bounding box.
'contained': PostGISOperator('@'),
# The "~" operator returns true if A's bounding box completely contains
# by B's bounding box.
'bbcontains': PostGISOperator('~'),
# The "&&" operator returns true if A's bounding box overlaps
# B's bounding box.
'bboverlaps': PostGISOperator('&&'),
}
self.geometry_functions = {
'equals': PostGISFunction(prefix, 'Equals'),
'disjoint': PostGISFunction(prefix, 'Disjoint'),
'touches': PostGISFunction(prefix, 'Touches'),
'crosses': PostGISFunction(prefix, 'Crosses'),
'within': PostGISFunction(prefix, 'Within'),
'overlaps': PostGISFunction(prefix, 'Overlaps'),
'contains': PostGISFunction(prefix, 'Contains'),
'intersects': PostGISFunction(prefix, 'Intersects'),
'relate': (PostGISRelate, six.string_types),
'coveredby': PostGISFunction(prefix, 'CoveredBy'),
'covers': PostGISFunction(prefix, 'Covers'),
'contains_properly': PostGISFunction(prefix, 'ContainsProperly'),
}
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float) + six.integer_types
def get_dist_ops(operator):
"Returns operations for both regular and spherical distances."
return {'cartesian': PostGISDistance(prefix, operator),
'sphere': PostGISSphereDistance(prefix, operator),
'spheroid': PostGISSpheroidDistance(prefix, operator),
}
self.distance_functions = {
'distance_gt': (get_dist_ops('>'), dtypes),
'distance_gte': (get_dist_ops('>='), dtypes),
'distance_lt': (get_dist_ops('<'), dtypes),
'distance_lte': (get_dist_ops('<='), dtypes),
'dwithin': (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
}
# Adding the distance functions to the geometries lookup.
self.geometry_functions.update(self.distance_functions)
# Only a subset of the operators and functions are available
# for the geography type.
self.geography_functions = self.distance_functions.copy()
self.geography_functions.update({
'coveredby': self.geometry_functions['coveredby'],
'covers': self.geometry_functions['covers'],
'intersects': self.geometry_functions['intersects'],
})
self.geography_operators = {
'bboverlaps': PostGISOperator('&&'),
}
# Creating a dictionary lookup of all GIS terms for PostGIS.
self.gis_terms = {'isnull'}
self.gis_terms.update(self.geometry_operators)
self.gis_terms.update(self.geometry_functions)
self.area = prefix + 'Area' self.area = prefix + 'Area'
self.bounding_circle = prefix + 'MinimumBoundingCircle' self.bounding_circle = prefix + 'MinimumBoundingCircle'
@ -452,95 +364,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
else: else:
raise Exception('Could not determine PROJ.4 version from PostGIS.') raise Exception('Could not determine PROJ.4 version from PostGIS.')
def num_params(self, lookup_type, num_param):
"""
Helper routine that returns a boolean indicating whether the number of
parameters is correct for the lookup type.
"""
def exactly_two(np):
return np == 2
def two_to_three(np):
return np >= 2 and np <= 3
if (lookup_type in self.distance_functions and
lookup_type != 'dwithin'):
return two_to_three(num_param)
else:
return exactly_two(num_param)
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"""
Constructs spatial SQL from the given lookup value tuple a
(alias, col, db_type), the lookup type string, lookup value, and
the geometry field.
"""
geo_col, db_type = lvalue
if lookup_type in self.geometry_operators:
if field.geography and lookup_type not in self.geography_operators:
raise ValueError('PostGIS geography does not support the '
'"%s" lookup.' % lookup_type)
# Handling a PostGIS operator.
op = self.geometry_operators[lookup_type]
return op.as_sql(geo_col, self.get_geom_placeholder(field, value))
elif lookup_type in self.geometry_functions:
if field.geography and lookup_type not in self.geography_functions:
raise ValueError('PostGIS geography type does not support the '
'"%s" lookup.' % lookup_type)
# See if a PostGIS geometry function matches the lookup type.
tmp = self.geometry_functions[lookup_type]
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# distance lookups.
if isinstance(tmp, tuple):
# First element of tuple is the PostGISOperation instance, and the
# second element is either the type or a tuple of acceptable types
# that may passed in as further parameters for the lookup type.
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, (tuple, list)):
raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
# Geometry is first element of lookup tuple.
geom = value[0]
# Number of valid tuple parameters depends on the lookup type.
nparams = len(value)
if not self.num_params(lookup_type, nparams):
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
if lookup_type == 'relate':
op = op(self.geom_func_prefix, value[1])
elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
if not field.geography and field.geodetic(self.connection):
# Setting up the geodetic operation appropriately.
if nparams == 3 and value[2] == 'spheroid':
op = op['spheroid']
else:
op = op['sphere']
else:
op = op['cartesian']
else:
op = tmp
geom = value
# Calling the `as_sql` function on the operation instance.
return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
def spatial_aggregate_sql(self, agg): def spatial_aggregate_sql(self, agg):
""" """
Returns the spatial aggregate SQL template and function for the Returns the spatial aggregate SQL template and function for the

View File

@ -1,9 +1,8 @@
import re import re
import sys import sys
from decimal import Decimal
from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.utils import SpatialOperation, SpatialFunction from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
@ -14,52 +13,6 @@ from django.utils import six
from django.utils.functional import cached_property from django.utils.functional import cached_property
class SpatiaLiteOperator(SpatialOperation):
"For SpatiaLite operators (e.g. `&&`, `~`)."
def __init__(self, operator):
super(SpatiaLiteOperator, self).__init__(operator=operator)
class SpatiaLiteFunction(SpatialFunction):
"For SpatiaLite function calls."
def __init__(self, function, **kwargs):
super(SpatiaLiteFunction, self).__init__(function, **kwargs)
class SpatiaLiteFunctionParam(SpatiaLiteFunction):
"For SpatiaLite functions that take another parameter."
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
class SpatiaLiteDistance(SpatiaLiteFunction):
"For SpatiaLite distance operations."
dist_func = 'Distance'
sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
def __init__(self, operator):
super(SpatiaLiteDistance, self).__init__(self.dist_func,
operator=operator)
class SpatiaLiteRelate(SpatiaLiteFunctionParam):
"For SpatiaLite Relate(<geom>, <pattern>) calls."
pattern_regex = re.compile(r'^[012TF\*]{9}$')
def __init__(self, pattern):
if not self.pattern_regex.match(pattern):
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
super(SpatiaLiteRelate, self).__init__('Relate')
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float) + six.integer_types
def get_dist_ops(operator):
"Returns operations for regular distances; spherical distances are not currently supported."
return (SpatiaLiteDistance(operator),)
class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
compiler_module = 'django.contrib.gis.db.models.sql.compiler' compiler_module = 'django.contrib.gis.db.models.sql.compiler'
name = 'spatialite' name = 'spatialite'
@ -101,42 +54,32 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
from_wkb = 'GeomFromWKB' from_wkb = 'GeomFromWKB'
select = 'AsText(%s)' select = 'AsText(%s)'
geometry_functions = { gis_operators = {
'equals': SpatiaLiteFunction('Equals'), 'equals': SpatialOperator(func='Equals'),
'disjoint': SpatiaLiteFunction('Disjoint'), 'disjoint': SpatialOperator(func='Disjoint'),
'touches': SpatiaLiteFunction('Touches'), 'touches': SpatialOperator(func='Touches'),
'crosses': SpatiaLiteFunction('Crosses'), 'crosses': SpatialOperator(func='Crosses'),
'within': SpatiaLiteFunction('Within'), 'within': SpatialOperator(func='Within'),
'overlaps': SpatiaLiteFunction('Overlaps'), 'overlaps': SpatialOperator(func='Overlaps'),
'contains': SpatiaLiteFunction('Contains'), 'contains': SpatialOperator(func='Contains'),
'intersects': SpatiaLiteFunction('Intersects'), 'intersects': SpatialOperator(func='Intersects'),
'relate': (SpatiaLiteRelate, six.string_types), 'relate': SpatialOperator(func='Relate'),
# Returns true if B's bounding box completely contains A's bounding box. # Returns true if B's bounding box completely contains A's bounding box.
'contained': SpatiaLiteFunction('MbrWithin'), 'contained': SpatialOperator(func='MbrWithin'),
# Returns true if A's bounding box completely contains B's bounding box. # Returns true if A's bounding box completely contains B's bounding box.
'bbcontains': SpatiaLiteFunction('MbrContains'), 'bbcontains': SpatialOperator(func='MbrContains'),
# Returns true if A's bounding box overlaps B's bounding box. # Returns true if A's bounding box overlaps B's bounding box.
'bboverlaps': SpatiaLiteFunction('MbrOverlaps'), 'bboverlaps': SpatialOperator(func='MbrOverlaps'),
# These are implemented here as synonyms for Equals # These are implemented here as synonyms for Equals
'same_as': SpatiaLiteFunction('Equals'), 'same_as': SpatialOperator(func='Equals'),
'exact': SpatiaLiteFunction('Equals'), 'exact': SpatialOperator(func='Equals'),
'distance_gt': SpatialOperator(func='Distance', op='>'),
'distance_gte': SpatialOperator(func='Distance', op='>='),
'distance_lt': SpatialOperator(func='Distance', op='<'),
'distance_lte': SpatialOperator(func='Distance', op='<='),
} }
distance_functions = {
'distance_gt': (get_dist_ops('>'), dtypes),
'distance_gte': (get_dist_ops('>='), dtypes),
'distance_lt': (get_dist_ops('<'), dtypes),
'distance_lte': (get_dist_ops('<='), dtypes),
}
geometry_functions.update(distance_functions)
def __init__(self, connection):
super(DatabaseOperations, self).__init__(connection)
# Creating the GIS terms dictionary.
self.gis_terms = {'isnull'}
self.gis_terms.update(self.geometry_functions)
@cached_property @cached_property
def spatial_version(self): def spatial_version(self):
"""Determine the version of the SpatiaLite library.""" """Determine the version of the SpatiaLite library."""
@ -316,58 +259,6 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
sql_function = getattr(self, agg_name) sql_function = getattr(self, agg_name)
return sql_template, sql_function return sql_template, sql_function
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"""
Returns the SpatiaLite-specific SQL for the given lookup value
[a tuple of (alias, column, db_type)], lookup type, lookup
value, the model field, and the quoting function.
"""
geo_col, db_type = lvalue
if lookup_type in self.geometry_functions:
# See if a SpatiaLite geometry function matches the lookup type.
tmp = self.geometry_functions[lookup_type]
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# distance lookups.
if isinstance(tmp, tuple):
# First element of tuple is the SpatiaLiteOperation instance, and the
# second element is either the type or a tuple of acceptable types
# that may passed in as further parameters for the lookup type.
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, (tuple, list)):
raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
# Geometry is first element of lookup tuple.
geom = value[0]
# Number of valid tuple parameters depends on the lookup type.
if len(value) != 2:
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
if lookup_type == 'relate':
op = op(value[1])
elif lookup_type in self.distance_functions:
op = op[0]
else:
op = tmp
geom = value
# Calling the `as_sql` function on the operation instance.
return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
# Routines for getting the OGC-compliant models. # Routines for getting the OGC-compliant models.
def geometry_columns(self): def geometry_columns(self):
from django.contrib.gis.db.backends.spatialite.models import SpatialiteGeometryColumns from django.contrib.gis.db.backends.spatialite.models import SpatialiteGeometryColumns

View File

@ -4,43 +4,24 @@ backends.
""" """
class SpatialOperation(object): class SpatialOperator(object):
""" """
Base class for generating spatial SQL. Class encapsulating the behavior specific to a GIS operation (used by lookups).
""" """
sql_template = '%(geo_col)s %(operator)s %(geometry)s' sql_template = None
def __init__(self, function='', operator='', result='', **kwargs): def __init__(self, op=None, func=None):
self.function = function self.op = op
self.operator = operator self.func = func
self.result = result
self.extra = kwargs
def as_sql(self, geo_col, geometry='%s'): @property
return self.sql_template % self.params(geo_col, geometry), [] def default_template(self):
if self.func:
return '%(func)s(%(lhs)s, %(rhs)s)'
else:
return '%(lhs)s %(op)s %(rhs)s'
def params(self, geo_col, geometry): def as_sql(self, connection, lookup, template_params, sql_params):
params = {'function': self.function, sql_template = self.sql_template or lookup.sql_template or self.default_template
'geo_col': geo_col, template_params.update({'op': self.op, 'func': self.func})
'geometry': geometry, return sql_template % template_params, sql_params
'operator': self.operator,
'result': self.result,
}
params.update(self.extra)
return params
class SpatialFunction(SpatialOperation):
"""
Base class for generating spatial SQL related to a function.
"""
sql_template = '%(function)s(%(geo_col)s, %(geometry)s)'
def __init__(self, func, result='', operator='', **kwargs):
# Getting the function prefix.
default = {'function': func,
'operator': operator,
'result': result
}
kwargs.update(default)
super(SpatialFunction, self).__init__(**kwargs)

View File

@ -1,15 +0,0 @@
from django.db.models.sql.constants import QUERY_TERMS
GIS_LOOKUPS = {
'bbcontains', 'bboverlaps', 'contained', 'contains',
'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint',
'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte',
'dwithin', 'equals', 'exact',
'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within',
'left', 'right', 'overlaps_left', 'overlaps_right',
'overlaps_above', 'overlaps_below',
'strictly_above', 'strictly_below'
}
ALL_TERMS = GIS_LOOKUPS | QUERY_TERMS
__all__ = ['ALL_TERMS', 'GIS_LOOKUPS']

View File

@ -2,8 +2,7 @@ from django.db.models.fields import Field
from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.expressions import SQLEvaluator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.gis import forms from django.contrib.gis import forms
from django.contrib.gis.db.models.constants import GIS_LOOKUPS from django.contrib.gis.db.models.lookups import gis_lookups
from django.contrib.gis.db.models.lookups import GISLookup
from django.contrib.gis.db.models.proxy import GeometryProxy from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geometry.backend import Geometry, GeometryException from django.contrib.gis.geometry.backend import Geometry, GeometryException
from django.utils import six from django.utils import six
@ -243,16 +242,15 @@ class GeometryField(Field):
parameters into the correct units for the coordinate system of the parameters into the correct units for the coordinate system of the
field. field.
""" """
if lookup_type in connection.ops.gis_terms:
# special case for isnull lookup # special case for isnull lookup
if lookup_type == 'isnull': if lookup_type == 'isnull':
return [] return []
elif lookup_type in self.class_lookups:
# Populating the parameters list, and wrapping the Geometry # Populating the parameters list, and wrapping the Geometry
# with the Adapter of the spatial backend. # with the Adapter of the spatial backend.
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
params = [connection.ops.Adapter(value[0])] params = [connection.ops.Adapter(value[0])]
if lookup_type in connection.ops.distance_functions: if self.class_lookups[lookup_type].distance:
# Getting the distance parameter in the units of the field. # Getting the distance parameter in the units of the field.
params += self.get_distance(value[1:], lookup_type, connection) params += self.get_distance(value[1:], lookup_type, connection)
elif lookup_type in connection.ops.truncate_params: elif lookup_type in connection.ops.truncate_params:
@ -291,9 +289,9 @@ class GeometryField(Field):
""" """
return connection.ops.get_geom_placeholder(self, value) return connection.ops.get_geom_placeholder(self, value)
for lookup_name in GIS_LOOKUPS:
lookup = type(lookup_name, (GISLookup,), {'lookup_name': lookup_name}) for klass in gis_lookups.values():
GeometryField.register_lookup(lookup) GeometryField.register_lookup(klass)
# The OpenGIS Geometry Type Fields # The OpenGIS Geometry Type Fields

View File

@ -1,10 +1,20 @@
from __future__ import unicode_literals
import re
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.db.models.lookups import Lookup from django.db.models.lookups import Lookup
from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.expressions import SQLEvaluator
from django.utils import six
gis_lookups = {}
class GISLookup(Lookup): class GISLookup(Lookup):
sql_template = None
transform_func = None
distance = False
@classmethod @classmethod
def _check_geo_field(cls, opts, lookup): def _check_geo_field(cls, opts, lookup):
""" """
@ -45,10 +55,19 @@ class GISLookup(Lookup):
else: else:
return False return False
def as_sql(self, qn, connection): def get_db_prep_lookup(self, value, connection):
# We use the same approach as was used by GeoWhereNode. It would # get_db_prep_lookup is called by process_rhs from super class
# be a good idea to upgrade GIS to use similar code that is used if isinstance(value, (tuple, list)):
# for other lookups. # First param is assumed to be the geometric object
params = [connection.ops.Adapter(value[0])] + list(value)[1:]
else:
params = [connection.ops.Adapter(value)]
return ('%s', params)
def process_rhs(self, qn, connection):
rhs, rhs_params = super(GISLookup, self).process_rhs(qn, connection)
geom = self.rhs
if isinstance(self.rhs, SQLEvaluator): if isinstance(self.rhs, SQLEvaluator):
# Make sure the F Expression destination field exists, and # Make sure the F Expression destination field exists, and
# set an `srid` attribute with the same as that of the # set an `srid` attribute with the same as that of the
@ -57,13 +76,256 @@ class GISLookup(Lookup):
if not geo_fld: if not geo_fld:
raise ValueError('No geographic field found in expression.') raise ValueError('No geographic field found in expression.')
self.rhs.srid = geo_fld.srid self.rhs.srid = geo_fld.srid
db_type = self.lhs.output_field.db_type(connection=connection) elif isinstance(self.rhs, (list, tuple)):
params = self.lhs.output_field.get_db_prep_lookup( geom = self.rhs[0]
self.lookup_name, self.rhs, connection=connection)
lhs_sql, lhs_params = self.process_lhs(qn, connection) rhs = connection.ops.get_geom_placeholder(self.lhs.source, geom)
# lhs_params not currently supported. return rhs, rhs_params
assert not lhs_params
data = (lhs_sql, db_type) def as_sql(self, qn, connection):
spatial_sql, spatial_params = connection.ops.spatial_lookup_sql( lhs_sql, sql_params = self.process_lhs(qn, connection)
data, self.lookup_name, self.rhs, self.lhs.output_field, qn) rhs_sql, rhs_params = self.process_rhs(qn, connection)
return spatial_sql, spatial_params + params sql_params.extend(rhs_params)
template_params = {'lhs': lhs_sql, 'rhs': rhs_sql}
backend_op = connection.ops.gis_operators[self.lookup_name]
return backend_op.as_sql(connection, self, template_params, sql_params)
# ------------------
# Geometry operators
# ------------------
class OverlapsLeftLookup(GISLookup):
"""
The overlaps_left operator returns true if A's bounding box overlaps or is to the
left of B's bounding box.
"""
lookup_name = 'overlaps_left'
gis_lookups['overlaps_left'] = OverlapsLeftLookup
class OverlapsRightLookup(GISLookup):
"""
The 'overlaps_right' operator returns true if A's bounding box overlaps or is to the
right of B's bounding box.
"""
lookup_name = 'overlaps_right'
gis_lookups['overlaps_right'] = OverlapsRightLookup
class OverlapsBelowLookup(GISLookup):
"""
The 'overlaps_below' operator returns true if A's bounding box overlaps or is below
B's bounding box.
"""
lookup_name = 'overlaps_below'
gis_lookups['overlaps_below'] = OverlapsBelowLookup
class OverlapsAboveLookup(GISLookup):
"""
The 'overlaps_above' operator returns true if A's bounding box overlaps or is above
B's bounding box.
"""
lookup_name = 'overlaps_above'
gis_lookups['overlaps_above'] = OverlapsAboveLookup
class LeftLookup(GISLookup):
"""
The 'left' operator returns true if A's bounding box is strictly to the left
of B's bounding box.
"""
lookup_name = 'left'
gis_lookups['left'] = LeftLookup
class RightLookup(GISLookup):
"""
The 'right' operator returns true if A's bounding box is strictly to the right
of B's bounding box.
"""
lookup_name = 'right'
gis_lookups['right'] = RightLookup
class StrictlyBelowLookup(GISLookup):
"""
The 'strictly_below' operator returns true if A's bounding box is strictly below B's
bounding box.
"""
lookup_name = 'strictly_below'
gis_lookups['strictly_below'] = StrictlyBelowLookup
class StrictlyAboveLookup(GISLookup):
"""
The 'strictly_above' operator returns true if A's bounding box is strictly above B's
bounding box.
"""
lookup_name = 'strictly_above'
gis_lookups['strictly_above'] = StrictlyAboveLookup
class SameAsLookup(GISLookup):
"""
The "~=" operator is the "same as" operator. It tests actual geometric
equality of two features. So if A and B are the same feature,
vertex-by-vertex, the operator returns true.
"""
lookup_name = 'same_as'
gis_lookups['same_as'] = SameAsLookup
class ExactLookup(SameAsLookup):
# Alias of same_as
lookup_name = 'exact'
gis_lookups['exact'] = ExactLookup
class BBContainsLookup(GISLookup):
"""
The 'bbcontains' operator returns true if A's bounding box completely contains
by B's bounding box.
"""
lookup_name = 'bbcontains'
gis_lookups['bbcontains'] = BBContainsLookup
class BBOverlapsLookup(GISLookup):
"""
The 'bboverlaps' operator returns true if A's bounding box overlaps B's bounding box.
"""
lookup_name = 'bboverlaps'
gis_lookups['bboverlaps'] = BBOverlapsLookup
class ContainedLookup(GISLookup):
"""
The 'contained' operator returns true if A's bounding box is completely contained
by B's bounding box.
"""
lookup_name = 'contained'
gis_lookups['contained'] = ContainedLookup
# ------------------
# Geometry functions
# ------------------
class ContainsLookup(GISLookup):
lookup_name = 'contains'
gis_lookups['contains'] = ContainsLookup
class ContainsProperlyLookup(GISLookup):
lookup_name = 'contains_properly'
gis_lookups['contains_properly'] = ContainsProperlyLookup
class CoveredByLookup(GISLookup):
lookup_name = 'coveredby'
gis_lookups['coveredby'] = CoveredByLookup
class CoversLookup(GISLookup):
lookup_name = 'covers'
gis_lookups['covers'] = CoversLookup
class CrossesLookup(GISLookup):
lookup_name = 'crosses'
gis_lookups['crosses'] = CrossesLookup
class DisjointLookup(GISLookup):
lookup_name = 'disjoint'
gis_lookups['disjoint'] = DisjointLookup
class EqualsLookup(GISLookup):
lookup_name = 'equals'
gis_lookups['equals'] = EqualsLookup
class IntersectsLookup(GISLookup):
lookup_name = 'intersects'
gis_lookups['intersects'] = IntersectsLookup
class OverlapsLookup(GISLookup):
lookup_name = 'overlaps'
gis_lookups['overlaps'] = OverlapsLookup
class RelateLookup(GISLookup):
lookup_name = 'relate'
sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)'
pattern_regex = re.compile(r'^[012TF\*]{9}$')
def get_db_prep_lookup(self, value, connection):
if len(value) != 2:
raise ValueError('relate must be passed a two-tuple')
# Check the pattern argument
backend_op = connection.ops.gis_operators[self.lookup_name]
if hasattr(backend_op, 'check_relate_argument'):
backend_op.check_relate_argument(value[1])
else:
pattern = value[1]
if not isinstance(pattern, six.string_types) or not self.pattern_regex.match(pattern):
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
return super(RelateLookup, self).get_db_prep_lookup(value, connection)
gis_lookups['relate'] = RelateLookup
class TouchesLookup(GISLookup):
lookup_name = 'touches'
gis_lookups['touches'] = TouchesLookup
class WithinLookup(GISLookup):
lookup_name = 'within'
gis_lookups['within'] = WithinLookup
class DistanceLookupBase(GISLookup):
distance = True
sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
def get_db_prep_lookup(self, value, connection):
if isinstance(value, (tuple, list)):
if not 2 <= len(value) <= 3:
raise ValueError("2 or 3-element tuple required for '%s' lookup." % self.lookup_name)
params = [connection.ops.Adapter(value[0])]
# Getting the distance parameter in the units of the field.
params += connection.ops.get_distance(self.lhs.output_field, value[1:], self.lookup_name)
return ('%s', params)
else:
return super(DistanceLookupBase, self).get_db_prep_lookup(value, connection)
class DWithinLookup(DistanceLookupBase):
lookup_name = 'dwithin'
sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)'
gis_lookups['dwithin'] = DWithinLookup
class DistanceGTLookup(DistanceLookupBase):
lookup_name = 'distance_gt'
gis_lookups['distance_gt'] = DistanceGTLookup
class DistanceGTELookup(DistanceLookupBase):
lookup_name = 'distance_gte'
gis_lookups['distance_gte'] = DistanceGTELookup
class DistanceLTLookup(DistanceLookupBase):
lookup_name = 'distance_lt'
gis_lookups['distance_lt'] = DistanceLTLookup
class DistanceLTELookup(DistanceLookupBase):
lookup_name = 'distance_lte'
gis_lookups['distance_lte'] = DistanceLTELookup

View File

@ -1,7 +1,7 @@
from django.db import connections from django.db import connections
from django.db.models.query import sql from django.db.models.query import sql
from django.db.models.sql.constants import QUERY_TERMS
from django.contrib.gis.db.models.constants import ALL_TERMS
from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.db.models.lookups import GISLookup from django.contrib.gis.db.models.lookups import GISLookup
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
@ -13,7 +13,7 @@ class GeoQuery(sql.Query):
A single spatial SQL query. A single spatial SQL query.
""" """
# Overridding the valid query terms. # Overridding the valid query terms.
query_terms = ALL_TERMS query_terms = QUERY_TERMS | set(GeometryField.class_lookups.keys())
aggregates_module = gis_aggregates aggregates_module = gis_aggregates
compiler = 'GeoSQLCompiler' compiler = 'GeoSQLCompiler'

View File

@ -612,6 +612,9 @@ Miscellaneous
:func:`~django.core.urlresolvers.reverse_lazy` now return Unicode strings :func:`~django.core.urlresolvers.reverse_lazy` now return Unicode strings
instead of byte strings. instead of byte strings.
* GIS-specific lookups have been refactored to use the
:class:`django.db.models.Lookup` API.
.. _deprecated-features-1.8: .. _deprecated-features-1.8:
Features deprecated in 1.8 Features deprecated in 1.8