app/django/contrib/gis/db/models/query.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     1 from django.core.exceptions import ImproperlyConfigured
       
     2 from django.db import connection
       
     3 from django.db.models.query import sql, QuerySet, Q
       
     4 
       
     5 from django.contrib.gis.db.backend import SpatialBackend
       
     6 from django.contrib.gis.db.models.fields import GeometryField, PointField
       
     7 from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
       
     8 from django.contrib.gis.measure import Area, Distance
       
     9 from django.contrib.gis.models import get_srid_info
       
    10 qn = connection.ops.quote_name
       
    11 
       
    12 # For backwards-compatibility; Q object should work just fine
       
    13 # after queryset-refactor.
       
    14 class GeoQ(Q): pass
       
    15 
       
    16 class GeomSQL(object):
       
    17     "Simple wrapper object for geometric SQL."
       
    18     def __init__(self, geo_sql):
       
    19         self.sql = geo_sql
       
    20     
       
    21     def as_sql(self, *args, **kwargs):
       
    22         return self.sql
       
    23 
       
    24 class GeoQuerySet(QuerySet):
       
    25     "The Geographic QuerySet."
       
    26 
       
    27     def __init__(self, model=None, query=None):
       
    28         super(GeoQuerySet, self).__init__(model=model, query=query)
       
    29         self.query = query or GeoQuery(self.model, connection)
       
    30 
       
    31     def area(self, tolerance=0.05, **kwargs):
       
    32         """
       
    33         Returns the area of the geographic field in an `area` attribute on 
       
    34         each element of this GeoQuerySet.
       
    35         """
       
    36         # Peforming setup here rather than in `_spatial_attribute` so that
       
    37         # we can get the units for `AreaField`.
       
    38         procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
       
    39         s = {'procedure_args' : procedure_args,
       
    40              'geo_field' : geo_field,
       
    41              'setup' : False,
       
    42              }
       
    43         if SpatialBackend.oracle:
       
    44             s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
       
    45             s['procedure_args']['tolerance'] = tolerance
       
    46             s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
       
    47         elif SpatialBackend.postgis:
       
    48             if not geo_field.geodetic:
       
    49                 # Getting the area units of the geographic field.
       
    50                 s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
       
    51             else:
       
    52                 # TODO: Do we want to support raw number areas for geodetic fields?
       
    53                 raise Exception('Area on geodetic coordinate systems not supported.')
       
    54         return self._spatial_attribute('area', s, **kwargs)
       
    55 
       
    56     def centroid(self, **kwargs):
       
    57         """
       
    58         Returns the centroid of the geographic field in a `centroid`
       
    59         attribute on each element of this GeoQuerySet.
       
    60         """
       
    61         return self._geom_attribute('centroid', **kwargs)
       
    62 
       
    63     def difference(self, geom, **kwargs):
       
    64         """
       
    65         Returns the spatial difference of the geographic field in a `difference`
       
    66         attribute on each element of this GeoQuerySet.
       
    67         """
       
    68         return self._geomset_attribute('difference', geom, **kwargs)
       
    69 
       
    70     def distance(self, geom, **kwargs):
       
    71         """
       
    72         Returns the distance from the given geographic field name to the
       
    73         given geometry in a `distance` attribute on each element of the
       
    74         GeoQuerySet.
       
    75 
       
    76         Keyword Arguments:
       
    77          `spheroid`  => If the geometry field is geodetic and PostGIS is
       
    78                         the spatial database, then the more accurate 
       
    79                         spheroid calculation will be used instead of the
       
    80                         quicker sphere calculation.
       
    81                         
       
    82          `tolerance` => Used only for Oracle. The tolerance is 
       
    83                         in meters -- a default of 5 centimeters (0.05) 
       
    84                         is used.
       
    85         """
       
    86         return self._distance_attribute('distance', geom, **kwargs)
       
    87 
       
    88     def envelope(self, **kwargs):
       
    89         """
       
    90         Returns a Geometry representing the bounding box of the 
       
    91         Geometry field in an `envelope` attribute on each element of
       
    92         the GeoQuerySet. 
       
    93         """
       
    94         return self._geom_attribute('envelope', **kwargs)
       
    95 
       
    96     def extent(self, **kwargs):
       
    97         """
       
    98         Returns the extent (aggregate) of the features in the GeoQuerySet.  The
       
    99         extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
       
   100         """
       
   101         convert_extent = None
       
   102         if SpatialBackend.postgis:
       
   103             def convert_extent(box, geo_field):
       
   104                 # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
       
   105                 # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; 
       
   106                 # parsing out and returning as a 4-tuple.
       
   107                 ll, ur = box[4:-1].split(',')
       
   108                 xmin, ymin = map(float, ll.split())
       
   109                 xmax, ymax = map(float, ur.split())
       
   110                 return (xmin, ymin, xmax, ymax)
       
   111         elif SpatialBackend.oracle:
       
   112             def convert_extent(wkt, geo_field):
       
   113                 raise NotImplementedError
       
   114         return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
       
   115 
       
   116     def gml(self, precision=8, version=2, **kwargs):
       
   117         """
       
   118         Returns GML representation of the given field in a `gml` attribute
       
   119         on each element of the GeoQuerySet.
       
   120         """
       
   121         s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
       
   122         if SpatialBackend.postgis:
       
   123             # PostGIS AsGML() aggregate function parameter order depends on the 
       
   124             # version -- uggh.
       
   125             major, minor1, minor2 = SpatialBackend.version
       
   126             if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
       
   127                 procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
       
   128             else:
       
   129                 procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
       
   130             s['procedure_args'] = {'precision' : precision, 'version' : version}
       
   131 
       
   132         return self._spatial_attribute('gml', s, **kwargs)
       
   133 
       
   134     def intersection(self, geom, **kwargs):
       
   135         """
       
   136         Returns the spatial intersection of the Geometry field in
       
   137         an `intersection` attribute on each element of this
       
   138         GeoQuerySet.
       
   139         """
       
   140         return self._geomset_attribute('intersection', geom, **kwargs)
       
   141 
       
   142     def kml(self, **kwargs):
       
   143         """
       
   144         Returns KML representation of the geometry field in a `kml`
       
   145         attribute on each element of this GeoQuerySet.
       
   146         """
       
   147         s = {'desc' : 'KML',
       
   148              'procedure_fmt' : '%(geo_col)s,%(precision)s',
       
   149              'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
       
   150              }
       
   151         return self._spatial_attribute('kml', s, **kwargs)
       
   152 
       
   153     def length(self, **kwargs):
       
   154         """
       
   155         Returns the length of the geometry field as a `Distance` object
       
   156         stored in a `length` attribute on each element of this GeoQuerySet.
       
   157         """
       
   158         return self._distance_attribute('length', None, **kwargs)
       
   159 
       
   160     def make_line(self, **kwargs):
       
   161         """
       
   162         Creates a linestring from all of the PointField geometries in the
       
   163         this GeoQuerySet and returns it.  This is a spatial aggregate
       
   164         method, and thus returns a geometry rather than a GeoQuerySet.
       
   165         """
       
   166         kwargs['geo_field_type'] = PointField
       
   167         kwargs['agg_field'] = GeometryField
       
   168         return self._spatial_aggregate('make_line', **kwargs)
       
   169 
       
   170     def mem_size(self, **kwargs):
       
   171         """
       
   172         Returns the memory size (number of bytes) that the geometry field takes
       
   173         in a `mem_size` attribute  on each element of this GeoQuerySet.
       
   174         """
       
   175         return self._spatial_attribute('mem_size', {}, **kwargs)
       
   176 
       
   177     def num_geom(self, **kwargs):
       
   178         """
       
   179         Returns the number of geometries if the field is a
       
   180         GeometryCollection or Multi* Field in a `num_geom`
       
   181         attribute on each element of this GeoQuerySet; otherwise
       
   182         the sets with None.
       
   183         """
       
   184         return self._spatial_attribute('num_geom', {}, **kwargs)
       
   185 
       
   186     def num_points(self, **kwargs):
       
   187         """
       
   188         Returns the number of points in the first linestring in the 
       
   189         Geometry field in a `num_points` attribute on each element of
       
   190         this GeoQuerySet; otherwise sets with None.
       
   191         """
       
   192         return self._spatial_attribute('num_points', {}, **kwargs)
       
   193 
       
   194     def perimeter(self, **kwargs):
       
   195         """
       
   196         Returns the perimeter of the geometry field as a `Distance` object
       
   197         stored in a `perimeter` attribute on each element of this GeoQuerySet.
       
   198         """
       
   199         return self._distance_attribute('perimeter', None, **kwargs)
       
   200 
       
   201     def point_on_surface(self, **kwargs):
       
   202         """
       
   203         Returns a Point geometry guaranteed to lie on the surface of the
       
   204         Geometry field in a `point_on_surface` attribute on each element
       
   205         of this GeoQuerySet; otherwise sets with None.
       
   206         """
       
   207         return self._geom_attribute('point_on_surface', **kwargs)
       
   208 
       
   209     def scale(self, x, y, z=0.0, **kwargs):
       
   210         """
       
   211         Scales the geometry to a new size by multiplying the ordinates
       
   212         with the given x,y,z scale factors.
       
   213         """
       
   214         s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
       
   215              'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
       
   216              'select_field' : GeomField(),
       
   217              }
       
   218         return self._spatial_attribute('scale', s, **kwargs)
       
   219 
       
   220     def svg(self, **kwargs):
       
   221         """
       
   222         Returns SVG representation of the geographic field in a `svg`
       
   223         attribute on each element of this GeoQuerySet.
       
   224         """
       
   225         s = {'desc' : 'SVG',
       
   226              'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
       
   227              'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
       
   228                                  'precision' : kwargs.pop('precision', 8)},
       
   229              }
       
   230         return self._spatial_attribute('svg', s, **kwargs)
       
   231 
       
   232     def sym_difference(self, geom, **kwargs):
       
   233         """
       
   234         Returns the symmetric difference of the geographic field in a 
       
   235         `sym_difference` attribute on each element of this GeoQuerySet.
       
   236         """
       
   237         return self._geomset_attribute('sym_difference', geom, **kwargs)
       
   238 
       
   239     def translate(self, x, y, z=0.0, **kwargs):
       
   240         """
       
   241         Translates the geometry to a new location using the given numeric
       
   242         parameters as offsets.
       
   243         """
       
   244         s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
       
   245              'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
       
   246              'select_field' : GeomField(),
       
   247              }
       
   248         return self._spatial_attribute('translate', s, **kwargs)
       
   249 
       
   250     def transform(self, srid=4326, **kwargs):
       
   251         """
       
   252         Transforms the given geometry field to the given SRID.  If no SRID is
       
   253         provided, the transformation will default to using 4326 (WGS84).
       
   254         """
       
   255         if not isinstance(srid, (int, long)):
       
   256             raise TypeError('An integer SRID must be provided.')
       
   257         field_name = kwargs.get('field_name', None)
       
   258         tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
       
   259 
       
   260         # Getting the selection SQL for the given geographic field.
       
   261         field_col = self._geocol_select(geo_field, field_name)
       
   262 
       
   263         # Why cascading substitutions? Because spatial backends like
       
   264         # Oracle and MySQL already require a function call to convert to text, thus
       
   265         # when there's also a transformation we need to cascade the substitutions.
       
   266         # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
       
   267         geo_col = self.query.custom_select.get(geo_field, field_col)
       
   268         
       
   269         # Setting the key for the field's column with the custom SELECT SQL to
       
   270         # override the geometry column returned from the database.
       
   271         custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
       
   272         # TODO: Should we have this as an alias?
       
   273         # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
       
   274         self.query.transformed_srid = srid # So other GeoQuerySet methods
       
   275         self.query.custom_select[geo_field] = custom_sel
       
   276         return self._clone()
       
   277 
       
   278     def union(self, geom, **kwargs):
       
   279         """
       
   280         Returns the union of the geographic field with the given
       
   281         Geometry in a `union` attribute on each element of this GeoQuerySet.
       
   282         """
       
   283         return self._geomset_attribute('union', geom, **kwargs)
       
   284 
       
   285     def unionagg(self, **kwargs):
       
   286         """
       
   287         Performs an aggregate union on the given geometry field.  Returns
       
   288         None if the GeoQuerySet is empty.  The `tolerance` keyword is for
       
   289         Oracle backends only.
       
   290         """
       
   291         kwargs['agg_field'] = GeometryField
       
   292         return self._spatial_aggregate('unionagg', **kwargs)
       
   293 
       
   294     ### Private API -- Abstracted DRY routines. ###
       
   295     def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
       
   296         """
       
   297         Performs set up for executing the spatial function.
       
   298         """
       
   299         # Does the spatial backend support this?
       
   300         func = getattr(SpatialBackend, att, False)
       
   301         if desc is None: desc = att
       
   302         if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
       
   303 
       
   304         # Initializing the procedure arguments. 
       
   305         procedure_args = {'function' : func}
       
   306         
       
   307         # Is there a geographic field in the model to perform this 
       
   308         # operation on?
       
   309         geo_field = self.query._geo_field(field_name)
       
   310         if not geo_field:
       
   311             raise TypeError('%s output only available on GeometryFields.' % func)
       
   312 
       
   313         # If the `geo_field_type` keyword was used, then enforce that 
       
   314         # type limitation.
       
   315         if not geo_field_type is None and not isinstance(geo_field, geo_field_type): 
       
   316             raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) 
       
   317 
       
   318         # Setting the procedure args.
       
   319         procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
       
   320 
       
   321         return procedure_args, geo_field
       
   322 
       
   323     def _spatial_aggregate(self, att, field_name=None, 
       
   324                            agg_field=None, convert_func=None, 
       
   325                            geo_field_type=None, tolerance=0.0005):
       
   326         """
       
   327         DRY routine for calling aggregate spatial stored procedures and
       
   328         returning their result to the caller of the function.
       
   329         """
       
   330         # Constructing the setup keyword arguments.
       
   331         setup_kwargs = {'aggregate' : True,
       
   332                         'field_name' : field_name,
       
   333                         'geo_field_type' : geo_field_type,
       
   334                         }
       
   335         procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
       
   336         
       
   337         if SpatialBackend.oracle:
       
   338             procedure_args['tolerance'] = tolerance
       
   339             # Adding in selection SQL for Oracle geometry columns.
       
   340             if agg_field is GeometryField: 
       
   341                 agg_sql = '%s' % SpatialBackend.select
       
   342             else: 
       
   343                 agg_sql = '%s'
       
   344             agg_sql =  agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
       
   345         else:
       
   346             agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
       
   347 
       
   348         # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
       
   349         # specifying the type of the aggregate field.
       
   350         self.query.select = [GeomSQL(agg_sql)]
       
   351         self.query.select_fields = [agg_field]
       
   352 
       
   353         try:
       
   354             # `asql` => not overriding `sql` module.
       
   355             asql, params = self.query.as_sql()
       
   356         except sql.datastructures.EmptyResultSet:
       
   357             return None   
       
   358 
       
   359         # Getting a cursor, executing the query, and extracting the returned
       
   360         # value from the aggregate function.
       
   361         cursor = connection.cursor()
       
   362         cursor.execute(asql, params)
       
   363         result = cursor.fetchone()[0]
       
   364         
       
   365         # If the `agg_field` is specified as a GeometryField, then autmatically
       
   366         # set up the conversion function.
       
   367         if agg_field is GeometryField and not callable(convert_func):
       
   368             if SpatialBackend.postgis:
       
   369                 def convert_geom(hex, geo_field):
       
   370                     if hex: return SpatialBackend.Geometry(hex)
       
   371                     else: return None
       
   372             elif SpatialBackend.oracle:
       
   373                 def convert_geom(clob, geo_field):
       
   374                     if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
       
   375                     else: return None
       
   376             convert_func = convert_geom
       
   377 
       
   378         # Returning the callback function evaluated on the result culled
       
   379         # from the executed cursor.
       
   380         if callable(convert_func):
       
   381             return convert_func(result, geo_field)
       
   382         else:
       
   383             return result
       
   384 
       
   385     def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
       
   386         """
       
   387         DRY routine for calling a spatial stored procedure on a geometry column
       
   388         and attaching its output as an attribute of the model.
       
   389 
       
   390         Arguments:
       
   391          att:
       
   392           The name of the spatial attribute that holds the spatial
       
   393           SQL function to call.
       
   394 
       
   395          settings:
       
   396           Dictonary of internal settings to customize for the spatial procedure. 
       
   397 
       
   398         Public Keyword Arguments:
       
   399 
       
   400          field_name:
       
   401           The name of the geographic field to call the spatial
       
   402           function on.  May also be a lookup to a geometry field
       
   403           as part of a foreign key relation.
       
   404 
       
   405          model_att:
       
   406           The name of the model attribute to attach the output of
       
   407           the spatial function to.
       
   408         """
       
   409         # Default settings.
       
   410         settings.setdefault('desc', None)
       
   411         settings.setdefault('geom_args', ())
       
   412         settings.setdefault('geom_field', None)
       
   413         settings.setdefault('procedure_args', {})
       
   414         settings.setdefault('procedure_fmt', '%(geo_col)s')
       
   415         settings.setdefault('select_params', [])
       
   416 
       
   417         # Performing setup for the spatial column, unless told not to.
       
   418         if settings.get('setup', True):
       
   419             default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
       
   420             for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
       
   421         else:
       
   422             geo_field = settings['geo_field']
       
   423             
       
   424         # The attribute to attach to the model.
       
   425         if not isinstance(model_att, basestring): model_att = att
       
   426 
       
   427         # Special handling for any argument that is a geometry.
       
   428         for name in settings['geom_args']:
       
   429             # Using the field's get_db_prep_lookup() to get any needed
       
   430             # transformation SQL -- we pass in a 'dummy' `contains` lookup.
       
   431             where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
       
   432             # Replacing the procedure format with that of any needed 
       
   433             # transformation SQL.
       
   434             old_fmt = '%%(%s)s' % name
       
   435             new_fmt = where[0] % '%%s'
       
   436             settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
       
   437             settings['select_params'].extend(params)
       
   438 
       
   439         # Getting the format for the stored procedure.
       
   440         fmt = '%%(function)s(%s)' % settings['procedure_fmt']
       
   441         
       
   442         # If the result of this function needs to be converted.
       
   443         if settings.get('select_field', False):
       
   444             sel_fld = settings['select_field']
       
   445             if isinstance(sel_fld, GeomField) and SpatialBackend.select:
       
   446                 self.query.custom_select[model_att] = SpatialBackend.select
       
   447             self.query.extra_select_fields[model_att] = sel_fld
       
   448 
       
   449         # Finally, setting the extra selection attribute with 
       
   450         # the format string expanded with the stored procedure
       
   451         # arguments.
       
   452         return self.extra(select={model_att : fmt % settings['procedure_args']}, 
       
   453                           select_params=settings['select_params'])
       
   454 
       
   455     def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
       
   456         """
       
   457         DRY routine for GeoQuerySet distance attribute routines.
       
   458         """
       
   459         # Setting up the distance procedure arguments.
       
   460         procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
       
   461 
       
   462         # If geodetic defaulting distance attribute to meters (Oracle and
       
   463         # PostGIS spherical distances return meters).  Otherwise, use the
       
   464         # units of the geometry field.
       
   465         if geo_field.geodetic:
       
   466             dist_att = 'm'
       
   467         else:
       
   468             dist_att = Distance.unit_attname(geo_field._unit_name)
       
   469 
       
   470         # Shortcut booleans for what distance function we're using.
       
   471         distance = func == 'distance'
       
   472         length = func == 'length'
       
   473         perimeter = func == 'perimeter'
       
   474         if not (distance or length or perimeter): 
       
   475             raise ValueError('Unknown distance function: %s' % func)
       
   476 
       
   477         # The field's get_db_prep_lookup() is used to get any 
       
   478         # extra distance parameters.  Here we set up the
       
   479         # parameters that will be passed in to field's function.
       
   480         lookup_params = [geom or 'POINT (0 0)', 0]
       
   481 
       
   482         # If the spheroid calculation is desired, either by the `spheroid`
       
   483         # keyword or wehn calculating the length of geodetic field, make
       
   484         # sure the 'spheroid' distance setting string is passed in so we
       
   485         # get the correct spatial stored procedure.            
       
   486         if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): 
       
   487             lookup_params.append('spheroid') 
       
   488         where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
       
   489 
       
   490         # The `geom_args` flag is set to true if a geometry parameter was 
       
   491         # passed in.
       
   492         geom_args = bool(geom)
       
   493 
       
   494         if SpatialBackend.oracle:
       
   495             if distance:
       
   496                 procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
       
   497             elif length or perimeter:
       
   498                 procedure_fmt = '%(geo_col)s,%(tolerance)s'
       
   499             procedure_args['tolerance'] = tolerance
       
   500         else:
       
   501             # Getting whether this field is in units of degrees since the field may have
       
   502             # been transformed via the `transform` GeoQuerySet method.
       
   503             if self.query.transformed_srid:
       
   504                 u, unit_name, s = get_srid_info(self.query.transformed_srid)
       
   505                 geodetic = unit_name in geo_field.geodetic_units
       
   506             else:
       
   507                 geodetic = geo_field.geodetic
       
   508             
       
   509             if distance:
       
   510                 if self.query.transformed_srid:
       
   511                     # Setting the `geom_args` flag to false because we want to handle
       
   512                     # transformation SQL here, rather than the way done by default
       
   513                     # (which will transform to the original SRID of the field rather
       
   514                     #  than to what was transformed to).
       
   515                     geom_args = False
       
   516                     procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
       
   517                     if geom.srid is None or geom.srid == self.query.transformed_srid:
       
   518                         # If the geom parameter srid is None, it is assumed the coordinates 
       
   519                         # are in the transformed units.  A placeholder is used for the
       
   520                         # geometry parameter.
       
   521                         procedure_fmt += ', %%s'
       
   522                     else:
       
   523                         # We need to transform the geom to the srid specified in `transform()`,
       
   524                         # so wrapping the geometry placeholder in transformation SQL.
       
   525                         procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
       
   526                 else:
       
   527                     # `transform()` was not used on this GeoQuerySet.
       
   528                     procedure_fmt  = '%(geo_col)s,%(geom)s'
       
   529 
       
   530                 if geodetic:
       
   531                     # Spherical distance calculation is needed (because the geographic
       
   532                     # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() 
       
   533                     # procedures may only do queries from point columns to point geometries
       
   534                     # some error checking is required.
       
   535                     if not isinstance(geo_field, PointField): 
       
   536                         raise TypeError('Spherical distance calculation only supported on PointFields.')
       
   537                     if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
       
   538                         raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
       
   539                     # The `function` procedure argument needs to be set differently for
       
   540                     # geodetic distance calculations.
       
   541                     if spheroid:
       
   542                         # Call to distance_spheroid() requires spheroid param as well.
       
   543                         procedure_fmt += ',%(spheroid)s'
       
   544                         procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
       
   545                     else:
       
   546                         procedure_args.update({'function' : SpatialBackend.distance_sphere})
       
   547             elif length or perimeter:
       
   548                 procedure_fmt = '%(geo_col)s'
       
   549                 if geodetic and length:
       
   550                     # There's no `length_sphere`
       
   551                     procedure_fmt += ',%(spheroid)s'
       
   552                     procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
       
   553 
       
   554         # Setting up the settings for `_spatial_attribute`.
       
   555         s = {'select_field' : DistanceField(dist_att),
       
   556              'setup' : False, 
       
   557              'geo_field' : geo_field,
       
   558              'procedure_args' : procedure_args,
       
   559              'procedure_fmt' : procedure_fmt,
       
   560              }
       
   561         if geom_args: 
       
   562             s['geom_args'] = ('geom',)
       
   563             s['procedure_args']['geom'] = geom
       
   564         elif geom:
       
   565             # The geometry is passed in as a parameter because we handled
       
   566             # transformation conditions in this routine.
       
   567             s['select_params'] = [SpatialBackend.Adaptor(geom)]
       
   568         return self._spatial_attribute(func, s, **kwargs)
       
   569 
       
   570     def _geom_attribute(self, func, tolerance=0.05, **kwargs):
       
   571         """
       
   572         DRY routine for setting up a GeoQuerySet method that attaches a
       
   573         Geometry attribute (e.g., `centroid`, `point_on_surface`).
       
   574         """
       
   575         s = {'select_field' : GeomField(),}
       
   576         if SpatialBackend.oracle:
       
   577             s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
       
   578             s['procedure_args'] = {'tolerance' : tolerance}
       
   579         return self._spatial_attribute(func, s, **kwargs)
       
   580                      
       
   581     def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
       
   582         """
       
   583         DRY routine for setting up a GeoQuerySet method that attaches a
       
   584         Geometry attribute and takes a Geoemtry parameter.  This is used
       
   585         for geometry set-like operations (e.g., intersection, difference, 
       
   586         union, sym_difference).
       
   587         """
       
   588         s = {'geom_args' : ('geom',),
       
   589              'select_field' : GeomField(),
       
   590              'procedure_fmt' : '%(geo_col)s,%(geom)s',
       
   591              'procedure_args' : {'geom' : geom},
       
   592             }
       
   593         if SpatialBackend.oracle:
       
   594             s['procedure_fmt'] += ',%(tolerance)s'
       
   595             s['procedure_args']['tolerance'] = tolerance
       
   596         return self._spatial_attribute(func, s, **kwargs)
       
   597 
       
   598     def _geocol_select(self, geo_field, field_name, aggregate=False):
       
   599         """
       
   600         Helper routine for constructing the SQL to select the geographic
       
   601         column.  Takes into account if the geographic field is in a
       
   602         ForeignKey relation to the current model.
       
   603         """
       
   604         # If this is an aggregate spatial query, the flag needs to be
       
   605         # set on the `GeoQuery` object of this queryset.
       
   606         if aggregate: self.query.aggregate = True
       
   607 
       
   608         # Is this operation going to be on a related geographic field?
       
   609         if not geo_field in self.model._meta.fields:
       
   610             # If so, it'll have to be added to the select related information
       
   611             # (e.g., if 'location__point' was given as the field name).
       
   612             self.query.add_select_related([field_name])
       
   613             self.query.pre_sql_setup()
       
   614             rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
       
   615             return self.query._field_column(geo_field, rel_table)
       
   616         else:
       
   617             return self.query._field_column(geo_field)