app/django/contrib/gis/db/backend/postgis/query.py
author Mario Ferraro <fadinlight@gmail.com>
Sun, 15 Nov 2009 22:12:20 +0100
changeset 3093 d1be59b6b627
parent 323 ff1a9aa48cfd
permissions -rw-r--r--
GMaps related JS changed to use new google namespace. Google is going to change permanently in the future the way to load its services, so better stay safe. Also this commit shows uses of the new melange.js module. Fixes Issue 634.

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