123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822 |
- import warnings
- from django.contrib.gis.db.models import aggregates
- from django.contrib.gis.db.models.fields import (
- GeometryField, LineStringField, PointField, get_srid_info,
- )
- from django.contrib.gis.db.models.lookups import GISLookup
- from django.contrib.gis.db.models.sql import (
- AreaField, DistanceField, GeomField, GMLField,
- )
- from django.contrib.gis.geometry.backend import Geometry
- from django.contrib.gis.measure import Area, Distance
- from django.db import connections
- from django.db.models.expressions import RawSQL
- from django.db.models.fields import Field
- from django.db.models.query import QuerySet
- from django.utils import six
- from django.utils.deprecation import (
- RemovedInDjango20Warning, RemovedInDjango110Warning,
- )
- class GeoQuerySet(QuerySet):
- "The Geographic QuerySet."
- # ### 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.
- """
- # Performing 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'))
- 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 analogous to a union operation, but much faster because
- boundaries are not dissolved.
- """
- warnings.warn(
- "The collect GeoQuerySet method is deprecated. Use the Collect() "
- "aggregate in an aggregate() or annotate() method.",
- RemovedInDjango110Warning, stacklevel=2
- )
- 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).
- """
- warnings.warn(
- "The extent GeoQuerySet method is deprecated. Use the Extent() "
- "aggregate in an aggregate() or annotate() method.",
- RemovedInDjango110Warning, stacklevel=2
- )
- 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).
- """
- warnings.warn(
- "The extent3d GeoQuerySet method is deprecated. Use the Extent3D() "
- "aggregate in an aggregate() or annotate() method.",
- RemovedInDjango110Warning, stacklevel=2
- )
- 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 geometry field in a `geojson`
- attribute on each element of the GeoQuerySet.
- The `crs` and `bbox` keywords may be set to True if the user 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+ and SpatiaLite 3.0+ '
- 'support GeoJSON serialization.')
- if not isinstance(precision, six.integer_types):
- raise TypeError('Precision keyword must be set with an integer.')
- options = 0
- if crs and bbox:
- options = 3
- elif bbox:
- options = 1
- elif crs:
- 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:
- s['procedure_fmt'] = '%(version)s,%(geo_col)s,%(precision)s'
- s['procedure_args'] = {'precision': precision, 'version': version}
- if backend.oracle:
- s['select_field'] = GMLField()
- 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.
- """
- warnings.warn(
- "The make_line GeoQuerySet method is deprecated. Use the MakeLine() "
- "aggregate in an aggregate() or annotate() method.",
- RemovedInDjango110Warning, stacklevel=2
- )
- 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,) + six.integer_types) 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, six.integer_types):
- 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, six.integer_types):
- raise TypeError('An integer SRID must be provided.')
- field_name = kwargs.get('field_name')
- self._spatial_setup('transform', field_name=field_name)
- self.query.add_context('transformed_srid', srid)
- 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.
- """
- warnings.warn(
- "The unionagg GeoQuerySet method is deprecated. Use the Union() "
- "aggregate in an aggregate() or annotate() method.",
- RemovedInDjango110Warning, stacklevel=2
- )
- 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._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 geo_field_type is not 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._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 geo_field_type is not 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.
- """
- warnings.warn(
- "The %s GeoQuerySet method is deprecated. See GeoDjango Functions "
- "documentation to find the expression-based replacement." % att,
- RemovedInDjango20Warning, stacklevel=2
- )
- # 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]
- # 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'))
- for k, v in six.iteritems(default_args):
- settings['procedure_args'].setdefault(k, v)
- else:
- geo_field = settings['geo_field']
- # The attribute to attach to the model.
- if not isinstance(model_att, six.string_types):
- 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, None, 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'):
- select_field = settings['select_field']
- if connection.ops.oracle:
- select_field.empty_strings_allowed = False
- else:
- select_field = Field()
- # Finally, setting the extra selection attribute with
- # the format string expanded with the stored procedure
- # arguments.
- self.query.add_annotation(
- RawSQL(fmt % settings['procedure_args'], settings['select_params'], select_field),
- model_att)
- return self
- 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'))
- # 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.
- srid = self.query.get_context('transformed_srid')
- if srid:
- u, unit_name, s = get_srid_info(srid, connection)
- geodetic = unit_name.lower() in geo_field.geodetic_units
- if geodetic and not connection.features.supports_distance_geodetic:
- raise ValueError(
- 'This database does not support linear distance '
- 'calculations on geodetic coordinate systems.'
- )
- if distance:
- if 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, srid)
- if geom.srid is None or geom.srid == 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, 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, srid))
- else:
- procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, 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(six.memoryview(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 connection.features.supports_3d_functions:
- # Use 3D variants of perimeter and length routines on supported backends.
- 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.
- """
- compiler = self.query.get_compiler(self.db)
- opts = self.model._meta
- if geo_field not 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).
- # Note: the operation really is defined as "must add select related!"
- self.query.add_select_related([field_name])
- # Call pre_sql_setup() so that compiler.select gets populated.
- compiler.pre_sql_setup()
- for col, _, _ in compiler.select:
- if col.output_field == geo_field:
- return col.as_sql(compiler, compiler.connection)[0]
- raise ValueError("%r not in compiler's related_select_cols" % geo_field)
- elif geo_field not 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.
- parent_model = geo_field.model._meta.concrete_model
- return self._field_column(compiler, geo_field, parent_model._meta.db_table)
- else:
- return self._field_column(compiler, geo_field)
- # Private API utilities, subject to change.
- def _geo_field(self, field_name=None):
- """
- Returns the first Geometry field encountered or the one specified via
- the `field_name` keyword. The `field_name` may be a string specifying
- the geometry field on this GeoQuerySet's model, or a lookup string
- to a geometry field via a ForeignKey relation.
- """
- if field_name is None:
- # Incrementing until the first geographic field is found.
- for field in self.model._meta.fields:
- if isinstance(field, GeometryField):
- return field
- return False
- else:
- # Otherwise, check by the given field name -- which may be
- # a lookup to a _related_ geographic field.
- return GISLookup._check_geo_field(self.model._meta, field_name)
- def _field_column(self, compiler, field, table_alias=None, column=None):
- """
- Helper function that returns the database column for the given field.
- The table and column are returned (quoted) in the proper format, e.g.,
- `"geoapp_city"."point"`. If `table_alias` is not specified, the
- database table associated with the model of this `GeoQuerySet` will be
- used. If `column` is specified, it will be used instead of the value
- in `field.column`.
- """
- if table_alias is None:
- table_alias = compiler.query.get_meta().db_table
- return "%s.%s" % (compiler.quote_name_unless_alias(table_alias),
- compiler.connection.ops.quote_name(column or field.column))
|