diff -r 000000000000 -r 0d40e90630ef web/lib/django/contrib/gis/db/models/query.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/lib/django/contrib/gis/db/models/query.py Wed Jan 20 00:34:04 2010 +0100 @@ -0,0 +1,700 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db import connection +from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet + +from django.contrib.gis.db.backend import SpatialBackend +from django.contrib.gis.db.models import aggregates +from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField +from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode +from django.contrib.gis.measure import Area, Distance + +class GeoQuerySet(QuerySet): + "The Geographic QuerySet." + + ### Methods overloaded from QuerySet ### + def __init__(self, model=None, query=None): + super(GeoQuerySet, self).__init__(model=model, query=query) + self.query = query or GeoQuery(self.model, connection) + + def values(self, *fields): + return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields) + + def values_list(self, *fields, **kwargs): + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' + % (kwargs.keys(),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is called with more than one field.") + return self._clone(klass=GeoValuesListQuerySet, setup=True, flat=flat, + _fields=fields) + + ### GeoQuerySet Methods ### + def area(self, tolerance=0.05, **kwargs): + """ + Returns the area of the geographic field in an `area` attribute on + each element of this GeoQuerySet. + """ + # Peforming setup here rather than in `_spatial_attribute` so that + # we can get the units for `AreaField`. + procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None)) + s = {'procedure_args' : procedure_args, + 'geo_field' : geo_field, + 'setup' : False, + } + if SpatialBackend.oracle: + s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' + s['procedure_args']['tolerance'] = tolerance + s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. + elif SpatialBackend.postgis or SpatialBackend.spatialite: + if not geo_field.geodetic: + # Getting the area units of the geographic field. + s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name)) + else: + # TODO: Do we want to support raw number areas for geodetic fields? + raise Exception('Area on geodetic coordinate systems not supported.') + return self._spatial_attribute('area', s, **kwargs) + + def centroid(self, **kwargs): + """ + Returns the centroid of the geographic field in a `centroid` + attribute on each element of this GeoQuerySet. + """ + return self._geom_attribute('centroid', **kwargs) + + def collect(self, **kwargs): + """ + Performs an aggregate collect operation on the given geometry field. + This is analagous to a union operation, but much faster because + boundaries are not dissolved. + """ + return self._spatial_aggregate(aggregates.Collect, **kwargs) + + def difference(self, geom, **kwargs): + """ + Returns the spatial difference of the geographic field in a `difference` + attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('difference', geom, **kwargs) + + def distance(self, geom, **kwargs): + """ + Returns the distance from the given geographic field name to the + given geometry in a `distance` attribute on each element of the + GeoQuerySet. + + Keyword Arguments: + `spheroid` => If the geometry field is geodetic and PostGIS is + the spatial database, then the more accurate + spheroid calculation will be used instead of the + quicker sphere calculation. + + `tolerance` => Used only for Oracle. The tolerance is + in meters -- a default of 5 centimeters (0.05) + is used. + """ + return self._distance_attribute('distance', geom, **kwargs) + + def envelope(self, **kwargs): + """ + Returns a Geometry representing the bounding box of the + Geometry field in an `envelope` attribute on each element of + the GeoQuerySet. + """ + return self._geom_attribute('envelope', **kwargs) + + def extent(self, **kwargs): + """ + Returns the extent (aggregate) of the features in the GeoQuerySet. The + extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). + """ + return self._spatial_aggregate(aggregates.Extent, **kwargs) + + def geojson(self, precision=8, crs=False, bbox=False, **kwargs): + """ + Returns a GeoJSON representation of the geomtry field in a `geojson` + attribute on each element of the GeoQuerySet. + + The `crs` and `bbox` keywords may be set to True if the users wants + the coordinate reference system and the bounding box to be included + in the GeoJSON representation of the geometry. + """ + if not SpatialBackend.postgis or not SpatialBackend.geojson: + raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') + + if not isinstance(precision, (int, long)): + raise TypeError('Precision keyword must be set with an integer.') + + # Setting the options flag -- which depends on which version of + # PostGIS we're using. + major, minor1, minor2 = SpatialBackend.version + if major >=1 and (minor1 >= 4): + options = 0 + if crs and bbox: options = 3 + elif bbox: options = 1 + elif crs: options = 2 + else: + options = 0 + if crs and bbox: options = 3 + elif crs: options = 1 + elif bbox: options = 2 + s = {'desc' : 'GeoJSON', + 'procedure_args' : {'precision' : precision, 'options' : options}, + 'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s', + } + return self._spatial_attribute('geojson', s, **kwargs) + + def gml(self, precision=8, version=2, **kwargs): + """ + Returns GML representation of the given field in a `gml` attribute + on each element of the GeoQuerySet. + """ + s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} + if SpatialBackend.postgis: + # PostGIS AsGML() aggregate function parameter order depends on the + # version -- uggh. + major, minor1, minor2 = SpatialBackend.version + if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): + procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' + else: + procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' + s['procedure_args'] = {'precision' : precision, 'version' : version} + + return self._spatial_attribute('gml', s, **kwargs) + + def intersection(self, geom, **kwargs): + """ + Returns the spatial intersection of the Geometry field in + an `intersection` attribute on each element of this + GeoQuerySet. + """ + return self._geomset_attribute('intersection', geom, **kwargs) + + def kml(self, **kwargs): + """ + Returns KML representation of the geometry field in a `kml` + attribute on each element of this GeoQuerySet. + """ + s = {'desc' : 'KML', + 'procedure_fmt' : '%(geo_col)s,%(precision)s', + 'procedure_args' : {'precision' : kwargs.pop('precision', 8)}, + } + return self._spatial_attribute('kml', s, **kwargs) + + def length(self, **kwargs): + """ + Returns the length of the geometry field as a `Distance` object + stored in a `length` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('length', None, **kwargs) + + def make_line(self, **kwargs): + """ + Creates a linestring from all of the PointField geometries in the + this GeoQuerySet and returns it. This is a spatial aggregate + method, and thus returns a geometry rather than a GeoQuerySet. + """ + return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **kwargs) + + def mem_size(self, **kwargs): + """ + Returns the memory size (number of bytes) that the geometry field takes + in a `mem_size` attribute on each element of this GeoQuerySet. + """ + return self._spatial_attribute('mem_size', {}, **kwargs) + + def num_geom(self, **kwargs): + """ + Returns the number of geometries if the field is a + GeometryCollection or Multi* Field in a `num_geom` + attribute on each element of this GeoQuerySet; otherwise + the sets with None. + """ + return self._spatial_attribute('num_geom', {}, **kwargs) + + def num_points(self, **kwargs): + """ + Returns the number of points in the first linestring in the + Geometry field in a `num_points` attribute on each element of + this GeoQuerySet; otherwise sets with None. + """ + return self._spatial_attribute('num_points', {}, **kwargs) + + def perimeter(self, **kwargs): + """ + Returns the perimeter of the geometry field as a `Distance` object + stored in a `perimeter` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('perimeter', None, **kwargs) + + def point_on_surface(self, **kwargs): + """ + Returns a Point geometry guaranteed to lie on the surface of the + Geometry field in a `point_on_surface` attribute on each element + of this GeoQuerySet; otherwise sets with None. + """ + return self._geom_attribute('point_on_surface', **kwargs) + + def scale(self, x, y, z=0.0, **kwargs): + """ + Scales the geometry to a new size by multiplying the ordinates + with the given x,y,z scale factors. + """ + if SpatialBackend.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D scaling.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('scale', s, **kwargs) + + def snap_to_grid(self, *args, **kwargs): + """ + Snap all points of the input geometry to the grid. How the + geometry is snapped to the grid depends on how many arguments + were given: + - 1 argument : A single size to snap both the X and Y grids to. + - 2 arguments: X and Y sizes to snap the grid to. + - 4 arguments: X, Y sizes and the X, Y origins. + """ + if False in [isinstance(arg, (float, int, long)) for arg in args]: + raise TypeError('Size argument(s) for the grid must be a float or integer values.') + + nargs = len(args) + if nargs == 1: + size = args[0] + procedure_fmt = '%(geo_col)s,%(size)s' + procedure_args = {'size' : size} + elif nargs == 2: + xsize, ysize = args + procedure_fmt = '%(geo_col)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize} + elif nargs == 4: + xsize, ysize, xorigin, yorigin = args + procedure_fmt = '%(geo_col)s,%(xorigin)s,%(yorigin)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize, + 'xorigin' : xorigin, 'yorigin' : yorigin} + else: + raise ValueError('Must provide 1, 2, or 4 arguments to `snap_to_grid`.') + + s = {'procedure_fmt' : procedure_fmt, + 'procedure_args' : procedure_args, + 'select_field' : GeomField(), + } + + return self._spatial_attribute('snap_to_grid', s, **kwargs) + + def svg(self, relative=False, precision=8, **kwargs): + """ + Returns SVG representation of the geographic field in a `svg` + attribute on each element of this GeoQuerySet. + + Keyword Arguments: + `relative` => If set to True, this will evaluate the path in + terms of relative moves (rather than absolute). + + `precision` => May be used to set the maximum number of decimal + digits used in output (defaults to 8). + """ + relative = int(bool(relative)) + if not isinstance(precision, (int, long)): + raise TypeError('SVG precision keyword argument must be an integer.') + s = {'desc' : 'SVG', + 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', + 'procedure_args' : {'rel' : relative, + 'precision' : precision, + } + } + return self._spatial_attribute('svg', s, **kwargs) + + def sym_difference(self, geom, **kwargs): + """ + Returns the symmetric difference of the geographic field in a + `sym_difference` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('sym_difference', geom, **kwargs) + + def translate(self, x, y, z=0.0, **kwargs): + """ + Translates the geometry to a new location using the given numeric + parameters as offsets. + """ + if SpatialBackend.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D translation.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('translate', s, **kwargs) + + def transform(self, srid=4326, **kwargs): + """ + Transforms the given geometry field to the given SRID. If no SRID is + provided, the transformation will default to using 4326 (WGS84). + """ + if not isinstance(srid, (int, long)): + raise TypeError('An integer SRID must be provided.') + field_name = kwargs.get('field_name', None) + tmp, geo_field = self._spatial_setup('transform', field_name=field_name) + + # Getting the selection SQL for the given geographic field. + field_col = self._geocol_select(geo_field, field_name) + + # Why cascading substitutions? Because spatial backends like + # Oracle and MySQL already require a function call to convert to text, thus + # when there's also a transformation we need to cascade the substitutions. + # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' + geo_col = self.query.custom_select.get(geo_field, field_col) + + # Setting the key for the field's column with the custom SELECT SQL to + # override the geometry column returned from the database. + custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid) + # TODO: Should we have this as an alias? + # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name)) + self.query.transformed_srid = srid # So other GeoQuerySet methods + self.query.custom_select[geo_field] = custom_sel + return self._clone() + + def union(self, geom, **kwargs): + """ + Returns the union of the geographic field with the given + Geometry in a `union` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('union', geom, **kwargs) + + def unionagg(self, **kwargs): + """ + Performs an aggregate union on the given geometry field. Returns + None if the GeoQuerySet is empty. The `tolerance` keyword is for + Oracle backends only. + """ + return self._spatial_aggregate(aggregates.Union, **kwargs) + + ### Private API -- Abstracted DRY routines. ### + def _spatial_setup(self, att, desc=None, field_name=None, geo_field_type=None): + """ + Performs set up for executing the spatial function. + """ + # Does the spatial backend support this? + func = getattr(SpatialBackend, att, False) + if desc is None: desc = att + if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc) + + # Initializing the procedure arguments. + procedure_args = {'function' : func} + + # Is there a geographic field in the model to perform this + # operation on? + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s output only available on GeometryFields.' % func) + + # If the `geo_field_type` keyword was used, then enforce that + # type limitation. + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) + + # Setting the procedure args. + procedure_args['geo_col'] = self._geocol_select(geo_field, field_name) + + return procedure_args, geo_field + + def _spatial_aggregate(self, aggregate, field_name=None, + geo_field_type=None, tolerance=0.05): + """ + DRY routine for calling aggregate spatial stored procedures and + returning their result to the caller of the function. + """ + # Getting the field the geographic aggregate will be called on. + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s aggregate only available on GeometryFields.' % aggregate.name) + + # Checking if there are any geo field type limitations on this + # aggregate (e.g. ST_Makeline only operates on PointFields). + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('%s aggregate may only be called on %ss.' % (aggregate.name, geo_field_type.__name__)) + + # Getting the string expression of the field name, as this is the + # argument taken by `Aggregate` objects. + agg_col = field_name or geo_field.name + + # Adding any keyword parameters for the Aggregate object. Oracle backends + # in particular need an additional `tolerance` parameter. + agg_kwargs = {} + if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance + + # Calling the QuerySet.aggregate, and returning only the value of the aggregate. + return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg'] + + def _spatial_attribute(self, att, settings, field_name=None, model_att=None): + """ + DRY routine for calling a spatial stored procedure on a geometry column + and attaching its output as an attribute of the model. + + Arguments: + att: + The name of the spatial attribute that holds the spatial + SQL function to call. + + settings: + Dictonary of internal settings to customize for the spatial procedure. + + Public Keyword Arguments: + + field_name: + The name of the geographic field to call the spatial + function on. May also be a lookup to a geometry field + as part of a foreign key relation. + + model_att: + The name of the model attribute to attach the output of + the spatial function to. + """ + # Default settings. + settings.setdefault('desc', None) + settings.setdefault('geom_args', ()) + settings.setdefault('geom_field', None) + settings.setdefault('procedure_args', {}) + settings.setdefault('procedure_fmt', '%(geo_col)s') + settings.setdefault('select_params', []) + + # Performing setup for the spatial column, unless told not to. + if settings.get('setup', True): + default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name) + for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) + else: + geo_field = settings['geo_field'] + + # The attribute to attach to the model. + if not isinstance(model_att, basestring): model_att = att + + # Special handling for any argument that is a geometry. + for name in settings['geom_args']: + # Using the field's get_db_prep_lookup() to get any needed + # transformation SQL -- we pass in a 'dummy' `contains` lookup. + where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name]) + # Replacing the procedure format with that of any needed + # transformation SQL. + old_fmt = '%%(%s)s' % name + new_fmt = where[0] % '%%s' + settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt) + settings['select_params'].extend(params) + + # Getting the format for the stored procedure. + fmt = '%%(function)s(%s)' % settings['procedure_fmt'] + + # If the result of this function needs to be converted. + if settings.get('select_field', False): + sel_fld = settings['select_field'] + if isinstance(sel_fld, GeomField) and SpatialBackend.select: + self.query.custom_select[model_att] = SpatialBackend.select + self.query.extra_select_fields[model_att] = sel_fld + + # Finally, setting the extra selection attribute with + # the format string expanded with the stored procedure + # arguments. + return self.extra(select={model_att : fmt % settings['procedure_args']}, + select_params=settings['select_params']) + + def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): + """ + DRY routine for GeoQuerySet distance attribute routines. + """ + # Setting up the distance procedure arguments. + procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None)) + + # If geodetic defaulting distance attribute to meters (Oracle and + # PostGIS spherical distances return meters). Otherwise, use the + # units of the geometry field. + if geo_field.geodetic: + dist_att = 'm' + else: + dist_att = Distance.unit_attname(geo_field.units_name) + + # Shortcut booleans for what distance function we're using. + distance = func == 'distance' + length = func == 'length' + perimeter = func == 'perimeter' + if not (distance or length or perimeter): + raise ValueError('Unknown distance function: %s' % func) + + # The field's get_db_prep_lookup() is used to get any + # extra distance parameters. Here we set up the + # parameters that will be passed in to field's function. + lookup_params = [geom or 'POINT (0 0)', 0] + + # If the spheroid calculation is desired, either by the `spheroid` + # keyword or when calculating the length of geodetic field, make + # sure the 'spheroid' distance setting string is passed in so we + # get the correct spatial stored procedure. + if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): + lookup_params.append('spheroid') + where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params) + + # The `geom_args` flag is set to true if a geometry parameter was + # passed in. + geom_args = bool(geom) + + if SpatialBackend.oracle: + if distance: + procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s' + elif length or perimeter: + procedure_fmt = '%(geo_col)s,%(tolerance)s' + procedure_args['tolerance'] = tolerance + else: + # Getting whether this field is in units of degrees since the field may have + # been transformed via the `transform` GeoQuerySet method. + if self.query.transformed_srid: + u, unit_name, s = get_srid_info(self.query.transformed_srid) + geodetic = unit_name in geo_field.geodetic_units + else: + geodetic = geo_field.geodetic + + if SpatialBackend.spatialite and geodetic: + raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.') + + if distance: + if self.query.transformed_srid: + # Setting the `geom_args` flag to false because we want to handle + # transformation SQL here, rather than the way done by default + # (which will transform to the original SRID of the field rather + # than to what was transformed to). + geom_args = False + procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + if geom.srid is None or geom.srid == self.query.transformed_srid: + # If the geom parameter srid is None, it is assumed the coordinates + # are in the transformed units. A placeholder is used for the + # geometry parameter. `GeomFromText` constructor is also needed + # to wrap geom placeholder for SpatiaLite. + if SpatialBackend.spatialite: + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.from_text, self.query.transformed_srid) + else: + procedure_fmt += ', %%s' + else: + # We need to transform the geom to the srid specified in `transform()`, + # so wrapping the geometry placeholder in transformation SQL. + # SpatiaLite also needs geometry placeholder wrapped in `GeomFromText` + # constructor. + if SpatialBackend.spatialite: + procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (SpatialBackend.transform, SpatialBackend.from_text, + geom.srid, self.query.transformed_srid) + else: + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + else: + # `transform()` was not used on this GeoQuerySet. + procedure_fmt = '%(geo_col)s,%(geom)s' + + if geodetic: + # Spherical distance calculation is needed (because the geographic + # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() + # procedures may only do queries from point columns to point geometries + # some error checking is required. + if not isinstance(geo_field, PointField): + raise ValueError('Spherical distance calculation only supported on PointFields.') + if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': + raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') + # The `function` procedure argument needs to be set differently for + # geodetic distance calculations. + if spheroid: + # Call to distance_spheroid() requires spheroid param as well. + procedure_fmt += ',%(spheroid)s' + procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]}) + else: + procedure_args.update({'function' : SpatialBackend.distance_sphere}) + elif length or perimeter: + procedure_fmt = '%(geo_col)s' + if geodetic and length: + # There's no `length_sphere` + procedure_fmt += ',%(spheroid)s' + procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]}) + + # Setting up the settings for `_spatial_attribute`. + s = {'select_field' : DistanceField(dist_att), + 'setup' : False, + 'geo_field' : geo_field, + 'procedure_args' : procedure_args, + 'procedure_fmt' : procedure_fmt, + } + if geom_args: + s['geom_args'] = ('geom',) + s['procedure_args']['geom'] = geom + elif geom: + # The geometry is passed in as a parameter because we handled + # transformation conditions in this routine. + s['select_params'] = [SpatialBackend.Adaptor(geom)] + return self._spatial_attribute(func, s, **kwargs) + + def _geom_attribute(self, func, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute (e.g., `centroid`, `point_on_surface`). + """ + s = {'select_field' : GeomField(),} + if SpatialBackend.oracle: + s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' + s['procedure_args'] = {'tolerance' : tolerance} + return self._spatial_attribute(func, s, **kwargs) + + def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute and takes a Geoemtry parameter. This is used + for geometry set-like operations (e.g., intersection, difference, + union, sym_difference). + """ + s = {'geom_args' : ('geom',), + 'select_field' : GeomField(), + 'procedure_fmt' : '%(geo_col)s,%(geom)s', + 'procedure_args' : {'geom' : geom}, + } + if SpatialBackend.oracle: + s['procedure_fmt'] += ',%(tolerance)s' + s['procedure_args']['tolerance'] = tolerance + return self._spatial_attribute(func, s, **kwargs) + + def _geocol_select(self, geo_field, field_name): + """ + Helper routine for constructing the SQL to select the geographic + column. Takes into account if the geographic field is in a + ForeignKey relation to the current model. + """ + opts = self.model._meta + if not geo_field in opts.fields: + # Is this operation going to be on a related geographic field? + # If so, it'll have to be added to the select related information + # (e.g., if 'location__point' was given as the field name). + self.query.add_select_related([field_name]) + self.query.pre_sql_setup() + rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] + return self.query._field_column(geo_field, rel_table) + elif not geo_field in opts.local_fields: + # This geographic field is inherited from another model, so we have to + # use the db table for the _parent_ model instead. + tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name) + return self.query._field_column(geo_field, parent_model._meta.db_table) + else: + return self.query._field_column(geo_field) + +class GeoValuesQuerySet(ValuesQuerySet): + def __init__(self, *args, **kwargs): + super(GeoValuesQuerySet, self).__init__(*args, **kwargs) + # This flag tells `resolve_columns` to run the values through + # `convert_values`. This ensures that Geometry objects instead + # of string values are returned with `values()` or `values_list()`. + self.query.geo_values = True + +class GeoValuesListQuerySet(GeoValuesQuerySet, ValuesListQuerySet): + pass