diff -r 6641e941ef1e -r ff1a9aa48cfd app/django/contrib/gis/db/backend/postgis/query.py --- /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(, ) 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))