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:
parent
4ef9618e12
commit
2bd1bbc424
|
@ -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')
|
||||||
|
|
|
@ -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))
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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']
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue