PageRenderTime 51ms CodeModel.GetById 5ms app.highlight 37ms RepoModel.GetById 1ms app.codeStats 0ms

/django/contrib/gis/db/models/query.py

https://code.google.com/p/mango-py/
Python | 777 lines | 385 code | 82 blank | 310 comment | 109 complexity | 21d8aaa7e45bff5bb6eab4ad4cf0a4f1 MD5 | raw file
  1from django.db import connections
  2from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet
  3
  4from django.contrib.gis.db.models import aggregates
  5from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField, LineStringField
  6from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
  7from django.contrib.gis.geometry.backend import Geometry
  8from django.contrib.gis.measure import Area, Distance
  9
 10class GeoQuerySet(QuerySet):
 11    "The Geographic QuerySet."
 12
 13    ### Methods overloaded from QuerySet ###
 14    def __init__(self, model=None, query=None, using=None):
 15        super(GeoQuerySet, self).__init__(model=model, query=query, using=using)
 16        self.query = query or GeoQuery(self.model)
 17
 18    def values(self, *fields):
 19        return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields)
 20
 21    def values_list(self, *fields, **kwargs):
 22        flat = kwargs.pop('flat', False)
 23        if kwargs:
 24            raise TypeError('Unexpected keyword arguments to values_list: %s'
 25                    % (kwargs.keys(),))
 26        if flat and len(fields) > 1:
 27            raise TypeError("'flat' is not valid when values_list is called with more than one field.")
 28        return self._clone(klass=GeoValuesListQuerySet, setup=True, flat=flat,
 29                           _fields=fields)
 30
 31    ### GeoQuerySet Methods ###
 32    def area(self, tolerance=0.05, **kwargs):
 33        """
 34        Returns the area of the geographic field in an `area` attribute on
 35        each element of this GeoQuerySet.
 36        """
 37        # Peforming setup here rather than in `_spatial_attribute` so that
 38        # we can get the units for `AreaField`.
 39        procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
 40        s = {'procedure_args' : procedure_args,
 41             'geo_field' : geo_field,
 42             'setup' : False,
 43             }
 44        connection = connections[self.db]
 45        backend = connection.ops
 46        if backend.oracle:
 47            s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
 48            s['procedure_args']['tolerance'] = tolerance
 49            s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
 50        elif backend.postgis or backend.spatialite:
 51            if backend.geography:
 52                # Geography fields support area calculation, returns square meters.
 53                s['select_field'] = AreaField('sq_m')
 54            elif not geo_field.geodetic(connection):
 55                # Getting the area units of the geographic field.
 56                s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection)))
 57            else:
 58                # TODO: Do we want to support raw number areas for geodetic fields?
 59                raise Exception('Area on geodetic coordinate systems not supported.')
 60        return self._spatial_attribute('area', s, **kwargs)
 61
 62    def centroid(self, **kwargs):
 63        """
 64        Returns the centroid of the geographic field in a `centroid`
 65        attribute on each element of this GeoQuerySet.
 66        """
 67        return self._geom_attribute('centroid', **kwargs)
 68
 69    def collect(self, **kwargs):
 70        """
 71        Performs an aggregate collect operation on the given geometry field.
 72        This is analagous to a union operation, but much faster because
 73        boundaries are not dissolved.
 74        """
 75        return self._spatial_aggregate(aggregates.Collect, **kwargs)
 76
 77    def difference(self, geom, **kwargs):
 78        """
 79        Returns the spatial difference of the geographic field in a `difference`
 80        attribute on each element of this GeoQuerySet.
 81        """
 82        return self._geomset_attribute('difference', geom, **kwargs)
 83
 84    def distance(self, geom, **kwargs):
 85        """
 86        Returns the distance from the given geographic field name to the
 87        given geometry in a `distance` attribute on each element of the
 88        GeoQuerySet.
 89
 90        Keyword Arguments:
 91         `spheroid`  => If the geometry field is geodetic and PostGIS is
 92                        the spatial database, then the more accurate
 93                        spheroid calculation will be used instead of the
 94                        quicker sphere calculation.
 95
 96         `tolerance` => Used only for Oracle. The tolerance is
 97                        in meters -- a default of 5 centimeters (0.05)
 98                        is used.
 99        """
100        return self._distance_attribute('distance', geom, **kwargs)
101
102    def envelope(self, **kwargs):
103        """
104        Returns a Geometry representing the bounding box of the
105        Geometry field in an `envelope` attribute on each element of
106        the GeoQuerySet.
107        """
108        return self._geom_attribute('envelope', **kwargs)
109
110    def extent(self, **kwargs):
111        """
112        Returns the extent (aggregate) of the features in the GeoQuerySet.  The
113        extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
114        """
115        return self._spatial_aggregate(aggregates.Extent, **kwargs)
116
117    def extent3d(self, **kwargs):
118        """
119        Returns the aggregate extent, in 3D, of the features in the
120        GeoQuerySet. It is returned as a 6-tuple, comprising:
121          (xmin, ymin, zmin, xmax, ymax, zmax).
122        """
123        return self._spatial_aggregate(aggregates.Extent3D, **kwargs)
124
125    def force_rhr(self, **kwargs):
126        """
127        Returns a modified version of the Polygon/MultiPolygon in which
128        all of the vertices follow the Right-Hand-Rule.  By default,
129        this is attached as the `force_rhr` attribute on each element
130        of the GeoQuerySet.
131        """
132        return self._geom_attribute('force_rhr', **kwargs)
133
134    def geojson(self, precision=8, crs=False, bbox=False, **kwargs):
135        """
136        Returns a GeoJSON representation of the geomtry field in a `geojson`
137        attribute on each element of the GeoQuerySet.
138
139        The `crs` and `bbox` keywords may be set to True if the users wants
140        the coordinate reference system and the bounding box to be included
141        in the GeoJSON representation of the geometry.
142        """
143        backend = connections[self.db].ops
144        if not backend.geojson:
145            raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.')
146
147        if not isinstance(precision, (int, long)):
148            raise TypeError('Precision keyword must be set with an integer.')
149
150        # Setting the options flag -- which depends on which version of
151        # PostGIS we're using.
152        if backend.spatial_version >= (1, 4, 0):
153            options = 0
154            if crs and bbox: options = 3
155            elif bbox: options = 1
156            elif crs: options = 2
157        else:
158            options = 0
159            if crs and bbox: options = 3
160            elif crs: options = 1
161            elif bbox: options = 2
162        s = {'desc' : 'GeoJSON',
163             'procedure_args' : {'precision' : precision, 'options' : options},
164             'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s',
165             }
166        return self._spatial_attribute('geojson', s, **kwargs)
167
168    def geohash(self, precision=20, **kwargs):
169        """
170        Returns a GeoHash representation of the given field in a `geohash`
171        attribute on each element of the GeoQuerySet.
172
173        The `precision` keyword may be used to custom the number of
174        _characters_ used in the output GeoHash, the default is 20.
175        """
176        s = {'desc' : 'GeoHash', 
177             'procedure_args': {'precision': precision},
178             'procedure_fmt': '%(geo_col)s,%(precision)s',
179             }
180        return self._spatial_attribute('geohash', s, **kwargs)
181
182    def gml(self, precision=8, version=2, **kwargs):
183        """
184        Returns GML representation of the given field in a `gml` attribute
185        on each element of the GeoQuerySet.
186        """
187        backend = connections[self.db].ops
188        s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
189        if backend.postgis:
190            # PostGIS AsGML() aggregate function parameter order depends on the
191            # version -- uggh.
192            if backend.spatial_version > (1, 3, 1):
193                procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
194            else:
195                procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
196            s['procedure_args'] = {'precision' : precision, 'version' : version}
197
198        return self._spatial_attribute('gml', s, **kwargs)
199
200    def intersection(self, geom, **kwargs):
201        """
202        Returns the spatial intersection of the Geometry field in
203        an `intersection` attribute on each element of this
204        GeoQuerySet.
205        """
206        return self._geomset_attribute('intersection', geom, **kwargs)
207
208    def kml(self, **kwargs):
209        """
210        Returns KML representation of the geometry field in a `kml`
211        attribute on each element of this GeoQuerySet.
212        """
213        s = {'desc' : 'KML',
214             'procedure_fmt' : '%(geo_col)s,%(precision)s',
215             'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
216             }
217        return self._spatial_attribute('kml', s, **kwargs)
218
219    def length(self, **kwargs):
220        """
221        Returns the length of the geometry field as a `Distance` object
222        stored in a `length` attribute on each element of this GeoQuerySet.
223        """
224        return self._distance_attribute('length', None, **kwargs)
225
226    def make_line(self, **kwargs):
227        """
228        Creates a linestring from all of the PointField geometries in the
229        this GeoQuerySet and returns it.  This is a spatial aggregate
230        method, and thus returns a geometry rather than a GeoQuerySet.
231        """
232        return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **kwargs)
233
234    def mem_size(self, **kwargs):
235        """
236        Returns the memory size (number of bytes) that the geometry field takes
237        in a `mem_size` attribute  on each element of this GeoQuerySet.
238        """
239        return self._spatial_attribute('mem_size', {}, **kwargs)
240
241    def num_geom(self, **kwargs):
242        """
243        Returns the number of geometries if the field is a
244        GeometryCollection or Multi* Field in a `num_geom`
245        attribute on each element of this GeoQuerySet; otherwise
246        the sets with None.
247        """
248        return self._spatial_attribute('num_geom', {}, **kwargs)
249
250    def num_points(self, **kwargs):
251        """
252        Returns the number of points in the first linestring in the
253        Geometry field in a `num_points` attribute on each element of
254        this GeoQuerySet; otherwise sets with None.
255        """
256        return self._spatial_attribute('num_points', {}, **kwargs)
257
258    def perimeter(self, **kwargs):
259        """
260        Returns the perimeter of the geometry field as a `Distance` object
261        stored in a `perimeter` attribute on each element of this GeoQuerySet.
262        """
263        return self._distance_attribute('perimeter', None, **kwargs)
264
265    def point_on_surface(self, **kwargs):
266        """
267        Returns a Point geometry guaranteed to lie on the surface of the
268        Geometry field in a `point_on_surface` attribute on each element
269        of this GeoQuerySet; otherwise sets with None.
270        """
271        return self._geom_attribute('point_on_surface', **kwargs)
272
273    def reverse_geom(self, **kwargs):
274        """
275        Reverses the coordinate order of the geometry, and attaches as a
276        `reverse` attribute on each element of this GeoQuerySet.
277        """
278        s = {'select_field' : GeomField(),}
279        kwargs.setdefault('model_att', 'reverse_geom')
280        if connections[self.db].ops.oracle:
281            s['geo_field_type'] = LineStringField
282        return self._spatial_attribute('reverse', s, **kwargs)
283
284    def scale(self, x, y, z=0.0, **kwargs):
285        """
286        Scales the geometry to a new size by multiplying the ordinates
287        with the given x,y,z scale factors.
288        """
289        if connections[self.db].ops.spatialite:
290            if z != 0.0:
291                raise NotImplementedError('SpatiaLite does not support 3D scaling.')
292            s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
293                 'procedure_args' : {'x' : x, 'y' : y},
294                 'select_field' : GeomField(),
295                 }
296        else:
297            s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
298                 'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
299                 'select_field' : GeomField(),
300                 }
301        return self._spatial_attribute('scale', s, **kwargs)
302
303    def snap_to_grid(self, *args, **kwargs):
304        """
305        Snap all points of the input geometry to the grid.  How the
306        geometry is snapped to the grid depends on how many arguments
307        were given:
308          - 1 argument : A single size to snap both the X and Y grids to.
309          - 2 arguments: X and Y sizes to snap the grid to.
310          - 4 arguments: X, Y sizes and the X, Y origins.
311        """
312        if False in [isinstance(arg, (float, int, long)) for arg in args]:
313            raise TypeError('Size argument(s) for the grid must be a float or integer values.')
314
315        nargs = len(args)
316        if nargs == 1:
317            size = args[0]
318            procedure_fmt = '%(geo_col)s,%(size)s'
319            procedure_args = {'size' : size}
320        elif nargs == 2:
321            xsize, ysize = args
322            procedure_fmt = '%(geo_col)s,%(xsize)s,%(ysize)s'
323            procedure_args = {'xsize' : xsize, 'ysize' : ysize}
324        elif nargs == 4:
325            xsize, ysize, xorigin, yorigin = args
326            procedure_fmt = '%(geo_col)s,%(xorigin)s,%(yorigin)s,%(xsize)s,%(ysize)s'
327            procedure_args = {'xsize' : xsize, 'ysize' : ysize,
328                              'xorigin' : xorigin, 'yorigin' : yorigin}
329        else:
330            raise ValueError('Must provide 1, 2, or 4 arguments to `snap_to_grid`.')
331
332        s = {'procedure_fmt' : procedure_fmt,
333             'procedure_args' : procedure_args,
334             'select_field' : GeomField(),
335             }
336
337        return self._spatial_attribute('snap_to_grid', s, **kwargs)
338
339    def svg(self, relative=False, precision=8, **kwargs):
340        """
341        Returns SVG representation of the geographic field in a `svg`
342        attribute on each element of this GeoQuerySet.
343
344        Keyword Arguments:
345         `relative`  => If set to True, this will evaluate the path in
346                        terms of relative moves (rather than absolute).
347
348         `precision` => May be used to set the maximum number of decimal
349                        digits used in output (defaults to 8).
350        """
351        relative = int(bool(relative))
352        if not isinstance(precision, (int, long)):
353            raise TypeError('SVG precision keyword argument must be an integer.')
354        s = {'desc' : 'SVG',
355             'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
356             'procedure_args' : {'rel' : relative,
357                                 'precision' : precision,
358                                 }
359             }
360        return self._spatial_attribute('svg', s, **kwargs)
361
362    def sym_difference(self, geom, **kwargs):
363        """
364        Returns the symmetric difference of the geographic field in a
365        `sym_difference` attribute on each element of this GeoQuerySet.
366        """
367        return self._geomset_attribute('sym_difference', geom, **kwargs)
368
369    def translate(self, x, y, z=0.0, **kwargs):
370        """
371        Translates the geometry to a new location using the given numeric
372        parameters as offsets.
373        """
374        if connections[self.db].ops.spatialite:
375            if z != 0.0:
376                raise NotImplementedError('SpatiaLite does not support 3D translation.')
377            s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
378                 'procedure_args' : {'x' : x, 'y' : y},
379                 'select_field' : GeomField(),
380                 }
381        else:
382            s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
383                 'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
384                 'select_field' : GeomField(),
385                 }
386        return self._spatial_attribute('translate', s, **kwargs)
387
388    def transform(self, srid=4326, **kwargs):
389        """
390        Transforms the given geometry field to the given SRID.  If no SRID is
391        provided, the transformation will default to using 4326 (WGS84).
392        """
393        if not isinstance(srid, (int, long)):
394            raise TypeError('An integer SRID must be provided.')
395        field_name = kwargs.get('field_name', None)
396        tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
397
398        # Getting the selection SQL for the given geographic field.
399        field_col = self._geocol_select(geo_field, field_name)
400
401        # Why cascading substitutions? Because spatial backends like
402        # Oracle and MySQL already require a function call to convert to text, thus
403        # when there's also a transformation we need to cascade the substitutions.
404        # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
405        geo_col = self.query.custom_select.get(geo_field, field_col)
406
407        # Setting the key for the field's column with the custom SELECT SQL to
408        # override the geometry column returned from the database.
409        custom_sel = '%s(%s, %s)' % (connections[self.db].ops.transform, geo_col, srid)
410        # TODO: Should we have this as an alias?
411        # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
412        self.query.transformed_srid = srid # So other GeoQuerySet methods
413        self.query.custom_select[geo_field] = custom_sel
414        return self._clone()
415
416    def union(self, geom, **kwargs):
417        """
418        Returns the union of the geographic field with the given
419        Geometry in a `union` attribute on each element of this GeoQuerySet.
420        """
421        return self._geomset_attribute('union', geom, **kwargs)
422
423    def unionagg(self, **kwargs):
424        """
425        Performs an aggregate union on the given geometry field.  Returns
426        None if the GeoQuerySet is empty.  The `tolerance` keyword is for
427        Oracle backends only.
428        """
429        return self._spatial_aggregate(aggregates.Union, **kwargs)
430
431    ### Private API -- Abstracted DRY routines. ###
432    def _spatial_setup(self, att, desc=None, field_name=None, geo_field_type=None):
433        """
434        Performs set up for executing the spatial function.
435        """
436        # Does the spatial backend support this?
437        connection = connections[self.db]
438        func = getattr(connection.ops, att, False)
439        if desc is None: desc = att
440        if not func:
441            raise NotImplementedError('%s stored procedure not available on '
442                                      'the %s backend.' %
443                                      (desc, connection.ops.name))
444
445        # Initializing the procedure arguments.
446        procedure_args = {'function' : func}
447
448        # Is there a geographic field in the model to perform this
449        # operation on?
450        geo_field = self.query._geo_field(field_name)
451        if not geo_field:
452            raise TypeError('%s output only available on GeometryFields.' % func)
453
454        # If the `geo_field_type` keyword was used, then enforce that
455        # type limitation.
456        if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
457            raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__))
458
459        # Setting the procedure args.
460        procedure_args['geo_col'] = self._geocol_select(geo_field, field_name)
461
462        return procedure_args, geo_field
463
464    def _spatial_aggregate(self, aggregate, field_name=None,
465                           geo_field_type=None, tolerance=0.05):
466        """
467        DRY routine for calling aggregate spatial stored procedures and
468        returning their result to the caller of the function.
469        """
470        # Getting the field the geographic aggregate will be called on.
471        geo_field = self.query._geo_field(field_name)
472        if not geo_field:
473            raise TypeError('%s aggregate only available on GeometryFields.' % aggregate.name)
474
475        # Checking if there are any geo field type limitations on this
476        # aggregate (e.g. ST_Makeline only operates on PointFields).
477        if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
478            raise TypeError('%s aggregate may only be called on %ss.' % (aggregate.name, geo_field_type.__name__))
479
480        # Getting the string expression of the field name, as this is the
481        # argument taken by `Aggregate` objects.
482        agg_col = field_name or geo_field.name
483
484        # Adding any keyword parameters for the Aggregate object. Oracle backends
485        # in particular need an additional `tolerance` parameter.
486        agg_kwargs = {}
487        if connections[self.db].ops.oracle: agg_kwargs['tolerance'] = tolerance
488
489        # Calling the QuerySet.aggregate, and returning only the value of the aggregate.
490        return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg']
491
492    def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
493        """
494        DRY routine for calling a spatial stored procedure on a geometry column
495        and attaching its output as an attribute of the model.
496
497        Arguments:
498         att:
499          The name of the spatial attribute that holds the spatial
500          SQL function to call.
501
502         settings:
503          Dictonary of internal settings to customize for the spatial procedure.
504
505        Public Keyword Arguments:
506
507         field_name:
508          The name of the geographic field to call the spatial
509          function on.  May also be a lookup to a geometry field
510          as part of a foreign key relation.
511
512         model_att:
513          The name of the model attribute to attach the output of
514          the spatial function to.
515        """
516        # Default settings.
517        settings.setdefault('desc', None)
518        settings.setdefault('geom_args', ())
519        settings.setdefault('geom_field', None)
520        settings.setdefault('procedure_args', {})
521        settings.setdefault('procedure_fmt', '%(geo_col)s')
522        settings.setdefault('select_params', [])
523
524        connection = connections[self.db]
525        backend = connection.ops
526
527        # Performing setup for the spatial column, unless told not to.
528        if settings.get('setup', True):
529            default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name,
530                                                          geo_field_type=settings.get('geo_field_type', None))
531            for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
532        else:
533            geo_field = settings['geo_field']
534
535        # The attribute to attach to the model.
536        if not isinstance(model_att, basestring): model_att = att
537
538        # Special handling for any argument that is a geometry.
539        for name in settings['geom_args']:
540            # Using the field's get_placeholder() routine to get any needed
541            # transformation SQL.
542            geom = geo_field.get_prep_value(settings['procedure_args'][name])
543            params = geo_field.get_db_prep_lookup('contains', geom, connection=connection)
544            geom_placeholder = geo_field.get_placeholder(geom, connection)
545
546            # Replacing the procedure format with that of any needed
547            # transformation SQL.
548            old_fmt = '%%(%s)s' % name
549            new_fmt = geom_placeholder % '%%s'
550            settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
551            settings['select_params'].extend(params)
552
553        # Getting the format for the stored procedure.
554        fmt = '%%(function)s(%s)' % settings['procedure_fmt']
555
556        # If the result of this function needs to be converted.
557        if settings.get('select_field', False):
558            sel_fld = settings['select_field']
559            if isinstance(sel_fld, GeomField) and backend.select:
560                self.query.custom_select[model_att] = backend.select
561            if connection.ops.oracle:
562                sel_fld.empty_strings_allowed = False
563            self.query.extra_select_fields[model_att] = sel_fld
564
565        # Finally, setting the extra selection attribute with
566        # the format string expanded with the stored procedure
567        # arguments.
568        return self.extra(select={model_att : fmt % settings['procedure_args']},
569                          select_params=settings['select_params'])
570
571    def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
572        """
573        DRY routine for GeoQuerySet distance attribute routines.
574        """
575        # Setting up the distance procedure arguments.
576        procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
577
578        # If geodetic defaulting distance attribute to meters (Oracle and
579        # PostGIS spherical distances return meters).  Otherwise, use the
580        # units of the geometry field.
581        connection = connections[self.db]
582        geodetic = geo_field.geodetic(connection)
583        geography = geo_field.geography
584
585        if geodetic:
586            dist_att = 'm'
587        else:
588            dist_att = Distance.unit_attname(geo_field.units_name(connection))
589
590        # Shortcut booleans for what distance function we're using and
591        # whether the geometry field is 3D.
592        distance = func == 'distance'
593        length = func == 'length'
594        perimeter = func == 'perimeter'
595        if not (distance or length or perimeter):
596            raise ValueError('Unknown distance function: %s' % func)
597        geom_3d = geo_field.dim == 3
598
599        # The field's get_db_prep_lookup() is used to get any
600        # extra distance parameters.  Here we set up the
601        # parameters that will be passed in to field's function.
602        lookup_params = [geom or 'POINT (0 0)', 0]
603
604        # Getting the spatial backend operations.
605        backend = connection.ops
606
607        # If the spheroid calculation is desired, either by the `spheroid`
608        # keyword or when calculating the length of geodetic field, make
609        # sure the 'spheroid' distance setting string is passed in so we
610        # get the correct spatial stored procedure.
611        if spheroid or (backend.postgis and geodetic and
612                        (not geography) and length):
613            lookup_params.append('spheroid')
614        lookup_params = geo_field.get_prep_value(lookup_params)
615        params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection)
616
617        # The `geom_args` flag is set to true if a geometry parameter was
618        # passed in.
619        geom_args = bool(geom)
620
621        if backend.oracle:
622            if distance:
623                procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
624            elif length or perimeter:
625                procedure_fmt = '%(geo_col)s,%(tolerance)s'
626            procedure_args['tolerance'] = tolerance
627        else:
628            # Getting whether this field is in units of degrees since the field may have
629            # been transformed via the `transform` GeoQuerySet method.
630            if self.query.transformed_srid:
631                u, unit_name, s = get_srid_info(self.query.transformed_srid, connection)
632                geodetic = unit_name in geo_field.geodetic_units
633
634            if backend.spatialite and geodetic:
635                raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.')
636
637            if distance:
638                if self.query.transformed_srid:
639                    # Setting the `geom_args` flag to false because we want to handle
640                    # transformation SQL here, rather than the way done by default
641                    # (which will transform to the original SRID of the field rather
642                    #  than to what was transformed to).
643                    geom_args = False
644                    procedure_fmt = '%s(%%(geo_col)s, %s)' % (backend.transform, self.query.transformed_srid)
645                    if geom.srid is None or geom.srid == self.query.transformed_srid:
646                        # If the geom parameter srid is None, it is assumed the coordinates
647                        # are in the transformed units.  A placeholder is used for the
648                        # geometry parameter.  `GeomFromText` constructor is also needed
649                        # to wrap geom placeholder for SpatiaLite.
650                        if backend.spatialite:
651                            procedure_fmt += ', %s(%%%%s, %s)' % (backend.from_text, self.query.transformed_srid)
652                        else:
653                            procedure_fmt += ', %%s'
654                    else:
655                        # We need to transform the geom to the srid specified in `transform()`,
656                        # so wrapping the geometry placeholder in transformation SQL.
657                        # SpatiaLite also needs geometry placeholder wrapped in `GeomFromText`
658                        # constructor.
659                        if backend.spatialite:
660                            procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text,
661                                                                          geom.srid, self.query.transformed_srid)
662                        else:
663                            procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid)
664                else:
665                    # `transform()` was not used on this GeoQuerySet.
666                    procedure_fmt  = '%(geo_col)s,%(geom)s'
667
668                if not geography and geodetic:
669                    # Spherical distance calculation is needed (because the geographic
670                    # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
671                    # procedures may only do queries from point columns to point geometries
672                    # some error checking is required.
673                    if not backend.geography:
674                        if not isinstance(geo_field, PointField):
675                            raise ValueError('Spherical distance calculation only supported on PointFields.')
676                        if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
677                            raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
678                    # The `function` procedure argument needs to be set differently for
679                    # geodetic distance calculations.
680                    if spheroid:
681                        # Call to distance_spheroid() requires spheroid param as well.
682                        procedure_fmt += ",'%(spheroid)s'"
683                        procedure_args.update({'function' : backend.distance_spheroid, 'spheroid' : params[1]})
684                    else:
685                        procedure_args.update({'function' : backend.distance_sphere})
686            elif length or perimeter:
687                procedure_fmt = '%(geo_col)s'
688                if not geography and geodetic and length:
689                    # There's no `length_sphere`, and `length_spheroid` also
690                    # works on 3D geometries.
691                    procedure_fmt += ",'%(spheroid)s'"
692                    procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]})
693                elif geom_3d and backend.postgis:
694                    # Use 3D variants of perimeter and length routines on PostGIS.
695                    if perimeter:
696                        procedure_args.update({'function' : backend.perimeter3d})
697                    elif length:
698                        procedure_args.update({'function' : backend.length3d})
699
700        # Setting up the settings for `_spatial_attribute`.
701        s = {'select_field' : DistanceField(dist_att),
702             'setup' : False,
703             'geo_field' : geo_field,
704             'procedure_args' : procedure_args,
705             'procedure_fmt' : procedure_fmt,
706             }
707        if geom_args:
708            s['geom_args'] = ('geom',)
709            s['procedure_args']['geom'] = geom
710        elif geom:
711            # The geometry is passed in as a parameter because we handled
712            # transformation conditions in this routine.
713            s['select_params'] = [backend.Adapter(geom)]
714        return self._spatial_attribute(func, s, **kwargs)
715
716    def _geom_attribute(self, func, tolerance=0.05, **kwargs):
717        """
718        DRY routine for setting up a GeoQuerySet method that attaches a
719        Geometry attribute (e.g., `centroid`, `point_on_surface`).
720        """
721        s = {'select_field' : GeomField(),}
722        if connections[self.db].ops.oracle:
723            s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
724            s['procedure_args'] = {'tolerance' : tolerance}
725        return self._spatial_attribute(func, s, **kwargs)
726
727    def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
728        """
729        DRY routine for setting up a GeoQuerySet method that attaches a
730        Geometry attribute and takes a Geoemtry parameter.  This is used
731        for geometry set-like operations (e.g., intersection, difference,
732        union, sym_difference).
733        """
734        s = {'geom_args' : ('geom',),
735             'select_field' : GeomField(),
736             'procedure_fmt' : '%(geo_col)s,%(geom)s',
737             'procedure_args' : {'geom' : geom},
738            }
739        if connections[self.db].ops.oracle:
740            s['procedure_fmt'] += ',%(tolerance)s'
741            s['procedure_args']['tolerance'] = tolerance
742        return self._spatial_attribute(func, s, **kwargs)
743
744    def _geocol_select(self, geo_field, field_name):
745        """
746        Helper routine for constructing the SQL to select the geographic
747        column.  Takes into account if the geographic field is in a
748        ForeignKey relation to the current model.
749        """
750        opts = self.model._meta
751        if not geo_field in opts.fields:
752            # Is this operation going to be on a related geographic field?
753            # If so, it'll have to be added to the select related information
754            # (e.g., if 'location__point' was given as the field name).
755            self.query.add_select_related([field_name])
756            compiler = self.query.get_compiler(self.db)
757            compiler.pre_sql_setup()
758            rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
759            return compiler._field_column(geo_field, rel_table)
760        elif not geo_field in opts.local_fields:
761            # This geographic field is inherited from another model, so we have to
762            # use the db table for the _parent_ model instead.
763            tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name)
764            return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table)
765        else:
766            return self.query.get_compiler(self.db)._field_column(geo_field)
767
768class GeoValuesQuerySet(ValuesQuerySet):
769    def __init__(self, *args, **kwargs):
770        super(GeoValuesQuerySet, self).__init__(*args, **kwargs)
771        # This flag tells `resolve_columns` to run the values through
772        # `convert_values`.  This ensures that Geometry objects instead
773        # of string values are returned with `values()` or `values_list()`.
774        self.query.geo_values = True
775
776class GeoValuesListQuerySet(GeoValuesQuerySet, ValuesListQuerySet):
777    pass