| from django.db import connections |
| from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet |
| |
| from django.contrib.gis.db.models import aggregates |
| from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField, LineStringField |
| from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode |
| from django.contrib.gis.geometry.backend import Geometry |
| 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, using=None): |
| super(GeoQuerySet, self).__init__(model=model, query=query, using=using) |
| self.query = query or GeoQuery(self.model) |
| |
| 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, |
| } |
| connection = connections[self.db] |
| backend = connection.ops |
| if backend.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 backend.postgis or backend.spatialite: |
| if backend.geography: |
| # Geography fields support area calculation, returns square meters. |
| s['select_field'] = AreaField('sq_m') |
| elif not geo_field.geodetic(connection): |
| # Getting the area units of the geographic field. |
| s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection))) |
| 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 extent3d(self, **kwargs): |
| """ |
| Returns the aggregate extent, in 3D, of the features in the |
| GeoQuerySet. It is returned as a 6-tuple, comprising: |
| (xmin, ymin, zmin, xmax, ymax, zmax). |
| """ |
| return self._spatial_aggregate(aggregates.Extent3D, **kwargs) |
| |
| def force_rhr(self, **kwargs): |
| """ |
| Returns a modified version of the Polygon/MultiPolygon in which |
| all of the vertices follow the Right-Hand-Rule. By default, |
| this is attached as the `force_rhr` attribute on each element |
| of the GeoQuerySet. |
| """ |
| return self._geom_attribute('force_rhr', **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. |
| """ |
| backend = connections[self.db].ops |
| if not backend.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. |
| if backend.spatial_version >= (1, 4, 0): |
| 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 geohash(self, precision=20, **kwargs): |
| """ |
| Returns a GeoHash representation of the given field in a `geohash` |
| attribute on each element of the GeoQuerySet. |
| |
| The `precision` keyword may be used to custom the number of |
| _characters_ used in the output GeoHash, the default is 20. |
| """ |
| s = {'desc' : 'GeoHash', |
| 'procedure_args': {'precision': precision}, |
| 'procedure_fmt': '%(geo_col)s,%(precision)s', |
| } |
| return self._spatial_attribute('geohash', 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. |
| """ |
| backend = connections[self.db].ops |
| s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} |
| if backend.postgis: |
| # PostGIS AsGML() aggregate function parameter order depends on the |
| # version -- uggh. |
| if backend.spatial_version > (1, 3, 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 reverse_geom(self, **kwargs): |
| """ |
| Reverses the coordinate order of the geometry, and attaches as a |
| `reverse` attribute on each element of this GeoQuerySet. |
| """ |
| s = {'select_field' : GeomField(),} |
| kwargs.setdefault('model_att', 'reverse_geom') |
| if connections[self.db].ops.oracle: |
| s['geo_field_type'] = LineStringField |
| return self._spatial_attribute('reverse', s, **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 connections[self.db].ops.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 connections[self.db].ops.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)' % (connections[self.db].ops.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? |
| connection = connections[self.db] |
| func = getattr(connection.ops, att, False) |
| if desc is None: desc = att |
| if not func: |
| raise NotImplementedError('%s stored procedure not available on ' |
| 'the %s backend.' % |
| (desc, connection.ops.name)) |
| |
| # 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 connections[self.db].ops.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', []) |
| |
| connection = connections[self.db] |
| backend = connection.ops |
| |
| # 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, |
| geo_field_type=settings.get('geo_field_type', None)) |
| 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_placeholder() routine to get any needed |
| # transformation SQL. |
| geom = geo_field.get_prep_value(settings['procedure_args'][name]) |
| params = geo_field.get_db_prep_lookup('contains', geom, connection=connection) |
| geom_placeholder = geo_field.get_placeholder(geom, connection) |
| |
| # Replacing the procedure format with that of any needed |
| # transformation SQL. |
| old_fmt = '%%(%s)s' % name |
| new_fmt = geom_placeholder % '%%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 backend.select: |
| self.query.custom_select[model_att] = backend.select |
| if connection.ops.oracle: |
| sel_fld.empty_strings_allowed = False |
| 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. |
| connection = connections[self.db] |
| geodetic = geo_field.geodetic(connection) |
| geography = geo_field.geography |
| |
| if geodetic: |
| dist_att = 'm' |
| else: |
| dist_att = Distance.unit_attname(geo_field.units_name(connection)) |
| |
| # Shortcut booleans for what distance function we're using and |
| # whether the geometry field is 3D. |
| distance = func == 'distance' |
| length = func == 'length' |
| perimeter = func == 'perimeter' |
| if not (distance or length or perimeter): |
| raise ValueError('Unknown distance function: %s' % func) |
| geom_3d = geo_field.dim == 3 |
| |
| # 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] |
| |
| # Getting the spatial backend operations. |
| backend = connection.ops |
| |
| # 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 (backend.postgis and geodetic and |
| (not geography) and length): |
| lookup_params.append('spheroid') |
| lookup_params = geo_field.get_prep_value(lookup_params) |
| params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection) |
| |
| # The `geom_args` flag is set to true if a geometry parameter was |
| # passed in. |
| geom_args = bool(geom) |
| |
| if backend.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, connection) |
| geodetic = unit_name in geo_field.geodetic_units |
| |
| if backend.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)' % (backend.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 backend.spatialite: |
| procedure_fmt += ', %s(%%%%s, %s)' % (backend.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 backend.spatialite: |
| procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text, |
| geom.srid, self.query.transformed_srid) |
| else: |
| procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid) |
| else: |
| # `transform()` was not used on this GeoQuerySet. |
| procedure_fmt = '%(geo_col)s,%(geom)s' |
| |
| if not geography and 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 backend.geography: |
| if not isinstance(geo_field, PointField): |
| raise ValueError('Spherical distance calculation only supported on PointFields.') |
| if not str(Geometry(buffer(params[0].ewkb)).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' : backend.distance_spheroid, 'spheroid' : params[1]}) |
| else: |
| procedure_args.update({'function' : backend.distance_sphere}) |
| elif length or perimeter: |
| procedure_fmt = '%(geo_col)s' |
| if not geography and geodetic and length: |
| # There's no `length_sphere`, and `length_spheroid` also |
| # works on 3D geometries. |
| procedure_fmt += ",'%(spheroid)s'" |
| procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]}) |
| elif geom_3d and backend.postgis: |
| # Use 3D variants of perimeter and length routines on PostGIS. |
| if perimeter: |
| procedure_args.update({'function' : backend.perimeter3d}) |
| elif length: |
| procedure_args.update({'function' : backend.length3d}) |
| |
| # 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'] = [backend.Adapter(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 connections[self.db].ops.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 connections[self.db].ops.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]) |
| compiler = self.query.get_compiler(self.db) |
| compiler.pre_sql_setup() |
| rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] |
| return compiler._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.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table) |
| else: |
| return self.query.get_compiler(self.db)._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 |