web/lib/django/contrib/gis/db/backend/postgis/query.py
changeset 0 0d40e90630ef
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 """
       
     2  This module contains the spatial lookup types, and the get_geo_where_clause()
       
     3  routine for PostGIS.
       
     4 """
       
     5 
       
     6 import re
       
     7 from decimal import Decimal
       
     8 from django.db import connection
       
     9 from django.conf import settings
       
    10 from django.contrib.gis.measure import Distance
       
    11 from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
       
    12 
       
    13 qn = connection.ops.quote_name
       
    14 
       
    15 # Get the PostGIS version information.
       
    16 # To avoid the need to do a database query to determine the PostGIS version
       
    17 # each time the server starts up, one can optionally specify a
       
    18 # POSTGIS_VERSION setting. This setting is intentionally undocumented and
       
    19 # should be considered experimental, because an upcoming GIS backend
       
    20 # refactoring might remove the need for it.
       
    21 if hasattr(settings, 'POSTGIS_VERSION') and settings.POSTGIS_VERSION is not None:
       
    22     version_tuple = settings.POSTGIS_VERSION
       
    23 else:
       
    24     # This import is intentionally within the 'else' so that it isn't executed
       
    25     # if the POSTGIS_VERSION setting is available.
       
    26     from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
       
    27     version_tuple = postgis_version_tuple()
       
    28 POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = version_tuple
       
    29 
       
    30 # The supported PostGIS versions.
       
    31 #  TODO: Confirm tests with PostGIS versions 1.1.x -- should work.  
       
    32 #        Versions <= 1.0.x do not use GEOS C API, and will not be supported.
       
    33 if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
       
    34     raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION)
       
    35 
       
    36 # Versions of PostGIS >= 1.2.2 changed their naming convention to be
       
    37 #  'SQL-MM-centric' to conform with the ISO standard. Practically, this
       
    38 #  means that 'ST_' prefixes geometry function names.
       
    39 GEOM_FUNC_PREFIX = ''
       
    40 if MAJOR_VERSION >= 1:
       
    41     if (MINOR_VERSION1 > 2 or
       
    42         (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
       
    43         GEOM_FUNC_PREFIX = 'ST_'
       
    44 
       
    45     def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
       
    46 
       
    47     # Custom selection not needed for PostGIS because GEOS geometries are
       
    48     # instantiated directly from the HEXEWKB returned by default.  If
       
    49     # WKT is needed for some reason in the future, this value may be changed,
       
    50     # e.g,, 'AsText(%s)'.
       
    51     GEOM_SELECT = None
       
    52 
       
    53     # Functions used by the GeoManager & GeoQuerySet
       
    54     AREA = get_func('Area')
       
    55     ASGEOJSON = get_func('AsGeoJson')
       
    56     ASKML = get_func('AsKML')
       
    57     ASGML = get_func('AsGML')
       
    58     ASSVG = get_func('AsSVG')
       
    59     CENTROID = get_func('Centroid')
       
    60     COLLECT = get_func('Collect')
       
    61     DIFFERENCE = get_func('Difference')
       
    62     DISTANCE = get_func('Distance')
       
    63     DISTANCE_SPHERE = get_func('distance_sphere')
       
    64     DISTANCE_SPHEROID = get_func('distance_spheroid')
       
    65     ENVELOPE = get_func('Envelope')
       
    66     EXTENT = get_func('extent')
       
    67     GEOM_FROM_TEXT = get_func('GeomFromText')
       
    68     GEOM_FROM_WKB = get_func('GeomFromWKB')
       
    69     INTERSECTION = get_func('Intersection')
       
    70     LENGTH = get_func('Length')
       
    71     LENGTH_SPHEROID = get_func('length_spheroid')
       
    72     MAKE_LINE = get_func('MakeLine')
       
    73     MEM_SIZE = get_func('mem_size')
       
    74     NUM_GEOM = get_func('NumGeometries')
       
    75     NUM_POINTS = get_func('npoints')
       
    76     PERIMETER = get_func('Perimeter')
       
    77     POINT_ON_SURFACE = get_func('PointOnSurface')
       
    78     SCALE = get_func('Scale')
       
    79     SNAP_TO_GRID = get_func('SnapToGrid')
       
    80     SYM_DIFFERENCE = get_func('SymDifference')
       
    81     TRANSFORM = get_func('Transform')
       
    82     TRANSLATE = get_func('Translate')
       
    83 
       
    84     # Special cases for union, KML, and GeoJSON methods.
       
    85     if MINOR_VERSION1 < 3:
       
    86         UNIONAGG = 'GeomUnion'
       
    87         UNION = 'Union'
       
    88     else:
       
    89         UNIONAGG = 'ST_Union'
       
    90         UNION = 'ST_Union'
       
    91 
       
    92     if MINOR_VERSION1 == 1:
       
    93         ASKML = False
       
    94 
       
    95     # Only 1.3.4+ have AsGeoJson.
       
    96     if (MINOR_VERSION1 < 3 or 
       
    97         (MINOR_VERSION1 == 3 and MINOR_VERSION2 < 4)):
       
    98         ASGEOJSON = False
       
    99 else:
       
   100     raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
       
   101 
       
   102 #### Classes used in constructing PostGIS spatial SQL ####
       
   103 class PostGISOperator(SpatialOperation):
       
   104     "For PostGIS operators (e.g. `&&`, `~`)."
       
   105     def __init__(self, operator):
       
   106         super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
       
   107 
       
   108 class PostGISFunction(SpatialFunction):
       
   109     "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
       
   110     def __init__(self, function, **kwargs):
       
   111         super(PostGISFunction, self).__init__(get_func(function), **kwargs)
       
   112 
       
   113 class PostGISFunctionParam(PostGISFunction):
       
   114     "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
       
   115     def __init__(self, func):
       
   116         super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)')
       
   117 
       
   118 class PostGISDistance(PostGISFunction):
       
   119     "For PostGIS distance operations."
       
   120     dist_func = 'Distance'
       
   121     def __init__(self, operator):
       
   122         super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s', 
       
   123                                               operator=operator, result='%%s')
       
   124 
       
   125 class PostGISSpheroidDistance(PostGISFunction):
       
   126     "For PostGIS spherical distance operations (using the spheroid)."
       
   127     dist_func = 'distance_spheroid'
       
   128     def __init__(self, operator):
       
   129         # An extra parameter in `end_subst` is needed for the spheroid string.
       
   130         super(PostGISSpheroidDistance, self).__init__(self.dist_func, 
       
   131                                                       beg_subst='%s(%s, %%s, %%s', 
       
   132                                                       end_subst=') %s %s',
       
   133                                                       operator=operator, result='%%s')
       
   134 
       
   135 class PostGISSphereDistance(PostGISFunction):
       
   136     "For PostGIS spherical distance operations."
       
   137     dist_func = 'distance_sphere'
       
   138     def __init__(self, operator):
       
   139         super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
       
   140                                                     operator=operator, result='%%s')
       
   141                                                     
       
   142 class PostGISRelate(PostGISFunctionParam):
       
   143     "For PostGIS Relate(<geom>, <pattern>) calls."
       
   144     pattern_regex = re.compile(r'^[012TF\*]{9}$')
       
   145     def __init__(self, pattern):
       
   146         if not self.pattern_regex.match(pattern):
       
   147             raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
       
   148         super(PostGISRelate, self).__init__('Relate')
       
   149 
       
   150 #### Lookup type mapping dictionaries of PostGIS operations. ####
       
   151 
       
   152 # PostGIS-specific operators. The commented descriptions of these
       
   153 # operators come from Section 6.2.2 of the official PostGIS documentation.
       
   154 POSTGIS_OPERATORS = {
       
   155     # The "&<" operator returns true if A's bounding box overlaps or
       
   156     #  is to the left of B's bounding box.
       
   157     'overlaps_left' : PostGISOperator('&<'),
       
   158     # The "&>" operator returns true if A's bounding box overlaps or
       
   159     #  is to the right of B's bounding box.
       
   160     'overlaps_right' : PostGISOperator('&>'),
       
   161     # The "<<" operator returns true if A's bounding box is strictly
       
   162     #  to the left of B's bounding box.
       
   163     'left' : PostGISOperator('<<'),
       
   164     # The ">>" operator returns true if A's bounding box is strictly
       
   165     #  to the right of B's bounding box.
       
   166     'right' : PostGISOperator('>>'),
       
   167     # The "&<|" operator returns true if A's bounding box overlaps or
       
   168     #  is below B's bounding box.
       
   169     'overlaps_below' : PostGISOperator('&<|'),
       
   170     # The "|&>" operator returns true if A's bounding box overlaps or
       
   171     #  is above B's bounding box.
       
   172     'overlaps_above' : PostGISOperator('|&>'),
       
   173     # The "<<|" operator returns true if A's bounding box is strictly
       
   174     #  below B's bounding box.
       
   175     'strictly_below' : PostGISOperator('<<|'),
       
   176     # The "|>>" operator returns true if A's bounding box is strictly
       
   177     # above B's bounding box.
       
   178     'strictly_above' : PostGISOperator('|>>'),
       
   179     # The "~=" operator is the "same as" operator. It tests actual
       
   180     #  geometric equality of two features. So if A and B are the same feature,
       
   181     #  vertex-by-vertex, the operator returns true.
       
   182     'same_as' : PostGISOperator('~='),
       
   183     'exact' : PostGISOperator('~='),
       
   184     # The "@" operator returns true if A's bounding box is completely contained
       
   185     #  by B's bounding box.
       
   186     'contained' : PostGISOperator('@'),
       
   187     # The "~" operator returns true if A's bounding box completely contains
       
   188     #  by B's bounding box.
       
   189     'bbcontains' : PostGISOperator('~'),
       
   190     # The "&&" operator returns true if A's bounding box overlaps
       
   191     #  B's bounding box.
       
   192     'bboverlaps' : PostGISOperator('&&'),
       
   193     }
       
   194 
       
   195 # For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
       
   196 # first before calling the more computationally expensive GEOS routines (called
       
   197 # "inline index magic"):
       
   198 # 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
       
   199 # 'covers'.
       
   200 POSTGIS_GEOMETRY_FUNCTIONS = {
       
   201     'equals' : PostGISFunction('Equals'),
       
   202     'disjoint' : PostGISFunction('Disjoint'),
       
   203     'touches' : PostGISFunction('Touches'),
       
   204     'crosses' : PostGISFunction('Crosses'),
       
   205     'within' : PostGISFunction('Within'),
       
   206     'overlaps' : PostGISFunction('Overlaps'),
       
   207     'contains' : PostGISFunction('Contains'),
       
   208     'intersects' : PostGISFunction('Intersects'),
       
   209     'relate' : (PostGISRelate, basestring),
       
   210     }
       
   211 
       
   212 # Valid distance types and substitutions
       
   213 dtypes = (Decimal, Distance, float, int, long)
       
   214 def get_dist_ops(operator):
       
   215     "Returns operations for both regular and spherical distances."
       
   216     return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
       
   217 DISTANCE_FUNCTIONS = {
       
   218     'distance_gt' : (get_dist_ops('>'), dtypes),
       
   219     'distance_gte' : (get_dist_ops('>='), dtypes),
       
   220     'distance_lt' : (get_dist_ops('<'), dtypes),
       
   221     'distance_lte' : (get_dist_ops('<='), dtypes),
       
   222     }
       
   223 
       
   224 if GEOM_FUNC_PREFIX == 'ST_':
       
   225     # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
       
   226     POSTGIS_GEOMETRY_FUNCTIONS.update(
       
   227         {'coveredby' : PostGISFunction('CoveredBy'),
       
   228          'covers' : PostGISFunction('Covers'),
       
   229          })
       
   230     DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
       
   231 
       
   232 # Distance functions are a part of PostGIS geometry functions.
       
   233 POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
       
   234 
       
   235 # Any other lookup types that do not require a mapping.
       
   236 MISC_TERMS = ['isnull']
       
   237 
       
   238 # These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
       
   239 #  allowed for geographic queries.
       
   240 POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first
       
   241 POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions
       
   242 POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
       
   243 POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups
       
   244 
       
   245 # For checking tuple parameters -- not very pretty but gets job done.
       
   246 def exactly_two(val): return val == 2
       
   247 def two_to_three(val): return val >= 2 and val <=3
       
   248 def num_params(lookup_type, val):
       
   249     if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
       
   250     else: return exactly_two(val)
       
   251 
       
   252 #### The `get_geo_where_clause` function for PostGIS. ####
       
   253 def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
       
   254     "Returns the SQL WHERE clause for use in PostGIS SQL construction."
       
   255     # Getting the quoted field as `geo_col`.
       
   256     geo_col = '%s.%s' % (qn(table_alias), qn(name))
       
   257     if lookup_type in POSTGIS_OPERATORS:
       
   258         # See if a PostGIS operator matches the lookup type.
       
   259         return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
       
   260     elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
       
   261         # See if a PostGIS geometry function matches the lookup type.
       
   262         tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
       
   263 
       
   264         # Lookup types that are tuples take tuple arguments, e.g., 'relate' and 
       
   265         # distance lookups.
       
   266         if isinstance(tmp, tuple):
       
   267             # First element of tuple is the PostGISOperation instance, and the
       
   268             # second element is either the type or a tuple of acceptable types
       
   269             # that may passed in as further parameters for the lookup type.
       
   270             op, arg_type = tmp
       
   271 
       
   272             # Ensuring that a tuple _value_ was passed in from the user
       
   273             if not isinstance(geo_annot.value, (tuple, list)): 
       
   274                 raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
       
   275            
       
   276             # Number of valid tuple parameters depends on the lookup type.
       
   277             nparams = len(geo_annot.value)
       
   278             if not num_params(lookup_type, nparams):
       
   279                 raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
       
   280             
       
   281             # Ensuring the argument type matches what we expect.
       
   282             if not isinstance(geo_annot.value[1], arg_type):
       
   283                 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
       
   284 
       
   285             # For lookup type `relate`, the op instance is not yet created (has
       
   286             # to be instantiated here to check the pattern parameter).
       
   287             if lookup_type == 'relate': 
       
   288                 op = op(geo_annot.value[1])
       
   289             elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
       
   290                 if geo_annot.geodetic:
       
   291                     # Geodetic distances are only availble from Points to PointFields.
       
   292                     if geo_annot.geom_type != 'POINT':
       
   293                         raise TypeError('PostGIS spherical operations are only valid on PointFields.')
       
   294                     if geo_annot.value[0].geom_typeid != 0:
       
   295                         raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
       
   296                     # Setting up the geodetic operation appropriately.
       
   297                     if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2]
       
   298                     else: op = op[1]
       
   299                 else:
       
   300                     op = op[0]
       
   301         else:
       
   302             op = tmp
       
   303         # Calling the `as_sql` function on the operation instance.
       
   304         return op.as_sql(geo_col)
       
   305     elif lookup_type == 'isnull':
       
   306         # Handling 'isnull' lookup type
       
   307         return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
       
   308 
       
   309     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))