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