--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/contrib/gis/db/backend/postgis/query.py Tue Oct 14 16:00:59 2008 +0000
@@ -0,0 +1,287 @@
+"""
+ 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))