web/lib/django/contrib/gis/db/backends/postgis/operations.py
changeset 29 cc9b7e14412b
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
       
     1 import re
       
     2 from decimal import Decimal
       
     3 
       
     4 from django.conf import settings
       
     5 from django.contrib.gis.db.backends.base import BaseSpatialOperations
       
     6 from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
       
     7 from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
       
     8 from django.contrib.gis.geometry.backend import Geometry
       
     9 from django.contrib.gis.measure import Distance
       
    10 from django.core.exceptions import ImproperlyConfigured
       
    11 from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
       
    12 from django.db.utils import DatabaseError
       
    13 
       
    14 #### Classes used in constructing PostGIS spatial SQL ####
       
    15 class PostGISOperator(SpatialOperation):
       
    16     "For PostGIS operators (e.g. `&&`, `~`)."
       
    17     def __init__(self, operator):
       
    18         super(PostGISOperator, self).__init__(operator=operator)
       
    19 
       
    20 class PostGISFunction(SpatialFunction):
       
    21     "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
       
    22     def __init__(self, prefix, function, **kwargs):
       
    23         super(PostGISFunction, self).__init__(prefix + function, **kwargs)
       
    24 
       
    25 class PostGISFunctionParam(PostGISFunction):
       
    26     "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
       
    27     sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
       
    28 
       
    29 class PostGISDistance(PostGISFunction):
       
    30     "For PostGIS distance operations."
       
    31     dist_func = 'Distance'
       
    32     sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
       
    33 
       
    34     def __init__(self, prefix, operator):
       
    35         super(PostGISDistance, self).__init__(prefix, self.dist_func,
       
    36                                               operator=operator)
       
    37 
       
    38 class PostGISSpheroidDistance(PostGISFunction):
       
    39     "For PostGIS spherical distance operations (using the spheroid)."
       
    40     dist_func = 'distance_spheroid'
       
    41     sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
       
    42     def __init__(self, prefix, operator):
       
    43         # An extra parameter in `end_subst` is needed for the spheroid string.
       
    44         super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
       
    45                                                       operator=operator)
       
    46 
       
    47 class PostGISSphereDistance(PostGISDistance):
       
    48     "For PostGIS spherical distance operations."
       
    49     dist_func = 'distance_sphere'
       
    50 
       
    51 class PostGISRelate(PostGISFunctionParam):
       
    52     "For PostGIS Relate(<geom>, <pattern>) calls."
       
    53     pattern_regex = re.compile(r'^[012TF\*]{9}$')
       
    54     def __init__(self, prefix, pattern):
       
    55         if not self.pattern_regex.match(pattern):
       
    56             raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
       
    57         super(PostGISRelate, self).__init__(prefix, 'Relate')
       
    58 
       
    59 
       
    60 class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
       
    61     compiler_module = 'django.contrib.gis.db.models.sql.compiler'
       
    62     name = 'postgis'
       
    63     postgis = True
       
    64     version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
       
    65     valid_aggregates = dict([(k, None) for k in
       
    66                              ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
       
    67 
       
    68     Adapter = PostGISAdapter
       
    69     Adaptor = Adapter # Backwards-compatibility alias.
       
    70 
       
    71     def __init__(self, connection):
       
    72         super(PostGISOperations, self).__init__(connection)
       
    73 
       
    74         # Trying to get the PostGIS version because the function
       
    75         # signatures will depend on the version used.  The cost
       
    76         # here is a database query to determine the version, which
       
    77         # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
       
    78         # comprising user-supplied values for the major, minor, and
       
    79         # subminor revision of PostGIS.
       
    80         try:
       
    81             if hasattr(settings, 'POSTGIS_VERSION'):
       
    82                 vtup = settings.POSTGIS_VERSION
       
    83                 if len(vtup) == 3:
       
    84                     # The user-supplied PostGIS version.
       
    85                     version = vtup
       
    86                 else:
       
    87                     # This was the old documented way, but it's stupid to
       
    88                     # include the string.
       
    89                     version = vtup[1:4]
       
    90             else:
       
    91                 vtup = self.postgis_version_tuple()
       
    92                 version = vtup[1:]
       
    93 
       
    94             # Getting the prefix -- even though we don't officially support
       
    95             # PostGIS 1.2 anymore, keeping it anyway in case a prefix change
       
    96             # for something else is necessary.
       
    97             if version >= (1, 2, 2):
       
    98                 prefix = 'ST_'
       
    99             else:
       
   100                 prefix = ''
       
   101 
       
   102             self.geom_func_prefix = prefix
       
   103             self.spatial_version = version
       
   104         except DatabaseError:
       
   105             raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". '
       
   106                                        'GeoDjango requires at least PostGIS version 1.3. '
       
   107                                        'Was the database created from a spatial database '
       
   108                                        'template?' % self.connection.settings_dict['NAME']
       
   109                                        )
       
   110         except Exception, e:
       
   111             # TODO: Raise helpful exceptions as they become known.
       
   112             raise
       
   113 
       
   114         # PostGIS-specific operators. The commented descriptions of these
       
   115         # operators come from Section 7.6 of the PostGIS 1.4 documentation.
       
   116         self.geometry_operators = {
       
   117             # The "&<" operator returns true if A's bounding box overlaps or
       
   118             # is to the left of B's bounding box.
       
   119             'overlaps_left' : PostGISOperator('&<'),
       
   120             # The "&>" operator returns true if A's bounding box overlaps or
       
   121             # is to the right of B's bounding box.
       
   122             'overlaps_right' : PostGISOperator('&>'),
       
   123             # The "<<" operator returns true if A's bounding box is strictly
       
   124             # to the left of B's bounding box.
       
   125             'left' : PostGISOperator('<<'),
       
   126             # The ">>" operator returns true if A's bounding box is strictly
       
   127             # to the right of B's bounding box.
       
   128             'right' : PostGISOperator('>>'),
       
   129             # The "&<|" operator returns true if A's bounding box overlaps or
       
   130             # is below B's bounding box.
       
   131             'overlaps_below' : PostGISOperator('&<|'),
       
   132             # The "|&>" operator returns true if A's bounding box overlaps or
       
   133             # is above B's bounding box.
       
   134             'overlaps_above' : PostGISOperator('|&>'),
       
   135             # The "<<|" operator returns true if A's bounding box is strictly
       
   136             # below B's bounding box.
       
   137             'strictly_below' : PostGISOperator('<<|'),
       
   138             # The "|>>" operator returns true if A's bounding box is strictly
       
   139             # above B's bounding box.
       
   140             'strictly_above' : PostGISOperator('|>>'),
       
   141             # The "~=" operator is the "same as" operator. It tests actual
       
   142             # geometric equality of two features. So if A and B are the same feature,
       
   143             # vertex-by-vertex, the operator returns true.
       
   144             'same_as' : PostGISOperator('~='),
       
   145             'exact' : PostGISOperator('~='),
       
   146             # The "@" operator returns true if A's bounding box is completely contained
       
   147             # by B's bounding box.
       
   148             'contained' : PostGISOperator('@'),
       
   149             # The "~" operator returns true if A's bounding box completely contains
       
   150             #  by B's bounding box.
       
   151             'bbcontains' : PostGISOperator('~'),
       
   152             # The "&&" operator returns true if A's bounding box overlaps
       
   153             # B's bounding box.
       
   154             'bboverlaps' : PostGISOperator('&&'),
       
   155             }
       
   156 
       
   157         self.geometry_functions = {
       
   158             'equals' : PostGISFunction(prefix, 'Equals'),
       
   159             'disjoint' : PostGISFunction(prefix, 'Disjoint'),
       
   160             'touches' : PostGISFunction(prefix, 'Touches'),
       
   161             'crosses' : PostGISFunction(prefix, 'Crosses'),
       
   162             'within' : PostGISFunction(prefix, 'Within'),
       
   163             'overlaps' : PostGISFunction(prefix, 'Overlaps'),
       
   164             'contains' : PostGISFunction(prefix, 'Contains'),
       
   165             'intersects' : PostGISFunction(prefix, 'Intersects'),
       
   166             'relate' : (PostGISRelate, basestring),
       
   167             }
       
   168 
       
   169         # Valid distance types and substitutions
       
   170         dtypes = (Decimal, Distance, float, int, long)
       
   171         def get_dist_ops(operator):
       
   172             "Returns operations for both regular and spherical distances."
       
   173             return {'cartesian' : PostGISDistance(prefix, operator),
       
   174                     'sphere' : PostGISSphereDistance(prefix, operator),
       
   175                     'spheroid' : PostGISSpheroidDistance(prefix, operator),
       
   176                     }
       
   177         self.distance_functions = {
       
   178             'distance_gt' : (get_dist_ops('>'), dtypes),
       
   179             'distance_gte' : (get_dist_ops('>='), dtypes),
       
   180             'distance_lt' : (get_dist_ops('<'), dtypes),
       
   181             'distance_lte' : (get_dist_ops('<='), dtypes),
       
   182             }
       
   183 
       
   184         # Versions 1.2.2+ have KML serialization support.
       
   185         if version < (1, 2, 2):
       
   186             ASKML = False
       
   187         else:
       
   188             ASKML = 'ST_AsKML'
       
   189             self.geometry_functions.update(
       
   190                 {'coveredby' : PostGISFunction(prefix, 'CoveredBy'),
       
   191                  'covers' : PostGISFunction(prefix, 'Covers'),
       
   192                  })
       
   193             self.distance_functions['dwithin'] = (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
       
   194 
       
   195         # Adding the distance functions to the geometries lookup.
       
   196         self.geometry_functions.update(self.distance_functions)
       
   197 
       
   198         # The union aggregate and topology operation use the same signature
       
   199         # in versions 1.3+.
       
   200         if version < (1, 3, 0):
       
   201             UNIONAGG = 'GeomUnion'
       
   202             UNION = 'Union'
       
   203             MAKELINE = False
       
   204         else:
       
   205             UNIONAGG = 'ST_Union'
       
   206             UNION = 'ST_Union'
       
   207             MAKELINE = 'ST_MakeLine'
       
   208 
       
   209         # Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
       
   210         if version < (1, 3, 4):
       
   211             GEOJSON = False
       
   212         else:
       
   213             GEOJSON = prefix + 'AsGeoJson'
       
   214 
       
   215         # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
       
   216         if version >= (1, 4, 0):
       
   217             GEOHASH = 'ST_GeoHash'
       
   218             BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
       
   219             self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
       
   220         else:
       
   221             GEOHASH, BOUNDINGCIRCLE = False, False
       
   222 
       
   223         # Geography type support added in 1.5.
       
   224         if version >= (1, 5, 0):
       
   225             self.geography = True
       
   226             # Only a subset of the operators and functions are available
       
   227             # for the geography type.
       
   228             self.geography_functions = self.distance_functions.copy()
       
   229             self.geography_functions.update({
       
   230                     'coveredby' : self.geometry_functions['coveredby'],
       
   231                     'covers' : self.geometry_functions['covers'],
       
   232                     'intersects' : self.geometry_functions['intersects'],
       
   233                     })
       
   234             self.geography_operators = {
       
   235                 'bboverlaps' : PostGISOperator('&&'),
       
   236                 'exact' : PostGISOperator('~='),
       
   237                 'same_as' : PostGISOperator('~='),
       
   238                 }
       
   239 
       
   240         # Creating a dictionary lookup of all GIS terms for PostGIS.
       
   241         gis_terms = ['isnull']
       
   242         gis_terms += self.geometry_operators.keys()
       
   243         gis_terms += self.geometry_functions.keys()
       
   244         self.gis_terms = dict([(term, None) for term in gis_terms])
       
   245 
       
   246         self.area = prefix + 'Area'
       
   247         self.bounding_circle = BOUNDINGCIRCLE
       
   248         self.centroid = prefix + 'Centroid'
       
   249         self.collect = prefix + 'Collect'
       
   250         self.difference = prefix + 'Difference'
       
   251         self.distance = prefix + 'Distance'
       
   252         self.distance_sphere = prefix + 'distance_sphere'
       
   253         self.distance_spheroid = prefix + 'distance_spheroid'
       
   254         self.envelope = prefix + 'Envelope'
       
   255         self.extent = prefix + 'Extent'
       
   256         self.extent3d = prefix + 'Extent3D'
       
   257         self.force_rhr = prefix + 'ForceRHR'
       
   258         self.geohash = GEOHASH
       
   259         self.geojson = GEOJSON
       
   260         self.gml = prefix + 'AsGML'
       
   261         self.intersection = prefix + 'Intersection'
       
   262         self.kml = ASKML
       
   263         self.length = prefix + 'Length'
       
   264         self.length3d = prefix + 'Length3D'
       
   265         self.length_spheroid = prefix + 'length_spheroid'
       
   266         self.makeline = MAKELINE
       
   267         self.mem_size = prefix + 'mem_size'
       
   268         self.num_geom = prefix + 'NumGeometries'
       
   269         self.num_points =prefix + 'npoints'
       
   270         self.perimeter = prefix + 'Perimeter'
       
   271         self.perimeter3d = prefix + 'Perimeter3D'
       
   272         self.point_on_surface = prefix + 'PointOnSurface'
       
   273         self.polygonize = prefix + 'Polygonize'
       
   274         self.reverse = prefix + 'Reverse'
       
   275         self.scale = prefix + 'Scale'
       
   276         self.snap_to_grid = prefix + 'SnapToGrid'
       
   277         self.svg = prefix + 'AsSVG'
       
   278         self.sym_difference = prefix + 'SymDifference'
       
   279         self.transform = prefix + 'Transform'
       
   280         self.translate = prefix + 'Translate'
       
   281         self.union = UNION
       
   282         self.unionagg = UNIONAGG
       
   283 
       
   284     def check_aggregate_support(self, aggregate):
       
   285         """
       
   286         Checks if the given aggregate name is supported (that is, if it's
       
   287         in `self.valid_aggregates`).
       
   288         """
       
   289         agg_name = aggregate.__class__.__name__
       
   290         return agg_name in self.valid_aggregates
       
   291 
       
   292     def convert_extent(self, box):
       
   293         """
       
   294         Returns a 4-tuple extent for the `Extent` aggregate by converting
       
   295         the bounding box text returned by PostGIS (`box` argument), for
       
   296         example: "BOX(-90.0 30.0, -85.0 40.0)".
       
   297         """
       
   298         ll, ur = box[4:-1].split(',')
       
   299         xmin, ymin = map(float, ll.split())
       
   300         xmax, ymax = map(float, ur.split())
       
   301         return (xmin, ymin, xmax, ymax)
       
   302 
       
   303     def convert_extent3d(self, box3d):
       
   304         """
       
   305         Returns a 6-tuple extent for the `Extent3D` aggregate by converting
       
   306         the 3d bounding-box text returnded by PostGIS (`box3d` argument), for
       
   307         example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
       
   308         """
       
   309         ll, ur = box3d[6:-1].split(',')
       
   310         xmin, ymin, zmin = map(float, ll.split())
       
   311         xmax, ymax, zmax = map(float, ur.split())
       
   312         return (xmin, ymin, zmin, xmax, ymax, zmax)
       
   313 
       
   314     def convert_geom(self, hex, geo_field):
       
   315         """
       
   316         Converts the geometry returned from PostGIS aggretates.
       
   317         """
       
   318         if hex:
       
   319             return Geometry(hex)
       
   320         else:
       
   321             return None
       
   322 
       
   323     def geo_db_type(self, f):
       
   324         """
       
   325         Return the database field type for the given geometry field.
       
   326         Typically this is `None` because geometry columns are added via
       
   327         the `AddGeometryColumn` stored procedure, unless the field
       
   328         has been specified to be of geography type instead.
       
   329         """
       
   330         if f.geography:
       
   331             if not self.geography:
       
   332                 raise NotImplementedError('PostGIS 1.5 required for geography column support.')
       
   333 
       
   334             if f.srid != 4326:
       
   335                 raise NotImplementedError('PostGIS 1.5 supports geography columns '
       
   336                                           'only with an SRID of 4326.')
       
   337 
       
   338             return 'geography(%s,%d)'% (f.geom_type, f.srid)
       
   339         else:
       
   340             return None
       
   341 
       
   342     def get_distance(self, f, dist_val, lookup_type):
       
   343         """
       
   344         Retrieve the distance parameters for the given geometry field,
       
   345         distance lookup value, and the distance lookup type.
       
   346 
       
   347         This is the most complex implementation of the spatial backends due to
       
   348         what is supported on geodetic geometry columns vs. what's available on
       
   349         projected geometry columns.  In addition, it has to take into account
       
   350         the newly introduced geography column type introudced in PostGIS 1.5.
       
   351         """
       
   352         # Getting the distance parameter and any options.
       
   353         if len(dist_val) == 1:
       
   354             value, option = dist_val[0], None
       
   355         else:
       
   356             value, option = dist_val
       
   357 
       
   358         # Shorthand boolean flags.
       
   359         geodetic = f.geodetic(self.connection)
       
   360         geography = f.geography and self.geography
       
   361 
       
   362         if isinstance(value, Distance):
       
   363             if geography:
       
   364                 dist_param = value.m
       
   365             elif geodetic:
       
   366                 if lookup_type == 'dwithin':
       
   367                     raise ValueError('Only numeric values of degree units are '
       
   368                                      'allowed on geographic DWithin queries.')
       
   369                 dist_param = value.m
       
   370             else:
       
   371                 dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
       
   372         else:
       
   373             # Assuming the distance is in the units of the field.
       
   374             dist_param = value
       
   375 
       
   376         if (not geography and geodetic and lookup_type != 'dwithin'
       
   377             and option == 'spheroid'):
       
   378             # using distance_spheroid requires the spheroid of the field as
       
   379             # a parameter.
       
   380             return [f._spheroid, dist_param]
       
   381         else:
       
   382             return [dist_param]
       
   383 
       
   384     def get_geom_placeholder(self, f, value):
       
   385         """
       
   386         Provides a proper substitution value for Geometries that are not in the
       
   387         SRID of the field.  Specifically, this routine will substitute in the
       
   388         ST_Transform() function call.
       
   389         """
       
   390         if value is None or value.srid == f.srid:
       
   391             placeholder = '%s'
       
   392         else:
       
   393             # Adding Transform() to the SQL placeholder.
       
   394             placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
       
   395 
       
   396         if hasattr(value, 'expression'):
       
   397             # If this is an F expression, then we don't really want
       
   398             # a placeholder and instead substitute in the column
       
   399             # of the expression.
       
   400             placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
       
   401 
       
   402         return placeholder
       
   403 
       
   404     def _get_postgis_func(self, func):
       
   405         """
       
   406         Helper routine for calling PostGIS functions and returning their result.
       
   407         """
       
   408         cursor = self.connection._cursor()
       
   409         try:
       
   410             try:
       
   411                 cursor.execute('SELECT %s()' % func)
       
   412                 row = cursor.fetchone()
       
   413             except:
       
   414                 # Responsibility of callers to perform error handling.
       
   415                 raise
       
   416         finally:
       
   417             # Close out the connection.  See #9437.
       
   418             self.connection.close()
       
   419         return row[0]
       
   420 
       
   421     def postgis_geos_version(self):
       
   422         "Returns the version of the GEOS library used with PostGIS."
       
   423         return self._get_postgis_func('postgis_geos_version')
       
   424 
       
   425     def postgis_lib_version(self):
       
   426         "Returns the version number of the PostGIS library used with PostgreSQL."
       
   427         return self._get_postgis_func('postgis_lib_version')
       
   428 
       
   429     def postgis_proj_version(self):
       
   430         "Returns the version of the PROJ.4 library used with PostGIS."
       
   431         return self._get_postgis_func('postgis_proj_version')
       
   432 
       
   433     def postgis_version(self):
       
   434         "Returns PostGIS version number and compile-time options."
       
   435         return self._get_postgis_func('postgis_version')
       
   436 
       
   437     def postgis_full_version(self):
       
   438         "Returns PostGIS version number and compile-time options."
       
   439         return self._get_postgis_func('postgis_full_version')
       
   440 
       
   441     def postgis_version_tuple(self):
       
   442         """
       
   443         Returns the PostGIS version as a tuple (version string, major,
       
   444         minor, subminor).
       
   445         """
       
   446         # Getting the PostGIS version
       
   447         version = self.postgis_lib_version()
       
   448         m = self.version_regex.match(version)
       
   449 
       
   450         if m:
       
   451             major = int(m.group('major'))
       
   452             minor1 = int(m.group('minor1'))
       
   453             minor2 = int(m.group('minor2'))
       
   454         else:
       
   455             raise Exception('Could not parse PostGIS version string: %s' % version)
       
   456 
       
   457         return (version, major, minor1, minor2)
       
   458 
       
   459     def proj_version_tuple(self):
       
   460         """
       
   461         Return the version of PROJ.4 used by PostGIS as a tuple of the
       
   462         major, minor, and subminor release numbers.
       
   463         """
       
   464         proj_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)')
       
   465         proj_ver_str = self.postgis_proj_version()
       
   466         m = proj_regex.search(proj_ver_str)
       
   467         if m:
       
   468             return tuple(map(int, [m.group(1), m.group(2), m.group(3)]))
       
   469         else:
       
   470             raise Exception('Could not determine PROJ.4 version from PostGIS.')
       
   471 
       
   472     def num_params(self, lookup_type, num_param):
       
   473         """
       
   474         Helper routine that returns a boolean indicating whether the number of
       
   475         parameters is correct for the lookup type.
       
   476         """
       
   477         def exactly_two(np): return np == 2
       
   478         def two_to_three(np): return np >= 2 and np <=3
       
   479         if (lookup_type in self.distance_functions and
       
   480             lookup_type != 'dwithin'):
       
   481             return two_to_three(num_param)
       
   482         else:
       
   483             return exactly_two(num_param)
       
   484 
       
   485     def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
       
   486         """
       
   487         Constructs spatial SQL from the given lookup value tuple a
       
   488         (alias, col, db_type), the lookup type string, lookup value, and
       
   489         the geometry field.
       
   490         """
       
   491         alias, col, db_type = lvalue
       
   492 
       
   493         # Getting the quoted geometry column.
       
   494         geo_col = '%s.%s' % (qn(alias), qn(col))
       
   495 
       
   496         if lookup_type in self.geometry_operators:
       
   497             if field.geography and not lookup_type in self.geography_operators:
       
   498                 raise ValueError('PostGIS geography does not support the '
       
   499                                  '"%s" lookup.' % lookup_type)
       
   500             # Handling a PostGIS operator.
       
   501             op = self.geometry_operators[lookup_type]
       
   502             return op.as_sql(geo_col, self.get_geom_placeholder(field, value))
       
   503         elif lookup_type in self.geometry_functions:
       
   504             if field.geography and not lookup_type in self.geography_functions:
       
   505                 raise ValueError('PostGIS geography type does not support the '
       
   506                                  '"%s" lookup.' % lookup_type)
       
   507 
       
   508             # See if a PostGIS geometry function matches the lookup type.
       
   509             tmp = self.geometry_functions[lookup_type]
       
   510 
       
   511             # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
       
   512             # distance lookups.
       
   513             if isinstance(tmp, tuple):
       
   514                 # First element of tuple is the PostGISOperation instance, and the
       
   515                 # second element is either the type or a tuple of acceptable types
       
   516                 # that may passed in as further parameters for the lookup type.
       
   517                 op, arg_type = tmp
       
   518 
       
   519                 # Ensuring that a tuple _value_ was passed in from the user
       
   520                 if not isinstance(value, (tuple, list)):
       
   521                     raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
       
   522 
       
   523                 # Geometry is first element of lookup tuple.
       
   524                 geom = value[0]
       
   525 
       
   526                 # Number of valid tuple parameters depends on the lookup type.
       
   527                 nparams = len(value)
       
   528                 if not self.num_params(lookup_type, nparams):
       
   529                     raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
       
   530 
       
   531                 # Ensuring the argument type matches what we expect.
       
   532                 if not isinstance(value[1], arg_type):
       
   533                     raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
       
   534 
       
   535                 # For lookup type `relate`, the op instance is not yet created (has
       
   536                 # to be instantiated here to check the pattern parameter).
       
   537                 if lookup_type == 'relate':
       
   538                     op = op(self.geom_func_prefix, value[1])
       
   539                 elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
       
   540                     if not field.geography and field.geodetic(self.connection):
       
   541                         # Geodetic distances are only availble from Points to
       
   542                         # PointFields on PostGIS 1.4 and below.
       
   543                         if not self.connection.ops.geography:
       
   544                             if field.geom_type != 'POINT':
       
   545                                 raise ValueError('PostGIS spherical operations are only valid on PointFields.')
       
   546 
       
   547                             if str(geom.geom_type) != 'Point':
       
   548                                 raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
       
   549 
       
   550                         # Setting up the geodetic operation appropriately.
       
   551                         if nparams == 3 and value[2] == 'spheroid':
       
   552                             op = op['spheroid']
       
   553                         else:
       
   554                             op = op['sphere']
       
   555                     else:
       
   556                         op = op['cartesian']
       
   557             else:
       
   558                 op = tmp
       
   559                 geom = value
       
   560 
       
   561             # Calling the `as_sql` function on the operation instance.
       
   562             return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
       
   563 
       
   564         elif lookup_type == 'isnull':
       
   565             # Handling 'isnull' lookup type
       
   566             return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
       
   567 
       
   568         raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
       
   569 
       
   570     def spatial_aggregate_sql(self, agg):
       
   571         """
       
   572         Returns the spatial aggregate SQL template and function for the
       
   573         given Aggregate instance.
       
   574         """
       
   575         agg_name = agg.__class__.__name__
       
   576         if not self.check_aggregate_support(agg):
       
   577             raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
       
   578         agg_name = agg_name.lower()
       
   579         if agg_name == 'union': agg_name += 'agg'
       
   580         sql_template = '%(function)s(%(field)s)'
       
   581         sql_function = getattr(self, agg_name)
       
   582         return sql_template, sql_function
       
   583 
       
   584     # Routines for getting the OGC-compliant models.
       
   585     def geometry_columns(self):
       
   586         from django.contrib.gis.db.backends.postgis.models import GeometryColumns
       
   587         return GeometryColumns
       
   588 
       
   589     def spatial_ref_sys(self):
       
   590         from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
       
   591         return SpatialRefSys