You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
287 lines
13 KiB
287 lines
13 KiB
""" |
|
This module contains the spatial lookup types, and the get_geo_where_clause() |
|
routine for PostGIS. |
|
""" |
|
import re |
|
from decimal import Decimal |
|
from django.db import connection |
|
from django.contrib.gis.measure import Distance |
|
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple |
|
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction |
|
qn = connection.ops.quote_name |
|
|
|
# Getting the PostGIS version information |
|
POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple() |
|
|
|
# The supported PostGIS versions. |
|
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. |
|
# Versions <= 1.0.x do not use GEOS C API, and will not be supported. |
|
if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1): |
|
raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION) |
|
|
|
# Versions of PostGIS >= 1.2.2 changed their naming convention to be |
|
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this |
|
# means that 'ST_' prefixes geometry function names. |
|
GEOM_FUNC_PREFIX = '' |
|
if MAJOR_VERSION >= 1: |
|
if (MINOR_VERSION1 > 2 or |
|
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): |
|
GEOM_FUNC_PREFIX = 'ST_' |
|
|
|
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) |
|
|
|
# Custom selection not needed for PostGIS because GEOS geometries are |
|
# instantiated directly from the HEXEWKB returned by default. If |
|
# WKT is needed for some reason in the future, this value may be changed, |
|
# e.g,, 'AsText(%s)'. |
|
GEOM_SELECT = None |
|
|
|
# Functions used by the GeoManager & GeoQuerySet |
|
AREA = get_func('Area') |
|
ASKML = get_func('AsKML') |
|
ASGML = get_func('AsGML') |
|
ASSVG = get_func('AsSVG') |
|
CENTROID = get_func('Centroid') |
|
DIFFERENCE = get_func('Difference') |
|
DISTANCE = get_func('Distance') |
|
DISTANCE_SPHERE = get_func('distance_sphere') |
|
DISTANCE_SPHEROID = get_func('distance_spheroid') |
|
ENVELOPE = get_func('Envelope') |
|
EXTENT = get_func('extent') |
|
GEOM_FROM_TEXT = get_func('GeomFromText') |
|
GEOM_FROM_WKB = get_func('GeomFromWKB') |
|
INTERSECTION = get_func('Intersection') |
|
LENGTH = get_func('Length') |
|
LENGTH_SPHEROID = get_func('length_spheroid') |
|
MAKE_LINE = get_func('MakeLine') |
|
MEM_SIZE = get_func('mem_size') |
|
NUM_GEOM = get_func('NumGeometries') |
|
NUM_POINTS = get_func('npoints') |
|
PERIMETER = get_func('Perimeter') |
|
POINT_ON_SURFACE = get_func('PointOnSurface') |
|
SCALE = get_func('Scale') |
|
SYM_DIFFERENCE = get_func('SymDifference') |
|
TRANSFORM = get_func('Transform') |
|
TRANSLATE = get_func('Translate') |
|
|
|
# Special cases for union and KML methods. |
|
if MINOR_VERSION1 < 3: |
|
UNIONAGG = 'GeomUnion' |
|
UNION = 'Union' |
|
else: |
|
UNIONAGG = 'ST_Union' |
|
UNION = 'ST_Union' |
|
|
|
if MINOR_VERSION1 == 1: |
|
ASKML = False |
|
else: |
|
raise NotImplementedError('PostGIS versions < 1.0 are not supported.') |
|
|
|
#### Classes used in constructing PostGIS spatial SQL #### |
|
class PostGISOperator(SpatialOperation): |
|
"For PostGIS operators (e.g. `&&`, `~`)." |
|
def __init__(self, operator): |
|
super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s') |
|
|
|
class PostGISFunction(SpatialFunction): |
|
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." |
|
def __init__(self, function, **kwargs): |
|
super(PostGISFunction, self).__init__(get_func(function), **kwargs) |
|
|
|
class PostGISFunctionParam(PostGISFunction): |
|
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)." |
|
def __init__(self, func): |
|
super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)') |
|
|
|
class PostGISDistance(PostGISFunction): |
|
"For PostGIS distance operations." |
|
dist_func = 'Distance' |
|
def __init__(self, operator): |
|
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s', |
|
operator=operator, result='%%s') |
|
|
|
class PostGISSpheroidDistance(PostGISFunction): |
|
"For PostGIS spherical distance operations (using the spheroid)." |
|
dist_func = 'distance_spheroid' |
|
def __init__(self, operator): |
|
# An extra parameter in `end_subst` is needed for the spheroid string. |
|
super(PostGISSpheroidDistance, self).__init__(self.dist_func, |
|
beg_subst='%s(%s, %%s, %%s', |
|
end_subst=') %s %s', |
|
operator=operator, result='%%s') |
|
|
|
class PostGISSphereDistance(PostGISFunction): |
|
"For PostGIS spherical distance operations." |
|
dist_func = 'distance_sphere' |
|
def __init__(self, operator): |
|
super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s', |
|
operator=operator, result='%%s') |
|
|
|
class PostGISRelate(PostGISFunctionParam): |
|
"For PostGIS 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(PostGISRelate, self).__init__('Relate') |
|
|
|
#### Lookup type mapping dictionaries of PostGIS operations. #### |
|
|
|
# PostGIS-specific operators. The commented descriptions of these |
|
# operators come from Section 6.2.2 of the official PostGIS documentation. |
|
POSTGIS_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('&&'), |
|
} |
|
|
|
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query |
|
# first before calling the more computationally expensive GEOS routines (called |
|
# "inline index magic"): |
|
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and |
|
# 'covers'. |
|
POSTGIS_GEOMETRY_FUNCTIONS = { |
|
'equals' : PostGISFunction('Equals'), |
|
'disjoint' : PostGISFunction('Disjoint'), |
|
'touches' : PostGISFunction('Touches'), |
|
'crosses' : PostGISFunction('Crosses'), |
|
'within' : PostGISFunction('Within'), |
|
'overlaps' : PostGISFunction('Overlaps'), |
|
'contains' : PostGISFunction('Contains'), |
|
'intersects' : PostGISFunction('Intersects'), |
|
'relate' : (PostGISRelate, basestring), |
|
} |
|
|
|
# Valid distance types and substitutions |
|
dtypes = (Decimal, Distance, float, int, long) |
|
def get_dist_ops(operator): |
|
"Returns operations for both regular and spherical distances." |
|
return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator)) |
|
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), |
|
} |
|
|
|
if GEOM_FUNC_PREFIX == 'ST_': |
|
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ |
|
POSTGIS_GEOMETRY_FUNCTIONS.update( |
|
{'coveredby' : PostGISFunction('CoveredBy'), |
|
'covers' : PostGISFunction('Covers'), |
|
}) |
|
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes) |
|
|
|
# Distance functions are a part of PostGIS geometry functions. |
|
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) |
|
|
|
# Any other lookup types that do not require a mapping. |
|
MISC_TERMS = ['isnull'] |
|
|
|
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types |
|
# allowed for geographic queries. |
|
POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first |
|
POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions |
|
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') |
|
POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups |
|
|
|
# For checking tuple parameters -- not very pretty but gets job done. |
|
def exactly_two(val): return val == 2 |
|
def two_to_three(val): return val >= 2 and val <=3 |
|
def num_params(lookup_type, val): |
|
if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val) |
|
else: return exactly_two(val) |
|
|
|
#### The `get_geo_where_clause` function for PostGIS. #### |
|
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): |
|
"Returns the SQL WHERE clause for use in PostGIS SQL construction." |
|
# Getting the quoted field as `geo_col`. |
|
geo_col = '%s.%s' % (qn(table_alias), qn(name)) |
|
if lookup_type in POSTGIS_OPERATORS: |
|
# See if a PostGIS operator matches the lookup type. |
|
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) |
|
elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: |
|
# See if a PostGIS geometry function matches the lookup type. |
|
tmp = POSTGIS_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(geo_annot.value, (tuple, list)): |
|
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) |
|
|
|
# Number of valid tuple parameters depends on the lookup type. |
|
nparams = len(geo_annot.value) |
|
if not 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(geo_annot.value[1], arg_type): |
|
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.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(geo_annot.value[1]) |
|
elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': |
|
if geo_annot.geodetic: |
|
# Geodetic distances are only availble from Points to PointFields. |
|
if geo_annot.geom_type != 'POINT': |
|
raise TypeError('PostGIS spherical operations are only valid on PointFields.') |
|
if geo_annot.value[0].geom_typeid != 0: |
|
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.') |
|
# Setting up the geodetic operation appropriately. |
|
if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2] |
|
else: op = op[1] |
|
else: |
|
op = op[0] |
|
else: |
|
op = tmp |
|
# Calling the `as_sql` function on the operation instance. |
|
return op.as_sql(geo_col) |
|
elif lookup_type == 'isnull': |
|
# Handling 'isnull' lookup type |
|
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) |
|
|
|
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
|