| """ |
| 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)) |