Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge 9c19916 into d5a0acc

  • Loading branch information...
commit 9f89f0c9e14cb7fbbb877bc6392aa11a867cc49f 2 parents d5a0acc + 9c19916
@claudep claudep authored
Showing with 2,084 additions and 92 deletions.
  1. +16 −6 django/contrib/gis/db/backends/base/features.py
  2. +23 −2 django/contrib/gis/db/backends/base/operations.py
  3. +2 −0  django/contrib/gis/db/backends/mysql/features.py
  4. +23 −1 django/contrib/gis/db/backends/mysql/operations.py
  5. +6 −2 django/contrib/gis/db/backends/postgis/adapter.py
  6. +7 −0 django/contrib/gis/db/backends/postgis/operations.py
  7. +21 −0 django/contrib/gis/db/backends/spatialite/operations.py
  8. +4 −1 django/contrib/gis/db/models/fields.py
  9. +401 −0 django/contrib/gis/db/models/functions.py
  10. +11 −0 django/contrib/gis/db/models/manager.py
  11. +8 −1 django/contrib/gis/db/models/query.py
  12. +7 −4 django/contrib/gis/sitemaps/views.py
  13. +2 −0  docs/internals/deprecation.txt
  14. +53 −54 docs/ref/contrib/gis/db-api.txt
  15. +437 −0 docs/ref/contrib/gis/functions.txt
  16. +132 −1 docs/ref/contrib/gis/geoquerysets.txt
  17. +1 −0  docs/ref/contrib/gis/index.txt
  18. +8 −4 docs/ref/contrib/gis/tutorial.txt
  19. +15 −0 docs/releases/1.9.txt
  20. +1 −0  docs/spelling_wordlist
  21. +288 −1 tests/gis_tests/distapp/tests.py
  22. +116 −13 tests/gis_tests/geo3d/tests.py
  23. +461 −0 tests/gis_tests/geoapp/test_functions.py
  24. +4 −1 tests/gis_tests/geoapp/tests.py
  25. +32 −1 tests/gis_tests/geogapp/tests.py
  26. +5 −0 tests/runtests.py
View
22 django/contrib/gis/db/backends/base/features.py
@@ -1,3 +1,4 @@
+import re
from functools import partial
from django.contrib.gis.db.models import aggregates
@@ -25,8 +26,9 @@ class BaseSpatialFeatures(object):
supports_real_shape_operations = True
# Can geometry fields be null?
supports_null_geometries = True
- # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems?
+ # Can the `distance`/`length` functions be applied on geodetic coordinate systems?
supports_distance_geodetic = True
+ supports_length_geodetic = True
# Is the database able to count vertices on polygons (with `num_points`)?
supports_num_points_poly = True
@@ -59,11 +61,11 @@ def supports_relate_lookup(self):
# `has_<name>_method` (defined in __init__) which accesses connection.ops
# to determine GIS method availability.
geoqueryset_methods = (
- 'area', 'centroid', 'difference', 'distance', 'distance_spheroid',
- 'envelope', 'force_rhr', 'geohash', 'gml', 'intersection', 'kml',
- 'length', 'num_geom', 'perimeter', 'point_on_surface', 'reverse',
- 'scale', 'snap_to_grid', 'svg', 'sym_difference', 'transform',
- 'translate', 'union', 'unionagg',
+ 'area', 'bounding_circle', 'centroid', 'difference', 'distance',
+ 'distance_spheroid', 'envelope', 'force_rhr', 'geohash', 'gml',
+ 'intersection', 'kml', 'length', 'mem_size', 'num_geom', 'num_points',
+ 'perimeter', 'point_on_surface', 'reverse', 'scale', 'snap_to_grid',
+ 'svg', 'sym_difference', 'transform', 'translate', 'union', 'unionagg',
)
# Specifies whether the Collect and Extent aggregates are supported by the database
@@ -86,5 +88,13 @@ def __init__(self, *args):
setattr(self.__class__, 'has_%s_method' % method,
property(partial(BaseSpatialFeatures.has_ops_method, method=method)))
+ def __getattr__(self, name):
+ m = re.match(r'has_(\w*)_function$', name)
+ if m:
+ func_name = m.group(1)
+ if func_name not in self.connection.ops.unsupported_functions:
+ return True
+ return False
+
def has_ops_method(self, method):
return getattr(self.connection.ops, method, False)
View
25 django/contrib/gis/db/backends/base/operations.py
@@ -22,6 +22,7 @@ class BaseSpatialOperations(object):
geometry = False
area = False
+ bounding_circle = False
centroid = False
difference = False
distance = False
@@ -30,7 +31,6 @@ class BaseSpatialOperations(object):
envelope = False
force_rhr = False
mem_size = False
- bounding_circle = False
num_geom = False
num_points = False
perimeter = False
@@ -48,6 +48,22 @@ class BaseSpatialOperations(object):
# Aggregates
disallowed_aggregates = ()
+ geom_func_prefix = ''
+
+ # Mapping between Django function names and backend names, when names do not
+ # match; used in spatial_function_name().
+ function_names = {}
+
+ # Blacklist/set of known unsupported functions of the backend
+ unsupported_functions = {
+ 'Area', 'AsGeoHash', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG',
+ 'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope',
+ 'ForceRHR', 'Intersection', 'Length', 'MemSize', 'NumGeometries',
+ 'NumPoints', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale',
+ 'SnapToGrid', 'SymDifference', 'Transform', 'Translate',
+ 'Union',
+ }
+
# Serialization
geohash = False
geojson = False
@@ -108,9 +124,14 @@ def check_expression_support(self, expression):
def spatial_aggregate_name(self, agg_name):
raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
+ def spatial_function_name(self, func_name):
+ if func_name in self.unsupported_functions:
+ raise NotImplementedError("This backend doesn't support the %s function." % func_name)
+ return self.function_names.get(func_name, self.geom_func_prefix + func_name)
+
# Routines for getting the OGC-compliant models.
def geometry_columns(self):
- raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method')
+ raise NotImplementedError('Subclasses of BaseSpatialOperations must provide a geometry_columns() method.')
def spatial_ref_sys(self):
raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method')
View
2  django/contrib/gis/db/backends/mysql/features.py
@@ -6,6 +6,8 @@
class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
has_spatialrefsys_table = False
supports_add_srs_entry = False
+ supports_distance_geodetic = False
+ supports_length_geodetic = False
supports_distances_lookups = False
supports_transform = False
supports_real_shape_operations = False
View
24 django/contrib/gis/db/backends/mysql/operations.py
@@ -4,6 +4,7 @@
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.db.models import aggregates
from django.db.backends.mysql.operations import DatabaseOperations
+from django.utils.functional import cached_property
class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
@@ -32,7 +33,28 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
'within': SpatialOperator(func='MBRWithin'),
}
- disallowed_aggregates = (aggregates.Collect, aggregates.Extent, aggregates.Extent3D, aggregates.MakeLine, aggregates.Union)
+ function_names = {
+ 'Distance': 'ST_Distance',
+ 'Length': 'GLength',
+ 'Union': 'ST_Union',
+ }
+
+ disallowed_aggregates = (
+ aggregates.Collect, aggregates.Extent, aggregates.Extent3D,
+ aggregates.MakeLine, aggregates.Union,
+ )
+
+ @cached_property
+ def unsupported_functions(self):
+ unsupported = {
+ 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle',
+ 'Difference', 'ForceRHR', 'GeoHash', 'Intersection', 'MemSize',
+ 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid',
+ 'SymDifference', 'Transform', 'Translate',
+ }
+ if self.connection.mysql_version < (5, 6, 1):
+ unsupported.update({'Distance', 'Union'})
+ return unsupported
def geo_db_type(self, f):
return f.geom_type
View
8 django/contrib/gis/db/backends/postgis/adapter.py
@@ -8,12 +8,13 @@
class PostGISAdapter(object):
- def __init__(self, geom):
+ def __init__(self, geom, geography=False):
"Initializes on the geometry."
# Getting the WKB (in string form, to allow easy pickling of
# the adaptor) and the SRID from the geometry.
self.ewkb = bytes(geom.ewkb)
self.srid = geom.srid
+ self.geography = geography
self._adapter = Binary(self.ewkb)
def __conform__(self, proto):
@@ -44,4 +45,7 @@ def prepare(self, conn):
def getquoted(self):
"Returns a properly quoted string for use in PostgreSQL/PostGIS."
# psycopg will figure out whether to use E'\\000' or '\000'
- return str('ST_GeomFromEWKB(%s)' % self._adapter.getquoted().decode())
+ return str('%s(%s)' % (
+ 'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB',
+ self._adapter.getquoted().decode())
+ )
View
7 django/contrib/gis/db/backends/postgis/operations.py
@@ -88,6 +88,13 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
'distance_lte': PostGISDistanceOperator(func='ST_Distance', op='<=', geography=True),
}
+ unsupported_functions = set()
+ function_names = {
+ 'BoundingCircle': 'ST_MinimumBoundingCircle',
+ 'MemSize': 'ST_Mem_Size',
+ 'NumPoints': 'ST_NPoints',
+ }
+
def __init__(self, connection):
super(PostGISOperations, self).__init__(connection)
View
21 django/contrib/gis/db/backends/spatialite/operations.py
@@ -1,3 +1,9 @@
+"""
+SQL functions reference lists:
+http://www.gaia-gis.it/spatialite-2.4.0/spatialite-sql-2.4.html
+http://www.gaia-gis.it/spatialite-3.0.0-BETA/spatialite-sql-3.0.0.html
+http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.1.html
+"""
import re
import sys
@@ -74,6 +80,21 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
'distance_lte': SpatialOperator(func='Distance', op='<='),
}
+ function_names = {
+ 'Length': 'ST_Length',
+ 'Reverse': 'ST_Reverse',
+ 'Scale': 'ScaleCoords',
+ 'Translate': 'ST_Translate',
+ 'Union': 'ST_Union',
+ }
+
+ @cached_property
+ def unsupported_functions(self):
+ unsupported = {'BoundingCircle', 'ForceRHR', 'GeoHash', 'MemSize'}
+ if self.spatial_version < (4, 0, 0):
+ unsupported.add('Reverse')
+ return unsupported
+
@cached_property
def spatial_version(self):
"""Determine the version of the SpatiaLite library."""
View
5 django/contrib/gis/db/models/fields.py
@@ -169,7 +169,10 @@ def geodetic(self, connection):
Returns true if this field's SRID corresponds with a coordinate
system that uses non-projected units (e.g., latitude/longitude).
"""
- return self.units_name(connection).lower() in self.geodetic_units
+ units_name = self.units_name(connection)
+ # Some backends like MySQL cannot determine units name. In that case,
+ # test if srid is 4326 (WGS84), even if this is over-simplification.
+ return units_name.lower() in self.geodetic_units if units_name else self.srid == 4326
def get_distance(self, value, lookup_type, connection):
"""
View
401 django/contrib/gis/db/models/functions.py
@@ -0,0 +1,401 @@
+from decimal import Decimal
+
+from django.contrib.gis.db.models.fields import GeometryField
+from django.contrib.gis.db.models.sql import AreaField
+from django.contrib.gis.geos.geometry import GEOSGeometry
+from django.contrib.gis.measure import (
+ Area as AreaMeasure, Distance as DistanceMeasure,
+)
+from django.core.exceptions import FieldError
+from django.db.models import FloatField, IntegerField, TextField
+from django.db.models.expressions import Func, Value
+from django.utils import six
+
+NUMERIC_TYPES = six.integer_types + (float, Decimal)
+
+
+class GeoFunc(Func):
+ function = None
+ output_field_class = None
+ geom_param_pos = 0
+
+ def __init__(self, *expressions, **extra):
+ if 'output_field' not in extra and self.output_field_class:
+ extra['output_field'] = self.output_field_class()
+ super(GeoFunc, self).__init__(*expressions, **extra)
+
+ @property
+ def name(self):
+ return self.__class__.__name__
+
+ @property
+ def srid(self):
+ expr = self.source_expressions[self.geom_param_pos]
+ if hasattr(expr, 'srid'):
+ return expr.srid
+ try:
+ return expr.field.srid
+ except (AttributeError, FieldError):
+ return None
+
+ def as_sql(self, compiler, connection):
+ if self.function is None:
+ self.function = connection.ops.spatial_function_name(self.name)
+ return super(GeoFunc, self).as_sql(compiler, connection)
+
+ def resolve_expression(self, *args, **kwargs):
+ res = super(GeoFunc, self).resolve_expression(*args, **kwargs)
+ base_srid = res.srid
+ if not base_srid:
+ raise TypeError("Geometry functions can only operate on geometric content.")
+
+ for pos, expr in enumerate(res.source_expressions[1:], start=1):
+ if isinstance(expr, GeomValue) and expr.srid != base_srid:
+ # Automatic SRID conversion so objects are comparable
+ res.source_expressions[pos] = Transform(expr, base_srid).resolve_expression(*args, **kwargs)
+ return res
+
+ def _handle_param(self, value, param_name='', check_types=None):
+ if not hasattr(value, 'resolve_expression'):
+ if check_types and not isinstance(value, check_types):
+ raise TypeError(
+ "The %s parameter has the wrong type: should be %s." % (
+ param_name, str(check_types))
+ )
+ return value
+
+
+class GeomValue(Value):
+ geography = False
+
+ @property
+ def srid(self):
+ return self.value.srid
+
+ def as_sql(self, compiler, connection):
+ if self.geography:
+ self.value = connection.ops.Adapter(self.value, geography=self.geography)
+ else:
+ self.value = connection.ops.Adapter(self.value)
+ return super(GeomValue, self).as_sql(compiler, connection)
+
+ def as_mysql(self, compiler, connection):
+ return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)]
+
+ def as_sqlite(self, compiler, connection):
+ return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)]
+
+
+class GeoFuncWithGeoParam(GeoFunc):
+ def __init__(self, expression, geom, *expressions, **extra):
+ if not hasattr(geom, 'srid'):
+ # Try to interpret it as a geometry input
+ try:
+ geom = GEOSGeometry(geom)
+ except Exception:
+ raise ValueError("This function requires a geometric parameter.")
+ if not geom.srid:
+ raise ValueError("Please provide a geometry attribute with a defined SRID.")
+ geom = GeomValue(geom)
+ super(GeoFuncWithGeoParam, self).__init__(expression, geom, *expressions, **extra)
+
+
+class Area(GeoFunc):
+ def as_sql(self, compiler, connection):
+ if connection.ops.oracle:
+ self.output_field = AreaField('sq_m') # Oracle returns area in units of meters.
+ else:
+ if connection.ops.geography:
+ # Geography fields support area calculation, returns square meters.
+ self.output_field = AreaField('sq_m')
+ elif not self.output_field.geodetic(connection):
+ # Getting the area units of the geographic field.
+ units = self.output_field.units_name(connection)
+ if units:
+ self.output_field = AreaField(
+ AreaMeasure.unit_attname(self.output_field.units_name(connection)))
+ else:
+ self.output_field = FloatField()
+ else:
+ # TODO: Do we want to support raw number areas for geodetic fields?
+ raise NotImplementedError('Area on geodetic coordinate systems not supported.')
+ return super(Area, self).as_sql(compiler, connection)
+
+
+class AsGeoJSON(GeoFunc):
+ output_field_class = TextField
+
+ def __init__(self, expression, bbox=False, crs=False, precision=8, **extra):
+ expressions = [expression]
+ if precision is not None:
+ expressions.append(self._handle_param(precision, 'precision', six.integer_types))
+ options = 0
+ if crs and bbox:
+ options = 3
+ elif bbox:
+ options = 1
+ elif crs:
+ options = 2
+ if options:
+ expressions.append(options)
+ super(AsGeoJSON, self).__init__(*expressions, **extra)
+
+
+class AsGML(GeoFunc):
+ geom_param_pos = 1
+ output_field_class = TextField
+
+ def __init__(self, expression, version=2, precision=8, **extra):
+ expressions = [version, expression]
+ if precision is not None:
+ expressions.append(self._handle_param(precision, 'precision', six.integer_types))
+ super(AsGML, self).__init__(*expressions, **extra)
+
+
+class AsKML(AsGML):
+ def as_sqlite(self, compiler, connection):
+ # No version parameter
+ self.source_expressions.pop(0)
+ return super(AsKML, self).as_sql(compiler, connection)
+
+
+class AsSVG(GeoFunc):
+ output_field_class = TextField
+
+ def __init__(self, expression, relative=False, precision=8, **extra):
+ relative = relative if hasattr(relative, 'resolve_expression') else int(relative)
+ expressions = [
+ expression,
+ relative,
+ self._handle_param(precision, 'precision', six.integer_types),
+ ]
+ super(AsSVG, self).__init__(*expressions, **extra)
+
+
+class BoundingCircle(GeoFunc):
+ def __init__(self, expression, num_seg=48, **extra):
+ super(BoundingCircle, self).__init__(*[expression, num_seg], **extra)
+
+
+class Centroid(GeoFunc):
+ pass
+
+
+class Difference(GeoFuncWithGeoParam):
+ pass
+
+
+class DistanceResultMixin(object):
+ def convert_value(self, value, expression, connection, context):
+ if value is None:
+ return None
+ geo_field = GeometryField(srid=self.srid) # Fake field to get SRID info
+ if geo_field.geodetic(connection):
+ dist_att = 'm'
+ else:
+ units = geo_field.units_name(connection)
+ if units:
+ dist_att = DistanceMeasure.unit_attname(units)
+ else:
+ dist_att = None
+ if dist_att:
+ return DistanceMeasure(**{dist_att: value})
+ return value
+
+
+class Distance(DistanceResultMixin, GeoFuncWithGeoParam):
+ output_field_class = FloatField
+ spheroid = None
+
+ def __init__(self, expr1, expr2, spheroid=None, **extra):
+ expressions = [expr1, expr2]
+ if spheroid is not None:
+ self.spheroid = spheroid
+ expressions += (self._handle_param(spheroid, 'spheroid', bool),)
+ super(Distance, self).__init__(*expressions, **extra)
+
+ def as_postgresql(self, compiler, connection):
+ geo_field = GeometryField(srid=self.srid) # Fake field to get SRID info
+ src_field = self.get_source_fields()[0]
+ geography = src_field.geography and self.srid == 4326
+ if geography:
+ # Set parameters as geography if base field is geography
+ for pos, expr in enumerate(
+ self.source_expressions[self.geom_param_pos + 1:], start=self.geom_param_pos + 1):
+ if isinstance(expr, GeomValue):
+ expr.geography = True
+ elif geo_field.geodetic(connection):
+ # Geometry fields with geodetic (lon/lat) coordinates need special distance functions
+ if self.spheroid:
+ self.function = 'ST_Distance_Spheroid' # More accurate, resource intensive
+ # Replace boolean param by the real spheroid of the base field
+ self.source_expressions[2] = Value(geo_field._spheroid)
+ else:
+ self.function = 'ST_Distance_Sphere'
+ return super(Distance, self).as_sql(compiler, connection)
+
+
+class Envelope(GeoFunc):
+ pass
+
+
+class ForceRHR(GeoFunc):
+ pass
+
+
+class GeoHash(GeoFunc):
+ output_field_class = TextField
+
+ def __init__(self, expression, precision=None, **extra):
+ expressions = [expression]
+ if precision is not None:
+ expressions.append(self._handle_param(precision, 'precision', six.integer_types))
+ super(GeoHash, self).__init__(*expressions, **extra)
+
+
+class Intersection(GeoFuncWithGeoParam):
+ pass
+
+
+class Length(DistanceResultMixin, GeoFunc):
+ output_field_class = FloatField
+
+ def __init__(self, expr1, spheroid=True, **extra):
+ self.spheroid = spheroid
+ super(Length, self).__init__(expr1, **extra)
+
+ def as_sql(self, compiler, connection):
+ geo_field = GeometryField(srid=self.srid) # Fake field to get SRID info
+ if geo_field.geodetic(connection) and not connection.features.supports_length_geodetic:
+ raise NotImplementedError("This backend doesn't support Length on geodetic fields")
+ return super(Length, self).as_sql(compiler, connection)
+
+ def as_postgresql(self, compiler, connection):
+ geo_field = GeometryField(srid=self.srid) # Fake field to get SRID info
+ src_field = self.get_source_fields()[0]
+ geography = src_field.geography and self.srid == 4326
+ if geography:
+ self.source_expressions.append(Value(self.spheroid))
+ elif geo_field.geodetic(connection):
+ # Geometry fields with geodetic (lon/lat) coordinates need length_spheroid
+ self.function = 'ST_Length_Spheroid'
+ self.source_expressions.append(Value(geo_field._spheroid))
+ else:
+ dim = min(f.dim for f in self.get_source_fields() if f)
+ if dim > 2:
+ self.function = connection.ops.length3d
+ return super(Length, self).as_sql(compiler, connection)
+
+ def as_sqlite(self, compiler, connection):
+ geo_field = GeometryField(srid=self.srid)
+ if geo_field.geodetic(connection):
+ if self.spheroid:
+ self.function = 'GeodesicLength'
+ else:
+ self.function = 'GreatCircleLength'
+ return super(Length, self).as_sql(compiler, connection)
+
+
+class MemSize(GeoFunc):
+ output_field_class = IntegerField
+
+
+class NumGeometries(GeoFunc):
+ output_field_class = IntegerField
+
+
+class NumPoints(GeoFunc):
+ output_field_class = IntegerField
+
+ def as_sqlite(self, compiler, connection):
+ if self.source_expressions[self.geom_param_pos].output_field.geom_type != 'LINESTRING':
+ raise TypeError("Spatialite NumPoints can only operate on LineString content")
+ return super(NumPoints, self).as_sql(compiler, connection)
+
+
+class Perimeter(DistanceResultMixin, GeoFunc):
+ output_field_class = FloatField
+
+ def as_postgresql(self, compiler, connection):
+ dim = min(f.dim for f in self.get_source_fields())
+ if dim > 2:
+ self.function = connection.ops.perimeter3d
+ return super(Perimeter, self).as_sql(compiler, connection)
+
+
+class PointOnSurface(GeoFunc):
+ pass
+
+
+class Reverse(GeoFunc):
+ pass
+
+
+class Scale(GeoFunc):
+ def __init__(self, expression, x, y, z=0.0, **extra):
+ expressions = [
+ expression,
+ self._handle_param(x, 'x', NUMERIC_TYPES),
+ self._handle_param(y, 'y', NUMERIC_TYPES),
+ ]
+ if z != 0.0:
+ expressions.append(self._handle_param(z, 'z', NUMERIC_TYPES))
+ super(Scale, self).__init__(*expressions, **extra)
+
+
+class SnapToGrid(GeoFunc):
+ def __init__(self, expression, *args, **extra):
+ nargs = len(args)
+ expressions = [expression]
+ if nargs in (1, 2):
+ expressions.extend(
+ [self._handle_param(arg, '', NUMERIC_TYPES) for arg in args]
+ )
+ elif nargs == 4:
+ # Reverse origin and size param ordering
+ expressions.extend(
+ [self._handle_param(arg, '', NUMERIC_TYPES) for arg in args[2:]]
+ )
+ expressions.extend(
+ [self._handle_param(arg, '', NUMERIC_TYPES) for arg in args[0:2]]
+ )
+ else:
+ raise ValueError('Must provide 1, 2, or 4 arguments to `SnapToGrid`.')
+ super(SnapToGrid, self).__init__(*expressions, **extra)
+
+
+class SymDifference(GeoFuncWithGeoParam):
+ pass
+
+
+class Transform(GeoFunc):
+ def __init__(self, expression, srid, **extra):
+ expressions = [
+ expression,
+ self._handle_param(srid, 'srid', six.integer_types),
+ ]
+ super(Transform, self).__init__(*expressions, **extra)
+
+ @property
+ def srid(self):
+ # Make srid the resulting srid of the transformation
+ return self.source_expressions[self.geom_param_pos + 1].value
+
+ def convert_value(self, value, expression, connection, context):
+ value = super(Transform, self).convert_value(value, expression, connection, context)
+ if not connection.ops.postgis and not value.srid:
+ # Some backends do not set the srid on the returning geometry
+ value.srid = self.srid
+ return value
+
+
+class Translate(Scale):
+ def as_sqlite(self, compiler, connection):
+ # Always provide the z parameter
+ if len(self.source_expressions) < 4:
+ self.source_expressions.append(Value(0))
+ return super(Translate, self).as_sql(compiler, connection)
+
+
+class Union(GeoFuncWithGeoParam):
+ pass
View
11 django/contrib/gis/db/models/manager.py
@@ -1,5 +1,8 @@
+import warnings
+
from django.contrib.gis.db.models.query import GeoQuerySet
from django.db.models.manager import Manager
+from django.utils.deprecation import RemovedInDjango21Warning
class GeoManager(Manager.from_queryset(GeoQuerySet)):
@@ -9,3 +12,11 @@ class GeoManager(Manager.from_queryset(GeoQuerySet)):
# so that geometry columns on Oracle and MySQL are selected
# properly.
use_for_related_fields = True
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "The GeoManager class is deprecated. Simply use a normal manager "
+ "once you have replaced all calls to GeoQuerySet methods by annotations.",
+ RemovedInDjango21Warning, stacklevel=2
+ )
+ super(GeoManager, self).__init__(*args, **kwargs)
View
9 django/contrib/gis/db/models/query.py
@@ -15,7 +15,9 @@
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
+from django.utils.deprecation import (
+ RemovedInDjango20Warning, RemovedInDjango21Warning,
+)
class GeoQuerySet(QuerySet):
@@ -513,6 +515,11 @@ def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
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,
+ RemovedInDjango21Warning, stacklevel=2
+ )
# Default settings.
settings.setdefault('desc', None)
settings.setdefault('geom_args', ())
View
11 django/contrib/gis/sitemaps/views.py
@@ -2,6 +2,7 @@
from django.apps import apps
from django.contrib.gis.db.models.fields import GeometryField
+from django.contrib.gis.db.models.functions import AsKML, Transform
from django.contrib.gis.shortcuts import render_to_kml, render_to_kmz
from django.core.exceptions import FieldDoesNotExist
from django.db import DEFAULT_DB_ALIAS, connections
@@ -31,15 +32,17 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB
connection = connections[using]
- if connection.features.has_kml_method:
+ if connection.features.has_AsKML_function:
# Database will take care of transformation.
- placemarks = klass._default_manager.using(using).kml(field_name=field_name)
+ placemarks = klass._default_manager.using(using).annotate(kml=AsKML(field_name))
else:
# If the database offers no KML method, we use the `kml`
# attribute of the lazy geometry instead.
placemarks = []
- if connection.features.has_transform_method:
- qs = klass._default_manager.using(using).transform(4326, field_name=field_name)
+ if connection.features.has_Transform_function:
+ qs = klass._default_manager.using(using).annotate(
+ **{'%s_4326' % field_name: Transform(field_name, 4326)})
+ field_name += '_4326'
else:
qs = klass._default_manager.using(using).all()
for mod in qs:
View
2  docs/internals/deprecation.txt
@@ -35,6 +35,8 @@ details on these changes.
* The ``django.contrib.auth.tests.utils.skipIfCustomUser()`` decorator will be
removed.
+* The ``GeoManager`` and ``GeoQuerySet`` classes will be removed.
+
.. _deprecation-removed-in-2.0:
2.0
View
107 docs/ref/contrib/gis/db-api.txt
@@ -115,13 +115,6 @@ GeoJSON , WKT, or HEXEWKB).
A complete reference can be found in the :ref:`spatial lookup reference
<spatial-lookups>`.
-.. note::
-
- GeoDjango constructs spatial SQL with the :class:`GeoQuerySet`, a
- subclass of :class:`~django.db.models.query.QuerySet`. The
- :class:`GeoManager` instance attached to your model is what
- enables use of :class:`GeoQuerySet`.
-
.. _distance-queries:
Distance Queries
@@ -152,7 +145,7 @@ The following distance lookups are available:
.. note::
For *measuring*, rather than querying on distances, use the
- :meth:`GeoQuerySet.distance` method.
+ :class:`~django.contrib.gis.db.models.functions.Distance` function.
Distance lookups take a tuple parameter comprising:
@@ -255,43 +248,47 @@ Lookup Type PostGIS Oracle MySQL [#]_ SpatiaLite
:lookup:`strictly_below` X
================================= ========= ======== ============ ==========
-.. _geoqueryset-method-compatibility:
-
-``GeoQuerySet`` Methods
------------------------
-The following table provides a summary of what :class:`GeoQuerySet` methods
-are available on each spatial backend. Please note that MySQL does not
-support any of these methods, and is thus excluded from the table.
-
-==================================== ======= ====== ==========
-Method PostGIS Oracle SpatiaLite
-==================================== ======= ====== ==========
-:meth:`GeoQuerySet.area` X X X
-:meth:`GeoQuerySet.centroid` X X X
-:meth:`GeoQuerySet.difference` X X X
-:meth:`GeoQuerySet.distance` X X X
-:meth:`GeoQuerySet.envelope` X X
-:meth:`GeoQuerySet.force_rhr` X
-:meth:`GeoQuerySet.geohash` X
-:meth:`GeoQuerySet.geojson` X X
-:meth:`GeoQuerySet.gml` X X X
-:meth:`GeoQuerySet.intersection` X X X
-:meth:`GeoQuerySet.kml` X X
-:meth:`GeoQuerySet.length` X X X
-:meth:`GeoQuerySet.mem_size` X
-:meth:`GeoQuerySet.num_geom` X X X
-:meth:`GeoQuerySet.num_points` X X X
-:meth:`GeoQuerySet.perimeter` X X
-:meth:`GeoQuerySet.point_on_surface` X X X
-:meth:`GeoQuerySet.reverse_geom` X X
-:meth:`GeoQuerySet.scale` X X
-:meth:`GeoQuerySet.snap_to_grid` X
-:meth:`GeoQuerySet.svg` X X
-:meth:`GeoQuerySet.sym_difference` X X X
-:meth:`GeoQuerySet.transform` X X X
-:meth:`GeoQuerySet.translate` X X
-:meth:`GeoQuerySet.union` X X X
-==================================== ======= ====== ==========
+.. _database-functions-compatibility:
+
+Database functions
+------------------
+
+.. module:: django.contrib.gis.db.models.functions
+ :synopsis: GeoDjango's database functions.
+
+The following table provides a summary of what geography-specific database
+functions are available on each spatial backend.
+
+==================================== ======= ====== ===== ==========
+Method PostGIS Oracle MySQL SpatiaLite
+==================================== ======= ====== ===== ==========
+:class:`Area` X X X X
+:class:`AsGeoJSON` X X
+:class:`AsGML` X X X
+:class:`AsKML` X X
+:class:`AsSVG` X X
+:class:`BoundingCircle` X
+:class:`Centroid` X X X
+:class:`Difference` X X X
+:class:`Distance` X X X X
+:class:`Envelope` X X
+:class:`ForceRHR` X
+:class:`GeoHash` X
+:class:`Intersection` X X X
+:class:`Length` X X X X
+:class:`MemSize` X
+:class:`NumGeometries` X X X
+:class:`NumPoints` X X X
+:class:`Perimeter` X X X
+:class:`PointOnSurface` X X X
+:class:`Reverse` X X X (≥ 4.0)
+:class:`Scale` X X
+:class:`SnapToGrid` X X
+:class:`SymDifference` X X X
+:class:`Transform` X X X
+:class:`Translate` X X
+:class:`Union` X X X
+==================================== ======= ====== ===== ==========
Aggregate Functions
-------------------
@@ -300,15 +297,17 @@ The following table provides a summary of what GIS-specific aggregate functions
are available on each spatial backend. Please note that MySQL does not
support any of these aggregates, and is thus excluded from the table.
-==================================== ======= ====== ==========
-Aggregate PostGIS Oracle SpatiaLite
-==================================== ======= ====== ==========
-:class:`Collect` X (from v3.0)
-:class:`Extent` X X (from v3.0)
-:class:`Extent3D` X
-:class:`MakeLine` X
-:class:`Union` X X X
-==================================== ======= ====== ==========
+.. currentmodule:: django.contrib.gis.db.models
+
+======================= ======= ====== ==========
+Aggregate PostGIS Oracle SpatiaLite
+======================= ======= ====== ==========
+:class:`Collect` X (from v3.0)
+:class:`Extent` X X (from v3.0)
+:class:`Extent3D` X
+:class:`MakeLine` X
+:class:`Union` X X X
+======================= ======= ====== ==========
.. rubric:: Footnotes
.. [#fnwkt] *See* Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification For SQL <http://www.opengis.org/docs/99-049.pdf>`_, Document 99-049 (May 5, 1999), at Ch. 3.2.5, p. 3-11 (SQL Textual Representation of Geometry).
View
437 docs/ref/contrib/gis/functions.txt
@@ -0,0 +1,437 @@
+=============================
+Geographic Database Functions
+=============================
+
+.. module:: django.contrib.gis.db.models.functions
+ :synopsis: Geographic Database Functions
+
+.. versionadded:: 1.9
+
+The functions documented on this page allow users to access geographic database
+functions to be used in annotations, aggregations, or filters in Django.
+
+Example::
+
+ >>> from django.contrib.gis.db.models.functions import Length
+ >>> Track.objects.annotate(length=Length('line')).filter(length__gt=100)
+
+Not all backends support all functions, so refer to the documentation of each
+function to see if your database backend supports the function you want to use.
+If you call a geographic function on a backend that doesn't support it, you'll
+get a ``NotImplementedError`` exception.
+
+Function's summary:
+
+================== ======================= ====================== =================== ================== =====================
+Measurement Relationships Operations Editors Output format Miscellaneous
+================== ======================= ====================== =================== ================== =====================
+:class:`Area` :class:`BoundingCircle` :class:`Difference` :class:`ForceRHR` :class:`AsGeoJSON` :class:`MemSize`
+:class:`Distance` :class:`Centroid` :class:`Intersection` :class:`Reverse` :class:`AsGML` :class:`NumGeometries`
+:class:`Length` :class:`Envelope` :class:`SymDifference` :class:`Scale` :class:`AsKML` :class:`NumPoints`
+:class:`Perimeter` :class:`PointOnSurface` :class:`Union` :class:`SnapToGrid` :class:`AsSVG`
+
+ :class:`Transform` :class:`GeoHash`
+
+ :class:`Translate`
+================== ======================= ====================== =================== ================== =====================
+
+Area
+----
+
+.. class:: Area(expression, **extra)
+
+*Availability*: MySQL, Oracle, PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns the area of the
+field as an :class:`~django.contrib.gis.measure.Area` measure. On MySQL, a raw
+float value is returned, as it's not possible to automatically determine the
+unit of the field.
+
+AsGeoJSON
+---------
+
+.. class:: AsGeoJSON(expression, bbox=False, crs=False, precision=8, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a `GeoJSON
+<http://geojson.org/>`_ representation of the geometry.
+
+Example::
+
+ >>> City.objects.annotate(json=AsGeoJSON('point')).get(name='Chicago').json
+ {"type":"Point","coordinates":[-87.65018,41.85039]}
+
+===================== =====================================================
+Keyword Argument Description
+===================== =====================================================
+``bbox`` Set this to ``True`` if you want the bounding box
+ to be included in the returned GeoJSON.
+
+``crs`` Set this to ``True`` if you want the coordinate
+ reference system to be included in the returned
+ GeoJSON.
+
+``precision`` It may be used to specify the number of significant
+ digits for the coordinates in the GeoJSON
+ representation -- the default value is 8.
+===================== =====================================================
+
+AsGML
+-----
+
+.. class:: AsGML(expression, version=2, precision=8, **extra)
+
+*Availability*: Oracle, PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a `Geographic Markup
+Language (GML)`__ representation of the geometry.
+
+Example::
+
+ >>> qs = Zipcode.objects.annotate(gml=AsGML('poly'))
+ >>> print(qs[0].gml)
+ <gml:Polygon srsName="EPSG:4326"><gml:OuterBoundaryIs>-147.78711,70.245363 ...
+ -147.78711,70.245363</gml:OuterBoundaryIs></gml:Polygon>
+
+===================== =====================================================
+Keyword Argument Description
+===================== =====================================================
+``precision`` Not used on Oracle. It may be used to specify the number
+ of significant digits for the coordinates in the GML
+ representation -- the default value is 8.
+
+``version`` Not used on Oracle. It may be used to specify the GML
+ version used, and may only be values of 2 or 3. The
+ default value is 2.
+===================== =====================================================
+
+__ http://en.wikipedia.org/wiki/Geography_Markup_Language
+
+AsKML
+-----
+
+.. class:: AsKML(expression, precision=8, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a `Keyhole Markup
+Language (KML)`__ representation of the geometry.
+
+Example::
+
+ >>> qs = Zipcode.objects.annotate(kml=AsKML('poly'))
+ >>> print(qs[0].kml)
+ <Polygon><outerBoundaryIs><LinearRing><coordinates>-103.04135,36.217596,0 ...
+ -103.04135,36.217596,0</coordinates></LinearRing></outerBoundaryIs></Polygon>
+
+===================== =====================================================
+Keyword Argument Description
+===================== =====================================================
+``precision`` This keyword may be used to specify the number of
+ significant digits for the coordinates in the KML
+ representation -- the default value is 8.
+===================== =====================================================
+
+__ https://developers.google.com/kml/documentation/
+
+AsSVG
+-----
+
+.. class:: AsSVG(expression, relative=False, precision=8, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a `Scalable Vector
+Graphics (SVG)`__ representation of the geometry.
+
+===================== =====================================================
+Keyword Argument Description
+===================== =====================================================
+``relative`` If set to ``True``, the path data will be implemented
+ in terms of relative moves. Defaults to ``False``,
+ meaning that absolute moves are used instead.
+
+``precision`` This keyword may be used to specify the number of
+ significant digits for the coordinates in the SVG
+ representation -- the default value is 8.
+===================== =====================================================
+
+__ http://www.w3.org/Graphics/SVG/
+
+BoundingCircle
+--------------
+
+.. class:: BoundingCircle(expression, num_seg=48, **extra)
+
+*Availability*: `PostGIS <http://postgis.net/docs/ST_MinimumBoundingCircle.html>`__
+
+Accepts a single geographic field or expression and returns the smallest circle
+polygon that can fully contain the geometry.
+
+Centroid
+--------
+
+.. class:: Centroid(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a single geographic field or expression and returns the ``centroid``
+value of the geometry.
+
+Difference
+----------
+
+.. class:: Difference(expr1, expr2, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts two geographic fields or expressions and returns the geometric
+difference, that is the part of geometry A that does not intersect with
+geometry B.
+
+Distance
+--------
+
+.. class:: Distance(expr1, expr2, spheroid=None, **extra)
+
+*Availability*: MySQL, PostGIS, Oracle, SpatiaLite
+
+Accepts two geographic fields or expressions and returns the distance between
+them, as a :class:`~django.contrib.gis.measure.Distance` object. On MySQL, a raw
+float value is returned, as it's not possible to automatically determine the
+unit of the field.
+
+On backends that support distance calculation on geodetic coordinates, the
+proper backend function is automatically chosen depending on the SRID value of
+the geometries (e.g. ``ST_Distance_Sphere`` on PostGIS).
+
+When distances are calculated with geodetic (angular) coordinates, as is the
+case with the default WGS84 (4326) SRID, you can set the ``spheroid`` keyword
+argument to decide if the calculation should be based on a simple sphere (less
+accurate, less resource-intensive) or on a spheroid (more accurate, more
+resource-intensive).
+
+In the following example, the distance from the city of Hobart to every other
+:class:`~django.contrib.gis.db.models.PointField` in the ``AustraliaCity``
+queryset is calculated::
+
+ >>> pnt = AustraliaCity.objects.get(name='Hobart').point
+ >>> for city in AustraliaCity.objects.distance('point', pnt):
+ ... print(city.name, city.distance)
+ Wollongong 990071.220408 m
+ Shellharbour 972804.613941 m
+ Thirroul 1002334.36351 m
+ ...
+
+.. note::
+
+ Because the ``distance`` attribute is a
+ :class:`~django.contrib.gis.measure.Distance` object, you can easily express
+ the value in the units of your choice. For example, ``city.distance.mi`` is
+ the distance value in miles and ``city.distance.km`` is the distance value
+ in kilometers. See :doc:`measure` for usage details and the list of
+ :ref:`supported_units`.
+
+Envelope
+--------
+
+.. class:: Envelope(expression, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns the geometry
+representing the bounding box of the geometry.
+
+ForceRHR
+--------
+
+.. class:: ForceRHR(expression, **extra)
+
+*Availability*: `PostGIS <http://postgis.net/docs/ST_ForceRHR.html>`__
+
+Accepts a single geographic field or expression and returns a modified version
+of the polygon/multipolygon in which all of the vertices follow the
+right-hand rule.
+
+GeoHash
+-------
+
+.. class:: GeoHash(expression, **extra)
+
+*Availability*: PostGIS
+
+Accepts a single geographic field or expression and returns a `GeoHash`__
+representation of the geometry.
+
+__ http://en.wikipedia.org/wiki/Geohash
+
+Intersection
+------------
+
+.. class:: Intersection(expr1, expr2, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts two geographic fields or expressions and returns the geometric
+intersection between them.
+
+Length
+------
+
+.. class:: Length(expression, spheroid=True, **extra)
+
+*Availability*: MySQL, Oracle, PostGIS, SpatiaLite
+
+Accepts a single geographic linestring or multilinestring field or expression
+and returns its length as an :class:`~django.contrib.gis.measure.Distance`
+measure. On MySQL, a raw float value is returned, as it's not possible to
+automatically determine the unit of the field.
+
+On PostGIS and SpatiaLite, when the coordinates are geodetic (angular), you can
+specify if the calculation should be based on a simple sphere (less
+accurate, less resource-intensive) or on a spheroid (more accurate, more
+resource-intensive) with the ``spheroid`` keyword argument.
+
+MemSize
+-------
+
+.. class:: MemSize(expression, **extra)
+
+*Availability*: PostGIS
+
+Accepts a single geographic field or expression and returns the memory size
+(number of bytes) that the geometry field takes.
+
+NumGeometries
+-------------
+
+.. class:: NumGeometries(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a single geographic field or expression and returns the number of
+geometries if the geometry field is a collection (e.g., a ``GEOMETRYCOLLECTION``
+or ``MULTI*`` field); otherwise returns ``None``.
+
+NumPoints
+---------
+
+.. class:: NumPoints(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a single geographic field or expression and returns the number of points
+in the first linestring in the geometry field; otherwise returns ``None``.
+
+Perimeter
+---------
+
+.. class:: Perimeter(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a single geographic field or expression and returns the perimeter of the
+geometry field as a :class:`~django.contrib.gis.measure.Distance` object. On
+MySQL, a raw float value is returned, as it's not possible to automatically
+determine the unit of the field.
+
+PointOnSurface
+--------------
+
+.. class:: PointOnSurface(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a single geographic field or expression and returns a ``Point`` geometry
+guaranteed to lie on the surface of the field; otherwise returns ``None``.
+
+Reverse
+-------
+
+.. class:: Reverse(expression, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite (≥ 4.0)
+
+Accepts a single geographic field or expression and returns a geometry with
+reversed coordinates.
+
+Scale
+-----
+
+.. class:: Scale(expression, x, y, z=0.0, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a geometry with
+scaled coordinates by multiplying them with the ``x``, ``y``, and optionally
+``z`` parameters.
+
+SnapToGrid
+----------
+
+.. class:: SnapToGrid(expression, *args, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a geometry with all
+points snapped to the given grid. How the geometry is snapped to the grid
+depends on how many numeric (either float, integer, or long) arguments are
+given.
+
+=================== =====================================================
+Number of Arguments Description
+=================== =====================================================
+1 A single size to snap both the X and Y grids to.
+2 X and Y sizes to snap the grid to.
+4 X, Y sizes and the corresponding X, Y origins.
+=================== =====================================================
+
+SymDifference
+-------------
+
+.. class:: SymDifference(expr1, expr2, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts two geographic fields or expressions and returns the geometric
+symmetric difference (union without the intersection) between the given
+parameters.
+
+Transform
+---------
+
+.. class:: Transform(expression, srid, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts a geographic field or expression and a SRID integer code, and returns
+the transformed geometry to the spatial reference system specified by the
+``srid`` parameter.
+
+.. note::
+
+ What spatial reference system an integer SRID corresponds to may depend on
+ the spatial database used. In other words, the SRID numbers used for Oracle
+ are not necessarily the same as those used by PostGIS.
+
+Translate
+---------
+
+.. class:: Translate(expression, x, y, z=0.0, **extra)
+
+*Availability*: PostGIS, SpatiaLite
+
+Accepts a single geographic field or expression and returns a geometry with
+its coordinates offset by the ``x``, ``y`` and optionally ``z`` numeric
+parameters.
+
+Union
+-----
+
+.. class:: Union(expr1, expr2, **extra)
+
+*Availability*: PostGIS, Oracle, SpatiaLite
+
+Accepts two geographic fields or expressions and returns the union of both
+geometries.
View
133 docs/ref/contrib/gis/geoquerysets.txt
@@ -633,6 +633,12 @@ Oracle ``SDO_WITHIN_DISTANCE(poly, geom, 5)``
``GeoQuerySet`` Methods
=======================
+.. deprecated:: 1.9
+
+ Using ``GeoQuerySet`` methods is now deprecated in favor of the new
+ :doc:`functions`. Albeit a little more verbose, they are much more powerful
+ in how it is possible to combine them to build more complex queries.
+
``GeoQuerySet`` methods specify that a spatial operation be performed
on each spatial operation on each geographic
field in the queryset and store its output in a new attribute on the model
@@ -645,7 +651,7 @@ of every ``GeoQuerySet`` method available in GeoDjango.
.. note::
What methods are available depend on your spatial backend. See
- the :ref:`compatibility table <geoqueryset-method-compatibility>`
+ the :ref:`compatibility table <database-functions-compatibility>`
for more details.
With a few exceptions, the following keyword arguments may be used with all
@@ -691,6 +697,11 @@ Measurement
.. method:: GeoQuerySet.area(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Area` function
+ instead.
+
Returns the area of the geographic field in an ``area`` attribute on
each element of this GeoQuerySet.
@@ -699,6 +710,11 @@ each element of this GeoQuerySet.
.. method:: GeoQuerySet.distance(geom, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Distance` function
+ instead.
+
This method takes a geometry as a parameter, and attaches a ``distance``
attribute to every model in the returned queryset that contains the
distance (as a :class:`~django.contrib.gis.measure.Distance` object) to the given geometry.
@@ -738,6 +754,11 @@ __ http://en.wikipedia.org/wiki/Tasmania
.. method:: GeoQuerySet.length(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Length` function
+ instead.
+
Returns the length of the geometry field in a ``length`` attribute
(a :class:`~django.contrib.gis.measure.Distance` object) on each model in
the queryset.
@@ -747,6 +768,11 @@ the queryset.
.. method:: GeoQuerySet.perimeter(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Perimeter` function
+ instead.
+
Returns the perimeter of the geometry field in a ``perimeter`` attribute
(a :class:`~django.contrib.gis.measure.Distance` object) on each model in
the queryset.
@@ -763,6 +789,11 @@ function evaluated on the geometry field.
.. method:: GeoQuerySet.centroid(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Centroid` function
+ instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
Returns the ``centroid`` value for the geographic field in a ``centroid``
@@ -773,6 +804,11 @@ attribute on each element of the ``GeoQuerySet``.
.. method:: GeoQuerySet.envelope(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Envelope` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
Returns a geometry representing the bounding box of the geometry field in
@@ -783,6 +819,11 @@ an ``envelope`` attribute on each element of the ``GeoQuerySet``.
.. method:: GeoQuerySet.point_on_surface(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.PointOnSurface`
+ function instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
Returns a Point geometry guaranteed to lie on the surface of the
@@ -797,6 +838,11 @@ Geometry Editors
.. method:: GeoQuerySet.force_rhr(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.ForceRHR` function
+ instead.
+
*Availability*: PostGIS
Returns a modified version of the polygon/multipolygon in which all
@@ -808,6 +854,11 @@ of the vertices follow the Right-Hand-Rule, and attaches as a
.. method:: GeoQuerySet.reverse_geom(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Reverse` function
+ instead.
+
*Availability*: PostGIS, Oracle
Reverse the coordinate order of the geometry field, and attaches as a
@@ -818,6 +869,11 @@ Reverse the coordinate order of the geometry field, and attaches as a
.. method:: GeoQuerySet.scale(x, y, z=0.0, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Scale` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
``snap_to_grid``
@@ -825,6 +881,11 @@ Reverse the coordinate order of the geometry field, and attaches as a
.. method:: GeoQuerySet.snap_to_grid(*args, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.SnapToGrid` function
+ instead.
+
Snap all points of the input geometry to the grid. How the
geometry is snapped to the grid depends on how many numeric
(either float, integer, or long) arguments are given.
@@ -842,6 +903,11 @@ Number of Arguments Description
.. method:: GeoQuerySet.transform(srid=4326, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Transform` function
+ instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
The ``transform`` method transforms the geometry field of a model to the spatial
@@ -873,6 +939,11 @@ Example::
~~~~~~~~~~~~~
.. method:: GeoQuerySet.translate(x, y, z=0.0, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Translate` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
Translates the geometry field to a new location using the given numeric
@@ -890,6 +961,11 @@ to each element of the ``GeoQuerySet`` that is the result of the operation.
.. method:: GeoQuerySet.difference(geom)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Difference` function
+ instead.
+
Returns the spatial difference of the geographic field with the given
geometry in a ``difference`` attribute on each element of the
``GeoQuerySet``.
@@ -900,6 +976,11 @@ geometry in a ``difference`` attribute on each element of the
.. method:: GeoQuerySet.intersection(geom)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Intersection`
+ function instead.
+
Returns the spatial intersection of the geographic field with the
given geometry in an ``intersection`` attribute on each element of the
``GeoQuerySet``.
@@ -909,6 +990,11 @@ given geometry in an ``intersection`` attribute on each element of the
.. method:: GeoQuerySet.sym_difference(geom)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.SymDifference`
+ function instead.
+
Returns the symmetric difference of the geographic field with the
given geometry in a ``sym_difference`` attribute on each element of the
``GeoQuerySet``.
@@ -918,6 +1004,11 @@ given geometry in a ``sym_difference`` attribute on each element of the
.. method:: GeoQuerySet.union(geom)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.Union` function
+ instead.
+
Returns the union of the geographic field with the given
geometry in an ``union`` attribute on each element of the
``GeoQuerySet``.
@@ -933,6 +1024,11 @@ of the geometry field in each model converted to the requested output format.
.. method:: GeoQuerySet.geohash(precision=20, **kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.GeoHash` function
+ instead.
+
Attaches a ``geohash`` attribute to every model the queryset
containing the `GeoHash`__ representation of the geometry.
@@ -943,6 +1039,11 @@ __ http://geohash.org/
.. method:: GeoQuerySet.geojson(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.AsGeoJSON` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
Attaches a ``geojson`` attribute to every model in the queryset that contains the
@@ -970,6 +1071,11 @@ __ http://geojson.org/
.. method:: GeoQuerySet.gml(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.AsGML` function
+ instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
Attaches a ``gml`` attribute to every model in the queryset that contains the
@@ -1001,6 +1107,11 @@ __ http://en.wikipedia.org/wiki/Geography_Markup_Language
.. method:: GeoQuerySet.kml(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.AsKML` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
Attaches a ``kml`` attribute to every model in the queryset that contains the
@@ -1029,6 +1140,11 @@ __ https://developers.google.com/kml/documentation/
.. method:: GeoQuerySet.svg(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.AsSVG` function
+ instead.
+
*Availability*: PostGIS, SpatiaLite
Attaches a ``svg`` attribute to every model in the queryset that contains
@@ -1056,6 +1172,11 @@ Miscellaneous
.. method:: GeoQuerySet.mem_size(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.MemSize` function
+ instead.
+
*Availability*: PostGIS
Returns the memory size (number of bytes) that the geometry field takes
@@ -1066,6 +1187,11 @@ in a ``mem_size`` attribute on each element of the ``GeoQuerySet``.
.. method:: GeoQuerySet.num_geom(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.NumGeometries`
+ function instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
Returns the number of geometries in a ``num_geom`` attribute on
@@ -1078,6 +1204,11 @@ otherwise sets with ``None``.
.. method:: GeoQuerySet.num_points(**kwargs)
+.. deprecated:: 1.9
+
+ Use the :class:`~django.contrib.gis.db.models.functions.NumPoints` function
+ instead.
+
*Availability*: PostGIS, Oracle, SpatiaLite
Returns the number of points in the first linestring in the
View
1  docs/ref/contrib/gis/index.txt
@@ -18,6 +18,7 @@ of spatially enabled data.
db-api
forms-api
geoquerysets
+ functions
measure
geos
gdal
View
12 docs/ref/contrib/gis/tutorial.txt
@@ -631,8 +631,8 @@ a ``contains`` lookup using the ``pnt_wkt`` as the parameter::
>>> qs
[<WorldBorder: United States>]
-Here, you retrieved a ``GeoQuerySet`` with only one model: the border of
-the United States (exactly what you would expect).
+Here, you retrieved a ``QuerySet`` with only one model: the border of the
+United States (exactly what you would expect).
Similarly, you may also use a :doc:`GEOS geometry object <geos>`.
Here, you can combine the ``intersects`` spatial lookup with the ``get``
@@ -718,8 +718,12 @@ the GEOS library::
>>> pnt.contains(sm.mpoly)
False
-``GeoQuerySet`` Methods
------------------------
+Geographic annotations
+----------------------
+
+GeoDjango also offers a set of geographic annotations to compute distances and
+several other operations (intersection, difference, etc.). See the
+:doc:`functions` documentation.
Putting your data on the map
View
15 docs/releases/1.9.txt
@@ -62,6 +62,11 @@ Minor features
:mod:`django.contrib.gis`
^^^^^^^^^^^^^^^^^^^^^^^^^^
+* All ``GeoQuerySet`` methods have been deprecated and replaced by
+ :doc:`equivalent database functions </ref/contrib/gis/functions>`. As soon
+ as the legacy methods have been replaced in your code, you should even be
+ able to remove the special ``GeoManager`` from your GIS-enabled classes.
+
* The GDAL interface now supports instantiating file-based and in-memory
:ref:`GDALRaster objects <raster-data-source-objects>` from raw data.
Setters for raster properties such as projection or pixel values have
@@ -396,6 +401,16 @@ of its methods and attributes are either changed or renamed.
The aim of these changes is to provide a documented API for relation fields.
+``GeoManager`` and ``GeoQuerySet`` custom methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+All custom ``GeoQuerySet`` methods (``area()``, ``distance()``, ``gml()``, ...)
+have been replaced by equivalent geographic expressions in annotations (see in
+new features). Hence the need to set a custom ``GeoManager`` to GIS-enabled
+models is now obsolete. As soon as your code doesn't call any of the deprecated
+methods, you can simply remove the ``objects = GeoManager()`` lines from your
+models.
+
Miscellaneous
~~~~~~~~~~~~~
View
1  docs/spelling_wordlist
@@ -492,6 +492,7 @@ Multi
multicolumn
multijoins
multiline
+multilinestring
multipart
multipolygon
multithreaded
View
289 tests/gis_tests/distapp/tests.py
@@ -1,10 +1,14 @@
from __future__ import unicode_literals
+from django.contrib.gis.db.models.functions import (
+ Area, Distance, Length, Perimeter, Transform,
+)
from django.contrib.gis.geos import HAS_GEOS
from django.contrib.gis.measure import D # alias for Distance
from django.db import connection
from django.db.models import Q
-from django.test import TestCase, skipUnlessDBFeature
+from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
+from django.utils.deprecation import RemovedInDjango21Warning
from ..utils import no_oracle, oracle, postgis, spatialite
@@ -95,6 +99,7 @@ def test_dwithin(self):
self.assertListEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist))))
@skipUnlessDBFeature("has_distance_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_distance_projected(self):
"""
Test the `distance` GeoQuerySet method on projected coordinate systems.
@@ -138,6 +143,7 @@ def test_distance_projected(self):
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
@skipUnlessDBFeature("has_distance_method", "supports_distance_geodetic")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_distance_geodetic(self):
"""
Test the `distance` GeoQuerySet method on geodetic coordinate systems.
@@ -199,6 +205,7 @@ def test_distance_geodetic(self):
@no_oracle # Oracle already handles geographic distance calculation.
@skipUnlessDBFeature("has_distance_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_distance_transform(self):
"""
Test the `distance` GeoQuerySet method used with `transform` on a geographic field.
@@ -319,6 +326,7 @@ def test_geodetic_distance_lookups(self):
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
@skipUnlessDBFeature("has_area_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_area(self):
"""
Test the `area` GeoQuerySet method.
@@ -332,6 +340,7 @@ def test_area(self):
self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol)
@skipUnlessDBFeature("has_length_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_length(self):
"""
Test the `length` GeoQuerySet method.
@@ -355,6 +364,7 @@ def test_length(self):
self.assertAlmostEqual(len_m2, i10.length.m, 2)
@skipUnlessDBFeature("has_perimeter_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_perimeter(self):
"""
Test the `perimeter` GeoQuerySet method.
@@ -371,6 +381,7 @@ def test_perimeter(self):
self.assertEqual(0, c.perim.m)
@skipUnlessDBFeature("has_area_method", "has_distance_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_measurement_null_fields(self):
"""
Test the measurement GeoQuerySet methods on fields with NULL values.
@@ -385,8 +396,284 @@ def test_measurement_null_fields(self):
self.assertIsNone(z.area)
@skipUnlessDBFeature("has_distance_method")
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_distance_order_by(self):
qs = SouthTexasCity.objects.distance(Point(3, 3)).order_by(
'distance'
).values_list('name', flat=True).filter(name__in=('San Antonio', 'Pearland'))
self.assertQuerysetEqual(qs, ['San Antonio', 'Pearland'], lambda x: x)
+
+
+'''
+=============================
+Distance functions on PostGIS
+=============================
+
+ | Projected Geometry | Lon/lat Geometry | Geography (4326)
+
+ST_Distance(geom1, geom2) | OK (meters) | :-( (degrees) | OK (meters)
+
+ST_Distance(geom1, geom2, use_spheroid=False) | N/A | N/A | OK (meters), less accurate, quick
+
+Distance_Sphere(geom1, geom2) | N/A | OK (meters) | N/A
+
+Distance_Spheroid(geom1, geom2, spheroid) | N/A | OK (meters) | N/A
+
+
+================================
+Distance functions on Spatialite
+================================
+
+ | Projected Geometry | Lon/lat Geometry
+
+ST_Distance(geom1, geom2) | OK (meters) | N/A
+
+ST_Distance(geom1, geom2, use_ellipsoid=True) | N/A | OK (meters)
+
+ST_Distance(geom1, geom2, use_ellipsoid=False) | N/A | OK (meters), less accurate, quick
+
+'''
+
+
+@skipUnlessDBFeature("gis_enabled")
+class DistanceFunctionsTests(TestCase):
+ fixtures = ['initial']
+
+ @skipUnlessDBFeature("has_Area_function")
+ def test_area(self):
+ # Reference queries:
+ # SELECT ST_Area(poly) FROM distapp_southtexaszipcode;
+ area_sq_m = [5437908.90234375, 10183031.4389648, 11254471.0073242, 9881708.91772461]
+ # Tolerance has to be lower for Oracle
+ tol = 2
+ for i, z in enumerate(SouthTexasZipcode.objects.annotate(area=Area('poly')).order_by('name')):
+ # MySQL is returning a raw float value
+ self.assertAlmostEqual(area_sq_m[i], z.area.sq_m if hasattr(z.area, 'sq_m') else z.area, tol)
+
+ @skipUnlessDBFeature("has_Distance_function")
+ def test_distance_simple(self):
+ """
+ Test a simple distance query, with projected coordinates and without
+ transformation.
+ """
+ lagrange = GEOSGeometry('POINT(805066.295722839 4231496.29461335)', 32140)
+ houston = SouthTexasCity.objects.annotate(dist=Distance('point', lagrange)).order_by('id').first()
+ tol = 2 if oracle else 5
+ self.assertAlmostEqual(
+ houston.dist.m if hasattr(houston.dist, 'm') else houston.dist,
+ 147075.069813,
+ tol
+ )
+
+ @skipUnlessDBFeature("has_Distance_function", "has_Transform_function")
+ def test_distance_projected(self):
+ """
+ Test the `Distance` function on projected coordinate systems.
+ """
+ # The point for La Grange, TX
+ lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
+ # Reference distances in feet and in meters. Got these values from
+ # using the provided raw SQL statements.
+ # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140))
+ # FROM distapp_southtexascity;
+ m_distances = [147075.069813, 139630.198056, 140888.552826,
+ 138809.684197, 158309.246259, 212183.594374,
+ 70870.188967, 165337.758878, 139196.085105]
+ # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278))
+ # FROM distapp_southtexascityft;
+ # Oracle 11 thinks this is not a projected coordinate system, so it's
+ # not tested.
+ ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
+ 455411.438904354, 519386.252102563, 696139.009211594,
+ 232513.278304279, 542445.630586414, 456679.155883207]
+
+ # Testing using different variations of parameters and using models
+ # with different projected coordinate systems.
+ dist1 = SouthTexasCity.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
+ if spatialite or oracle:
+ dist_qs = [dist1]
+ else:
+ dist2 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange)).order_by('id')
+ # Using EWKT string parameter.
+ dist3 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange.ewkt)).order_by('id')
+ dist_qs = [dist1, dist2, dist3]
+
+ # Original query done on PostGIS, have to adjust AlmostEqual tolerance
+ # for Oracle.
+ tol = 2 if oracle else 5
+
+ # Ensuring expected distances are returned for each distance queryset.
+ for qs in dist_qs:
+ for i, c in enumerate(qs):
+ self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
+ self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
+
+ @skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic")
+ def test_distance_geodetic(self):
+ """
+ Test the `Distance` function on geodetic coordinate systems.
+ """
+ # Testing geodetic distance calculation with a non-point geometry
+ # (a LineString of Wollongong and Shellharbour coords).
+ ls = LineString(((150.902, -34.4245), (150.87, -34.5789)), srid=4326)
+
+ # Reference query:
+ # SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326))
+ # FROM distapp_australiacity ORDER BY name;
+ distances = [1120954.92533513, 140575.720018241, 640396.662906304,
+ 60580.9693849269, 972807.955955075, 568451.8357838,
+ 40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0]
+ qs = AustraliaCity.objects.annotate(distance=Distance('point', ls)).order_by('name')
+ for city, distance in zip(qs, distances):
+ # Testing equivalence to within a meter.
+ self.assertAlmostEqual(distance, city.distance.m, 0)
+
+ @skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic")
+ def test_distance_geodetic_spheroid(self):
+ tol = 2 if oracle else 5
+
+ # Got the reference distances using the raw SQL statements:
+ # SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326),
+ # 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
+ # SELECT ST_distance_sphere(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326))
+ # FROM distapp_australiacity WHERE (NOT (id = 11)); st_distance_sphere
+ if connection.ops.postgis and connection.ops.proj_version_tuple() >= (4, 7, 0):
+ # PROJ.4 versions 4.7+ have updated datums, and thus different
+ # distance values.
+ spheroid_distances = [60504.0628957201, 77023.9489850262, 49154.8867574404,
+ 90847.4358768573, 217402.811919332, 709599.234564757,
+ 640011.483550888, 7772.00667991925, 1047861.78619339,
+ 1165126.55236034]
+ sphere_distances = [60580.9693849267, 77144.0435286473, 49199.4415344719,
+ 90804.7533823494, 217713.384600405, 709134.127242793,
+ 639828.157159169, 7786.82949717788, 1049204.06569028,
+ 1162623.7238134]
+
+ else:
+ spheroid_distances = [60504.0628825298, 77023.948962654, 49154.8867507115,
+ 90847.435881812, 217402.811862568, 709599.234619957,
+ 640011.483583758, 7772.00667666425, 1047861.7859506,
+ 1165126.55237647]
+ sphere_distances = [60580.7612632291, 77143.7785056615, 49199.2725132184,
+ 90804.4414289463, 217712.63666124, 709131.691061906,
+ 639825.959074112, 7786.80274606706, 1049200.46122281,
+ 1162619.7297006]
+
+ # Testing with spheroid distances first.
+ hillsdale = AustraliaCity.objects.get(name='Hillsdale')
+ qs = AustraliaCity.objects.exclude(id=hillsdale.id).annotate(
+ distance=Distance('point', hillsdale.point, spheroid=True)
+ ).order_by('id')
+ for i, c in enumerate(qs):
+ self.assertAlmostEqual(spheroid_distances[i], c.distance.m, tol)
+ if postgis:
+ # PostGIS uses sphere-only distances by default, testing these as well.
+ qs = AustraliaCity.objects.exclude(id=hillsdale.id).annotate(
+ distance=Distance('point', hillsdale.point)
+ ).order_by('id')
+ for i, c in enumerate(qs):
+ self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
+
+ @no_oracle # Oracle already handles geographic distance calculation.
+ @skipUnlessDBFeature("has_Distance_function", 'has_Transform_function')
+ def test_distance_transform(self):
+ """
+ Test the `Distance` function used with `Transform` on a geographic field.
+ """
+ # We'll be using a Polygon (created by buffering the centroid
+ # of 77005 to 100m) -- which aren't allowed in geographic distance
+ # queries normally, however our field has been transformed to
+ # a non-geographic system.
+ z = SouthTexasZipcode.objects.get(name='77005')
+
+ # Reference query:
+ # SELECT ST_Distance(ST_Transform("distapp_censuszipcode"."poly", 32140),
+ # ST_GeomFromText('<buffer_wkt>', 32140))
+ # FROM "distapp_censuszipcode";
+ dists_m = [3553.30384972258, 1243.18391525602, 2186.15439472242]
+
+ # Having our buffer in the SRID of the transformation and of the field
+ # -- should get the same results. The first buffer has no need for
+ # transformation SQL because it is the same SRID as what was given
+ # to `transform()`. The second buffer will need to be transformed,
+ # however.
+ buf1 = z.poly.centroid.buffer(100)
+ buf2 = buf1.transform(4269, clone=True)
+ ref_zips = ['77002', '77025', '77401']
+
+ for buf in [buf1, buf2]:
+ qs = CensusZipcode.objects.exclude(name='77005').annotate(
+ distance=Distance(Transform('poly', 32140), buf)
+ ).order_by('name')
+ self.assertEqual(ref_zips, sorted([c.name for c in qs]))
+ for i, z in enumerate(qs):
+ self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
+
+ @skipUnlessDBFeature("has_Distance_function")
+ def test_distance_order_by(self):
+ qs = SouthTexasCity.objects.annotate(distance=Distance('point', Point(3, 3, srid=32140))).order_by(
+ 'distance'
+ ).values_list('name', flat=True).filter(name__in=('San Antonio', 'Pearland'))
+ self.assertQuerysetEqual(qs, ['San Antonio', 'Pearland'], lambda x: x)
+
+ @skipUnlessDBFeature("has_Length_function")
+ def test_length(self):
+ """
+ Test the `Length` function.
+ """
+ # Reference query (should use `length_spheroid`).
+ # SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563,
+ # AUTHORITY["EPSG","7030"]]');
+ len_m1 = 473504.769553813
+ len_m2 = 4617.668
+
+ if connection.features.supports_length_geodetic:
+ qs = Interstate.objects.annotate(length=Length('path'))
+ tol = 2 if oracle else 3
+ self.assertAlmostEqual(len_m1, qs[0].length.m, tol)
+ # TODO: test with spheroid argument (True and False)
+ else:
+ # Does not support geodetic coordinate systems.
+ with self.assertRaises(NotImplementedError):
+ list(Interstate.objects.annotate(length=Length('path')))
+
+ # Now doing length on a projected coordinate system.
+ i10 = SouthTexasInterstate.objects.annotate(length=Length('path')).get(name='I-10')
+ self.assertAlmostEqual(len_m2, i10.length.m if isinstance(i10.length, D) else i10.length, 2)
+ self.assertTrue(
+ SouthTexasInterstate.objects.annotate(length=Length('path')).filter(length__gt=4000).exists()
+ )
+
+ @skipUnlessDBFeature("has_Perimeter_function")
+ def test_perimeter(self):
+ """
+ Test the `Perimeter` function.
+ """
+ # Reference query:
+ # SELECT ST_Perimeter(distapp_southtexaszipcode.poly) FROM distapp_southtexaszipcode;
+ perim_m = [18404.3550889361, 15627.2108551001, 20632.5588368978, 17094.5996143697]
+ tol = 2 if oracle else 7
+ qs = SouthTexasZipcode.objects.annotate(perimeter=Perimeter('poly')).order_by('name')
+ for i, z in enumerate(qs):
+ self.assertAlmostEqual(perim_m[i], z.perimeter.m, tol)
+
+ # Running on points; should return 0.
+ qs = SouthTexasCity.objects.annotate(perim=Perimeter('point'))
+ for city in qs:
+ self.assertEqual(0, city.perim.m)
+
+ @skipUnlessDBFeature("supports_null_geometries", "has_Area_function", "has_Distance_function")
+ def test_measurement_null_fields(self):
+ """
+ Test the measurement functions on fields with NULL values.
+ """
+ # Creating SouthTexasZipcode w/NULL value.
+ SouthTexasZipcode.objects.create(name='78212')
+ # Performing distance/area queries against the NULL PolygonField,
+ # and ensuring the result of the operations is None.
+ htown = SouthTexasCity.objects.get(name='Downtown Houston')
+ z = SouthTexasZipcode.objects.annotate(
+ distance=Distance('poly', htown.point), area=Area('poly')
+ ).get(name='78212')
+ self.assertIsNone(z.distance)
+ self.assertIsNone(z.area)
View
129 tests/gis_tests/geo3d/tests.py
@@ -4,11 +4,16 @@
import re
from unittest import skipUnless
+from django.contrib.gis.db.models.functions import (
+ AsGeoJSON, AsKML, Length, Perimeter, Scale, Translate,
+)
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.geos import HAS_GEOS
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
from django.utils._os import upath
-from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.deprecation import (
+ RemovedInDjango20Warning, RemovedInDjango21Warning,
+)
if HAS_GEOS:
from django.contrib.gis.db.models import Union, Extent3D
@@ -73,18 +78,7 @@
)
-@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
-@skipUnlessDBFeature("gis_enabled", "supports_3d_storage")
-class Geo3DTest(TestCase):
- """
- Only a subset of the PostGIS routines are 3D-enabled, and this TestCase
- tries to test the features that can handle 3D and that are also
- available within GeoDjango. For more information, see the PostGIS docs
- on the routines that support 3D:
-
- http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
- """
-
+class Geo3DLoadingHelper(object):
def _load_interstate_data(self):
# Interstate (2D / 3D and Geographic/Projected variants)
for name, line, exp_z in interstate_data:
@@ -109,6 +103,19 @@ def _load_polygon_data(self):
Polygon2D.objects.create(name='2D BBox', poly=bbox_2d)
Polygon3D.objects.create(name='3D BBox', poly=bbox_3d)
+
+@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
+@skipUnlessDBFeature("gis_enabled", "supports_3d_storage")
+class Geo3DTest(Geo3DLoadingHelper, TestCase):
+ """
+ Only a subset of the PostGIS routines are 3D-enabled, and this TestCase
+ tries to test the features that can handle 3D and that are also
+ available within GeoDjango. For more information, see the PostGIS docs
+ on the routines that support 3D:
+
+ http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
+ """
+
def test_3d_hasz(self):
"""
Make sure data is 3D and has expected Z values -- shouldn't change
@@ -167,6 +174,7 @@ def test_3d_layermapping(self):
lm.save()
self.assertEqual(3, MultiPoint3D.objects.count())
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_kml(self):
"""
Test GeoQuerySet.kml() with Z values.
@@ -178,6 +186,7 @@ def test_kml(self):
ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$')
self.assertTrue(ref_kml_regex.match(h.kml))
+ @ignore_warnings(category=RemovedInDjango21Warning)
def test_geojson(self):
"""
Test GeoQuerySet.geojson() with Z values.
@@ -229,6 +238,7 @@ def check_extent3d(extent3d, tol=6):
self.assertIsNone(City3D.objects.none().extent3d())
self.assertIsNone(City3D.objects.none().aggregate(Extent3D('point'))['point__extent3d'])
+ @ignore_warnings(category=RemovedInDjango21Warning)
@skipUnlessDBFeature("supports_3d_functions")
def test_perimeter(self):
"""
@@ -247,6 +257,7 @@ def test_perimeter(self):
Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m,
tol)
+ @ignore_warnings(category=RemovedInDjango21Warning)
@skipUnlessDBFeature("supports_3d_functions")
def test_length(self):
"""
@@ -280,6 +291,7 @@ def test_length(self):
InterstateProj3D.objects.length().get(name='I-45').length.m,
tol)
+ @ignore_warnings(category=RemovedInDjango21Warning)
@skipUnlessDBFeature("supports_3d_functions")
def test_scale(self):
"""
@@ -292,6 +304,7 @@ def test_scale(self):
for city in City3D.objects.scale(1.0, 1.0, zscale):
self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z)
+ @ignore_warnings(category=RemovedInDjango21Warning)
@skipUnlessDBFeature("supports_3d_functions")
def test_translate(self):
"""
@@ -302,3 +315,93 @@ def test_translate(self):
for ztrans in ztranslations:
for city in City3D.objects.translate(0, 0, ztrans):
self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
+
+
+@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
+@skipUnlessDBFeature("gis_enabled", "supports_3d_functions")
+class Geo3DFunctionsTests(Geo3DLoadingHelper, TestCase):
+ def test_kml(self):
+ """
+ Test KML() function with Z values.
+ """
+ self._load_city_data()
+ h = City3D.objects.annotate(kml=AsKML('point', precision=6)).get(name='Houston')
+ # KML should be 3D.
+ # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';`
+ ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$')
+ self.assertTrue(ref_kml_regex.match(h.kml))
+
+ def test_geojson(self):
+ """
+ Test GeoJSON() function with Z values.
+ """
+ self._load_city_data()
+ h = City3D.objects.annotate(geojson=AsGeoJSON('point', precision=6)).get(name='Houston')
+ # GeoJSON should be 3D
+ # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';`
+ ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$')
+ self.assertTrue(ref_json_regex.match(h.geojson))
+
+ def test_perimeter(self):
+ """
+ Testing Perimeter() function on 3D fields.
+ """
+ self._load_polygon_data()
+ # Reference query for values below:
+ # `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;`
+ ref_perim_3d = 76859.2620451
+ ref_perim_2d = 76859.2577803
+ tol = 6
+ poly2d = Polygon2D.objects.annotate(perimeter=Perimeter('poly')).get(name='2D BBox')
+ self.assertAlmostEqual(ref_perim_2d, poly2d.perimeter.m, tol)
+ poly3d = Polygon3D.objects.annotate(perimeter=Perimeter('poly')).get(name='3D BBox')
+ self.assertAlmostEqual(ref_perim_3d, poly3d.perimeter.m, tol)
+
+ def test_length(self):
+ """
+ Testing Length() function on 3D fields.
+ """
+ # ST_Length_Spheroid Z-aware, and thus does not need to use
+ # a separate function internally.
+ # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]')
+ # FROM geo3d_interstate[2d|3d];`
+ self._load_interstate_data()
+ tol = 3
+ ref_length_2d = 4368.1721949481
+ ref_length_3d = 4368.62547052088
+ inter2d = Interstate2D.objects.annotate(length=Length('line')).get(name='I-45')
+ self.assertAlmostEqual(ref_length_2d, inter2d.length.m, tol)
+ inter3d = Interstate3D.objects.annotate(length=Length('line')).get(name='I-45')
+ self.assertAlmostEqual(ref_length_3d, inter3d.length.m, tol)
+
+ # Making sure `ST_Length3D` is used on for a projected
+ # and 3D model rather than `ST_Length`.
+ # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;`
+ ref_length_2d = 4367.71564892392
+ # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;`
+ ref_length_3d = 4368.16897234101
+ inter2d = InterstateProj2D.objects.annotate(length=Length('line')).get(name='I-45')
+ self.assertAlmostEqual(ref_length_2d, inter2d.length.m, tol)
+ inter3d = InterstateProj3D.objects.annotate(length=Length('line')).get(name='I-45')
+ self.assertAlmostEqual(ref_length_3d, inter3d.length.m, tol)
+
+ def test_scale(self):
+ """
+ Testing Scale() function on Z values.
+ """
+ self._load_city_data()
+ # Mapping of City name to reference Z values.
+ zscales = (-3, 4, 23)
+ for zscale in zscales:
+ for city in City3D.objects.annotate(scale=Scale('point', 1.0, 1.0, zscale)):
+ self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z)
+
+ def test_translate(self):
+ """
+ Testing Translate() function on Z values.
+ """
+ self._load_city_data()
+ ztranslations = (5.23, 23, -17)
+ for ztrans in ztranslations:
+ for city in City3D.objects.annotate(translate=Translate('point', 0, 0, ztrans)):
+ self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
View
461 tests/gis_tests/geoapp/test_functions.py
@@ -0,0 +1,461 @@
+from __future__ import unicode_literals
+
+import re
+from decimal import Decimal
+
+from django.contrib.gis.db.models import functions
+from django.contrib.gis.geos import HAS_GEOS
+from django.db import connection
+from django.test import TestCase, skipUnlessDBFeature
+from django.utils import six
+
+from ..utils import mysql, oracle, postgis, spatialite
+
+if HAS_GEOS:
+ from django.contrib.gis.geos import LineString, Point, Polygon, fromstr
+ from .models import Country, City, State, Track
+
+
+@skipUnlessDBFeature("gis_enabled")
+class GISFunctionsTests(TestCase):
+ """
+ Testing functions from django/contrib/gis/db/models/functions.py.
+ Several tests are taken and adapted from GeoQuerySetTest.
+ Area/Distance/Length/Perimeter are tested in distapp/tests.
+
+ Please keep the tests in function's alphabetic order.
+ """
+ fixtures = ['initial']
+
+ def test_asgeojson(self):
+ # Only PostGIS and SpatiaLite 3.0+ support GeoJSON.
+ if not connection.ops.geojson:
+ with self.assertRaises(NotImplementedError):
+ list(Country.objects.annotate(json=functions.AsGeoJSON('mpoly')))
+ return
+
+ pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}'
+ houston_json = (
+ '{"type":"Point","crs":{"type":"name","properties":'
+ '{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}'
+ )
+ victoria_json = (
+ '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],'
+ '"coordinates":[-123.305196,48.462611]}'
+ )
+ chicago_json = (
+ '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},'
+ '"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}'
+ )
+ if spatialite:
+ victoria_json = (
+ '{"type":"Point","bbox":[-123.305196,48.462611,-123.305196,48.462611],'
+ '"coordinates":[-123.305196,48.462611]}'
+ )
+
+ # Precision argument should only be an integer
+ with self.assertRaises(TypeError):
+ City.objects.annotate(geojson=functions.AsGeoJSON('point', precision='foo'))
+
+ # Reference queries and values.
+ # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 0)
+ # FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Pueblo';
+ self.assertEqual(
+ pueblo_json,
+ City.objects.annotate(geojson=functions.AsGeoJSON('point')).get(name='Pueblo').geojson
+ )
+
+ # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city"
+ # WHERE "geoapp_city"."name" = 'Houston';
+ # This time we want to include the CRS by using the `crs` keyword.
+ self.assertEqual(
+ houston_json,
+ City.objects.annotate(json=functions.AsGeoJSON('point', crs=True)).get(name='Houston').json
+ )
+
+ # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city"
+ # WHERE "geoapp_city"."name" = 'Houston';
+ # This time we include the bounding box by using the `bbox` keyword.
+ self.assertEqual(
+ victoria_json,
+ City.objects.annotate(
+ geojson=functions.AsGeoJSON('point', bbox=True)
+ ).get(name='Victoria').geojson
+ )
+
+ # SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city"
+ # WHERE "geoapp_city"."name" = 'Chicago';
+ # Finally, we set every available keyword.
+ self.assertEqual(
+ chicago_json,
+