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