mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #24214 -- Added GIS functions to replace geoqueryset's methods
Thanks Simon Charette and Tim Graham for the reviews.
This commit is contained in:
		| @@ -1,3 +1,4 @@ | ||||
| import re | ||||
| from functools import partial | ||||
|  | ||||
| from django.contrib.gis.db.models import aggregates | ||||
| @@ -59,11 +60,11 @@ class BaseSpatialFeatures(object): | ||||
|     # `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 +87,13 @@ class BaseSpatialFeatures(object): | ||||
|             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) | ||||
|   | ||||
| @@ -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 @@ class BaseSpatialOperations(object): | ||||
|     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') | ||||
|   | ||||
| @@ -8,12 +8,13 @@ from psycopg2.extensions import ISQLQuote | ||||
|  | ||||
|  | ||||
| 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 @@ class PostGISAdapter(object): | ||||
|     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()) | ||||
|         ) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										351
									
								
								django/contrib/gis/db/models/functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								django/contrib/gis/db/models/functions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,351 @@ | ||||
| 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) | ||||
|  | ||||
|  | ||||
| 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. | ||||
|                 self.output_field = AreaField( | ||||
|                     AreaMeasure.unit_attname(self.output_field.units_name(connection))) | ||||
|             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): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| 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: | ||||
|             dist_att = DistanceMeasure.unit_attname(geo_field.units_name(connection)) | ||||
|         return DistanceMeasure(**{dist_att: 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_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) | ||||
|  | ||||
|  | ||||
| class MemSize(GeoFunc): | ||||
|     output_field_class = IntegerField | ||||
|  | ||||
|  | ||||
| class NumGeometries(GeoFunc): | ||||
|     output_field_class = IntegerField | ||||
|  | ||||
|  | ||||
| class NumPoints(GeoFunc): | ||||
|     output_field_class = IntegerField | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class Translate(Scale): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Union(GeoFuncWithGeoParam): | ||||
|     pass | ||||
| @@ -1,5 +1,8 @@ | ||||
| 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 | ||||
| @@ -390,3 +393,275 @@ class DistanceTest(TestCase): | ||||
|             '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')): | ||||
|             self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, 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_distance_geodetic: | ||||
|             qs = Interstate.objects.annotate(length=Length('path')) | ||||
|             tol = 2 if oracle else 3 | ||||
|             self.assertAlmostEqual(len_m1, qs[0].length.m, tol) | ||||
|         else: | ||||
|             # Does not support geodetic coordinate systems. | ||||
|             self.assertRaises(ValueError, 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, 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("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) | ||||
|   | ||||
| @@ -4,6 +4,9 @@ import os | ||||
| 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 | ||||
| @@ -73,18 +76,7 @@ bbox_data = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| @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 +101,19 @@ class Geo3DTest(TestCase): | ||||
|         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 | ||||
| @@ -302,3 +307,93 @@ class Geo3DTest(TestCase): | ||||
|         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) | ||||
|   | ||||
							
								
								
									
										447
									
								
								tests/gis_tests/geoapp/test_functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								tests/gis_tests/geoapp/test_functions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,447 @@ | ||||
| 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 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, | ||||
|             City.objects.annotate( | ||||
|                 geojson=functions.AsGeoJSON('point', bbox=True, crs=True, precision=5) | ||||
|                 ).get(name='Chicago').geojson | ||||
|         ) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_AsGML_function") | ||||
|     def test_asgml(self): | ||||
|         # Should throw a TypeError when tyring to obtain GML from a | ||||
|         # non-geometry field. | ||||
|         qs = City.objects.all() | ||||
|         with self.assertRaises(TypeError): | ||||
|             qs.annotate(gml=functions.AsGML('name')) | ||||
|         ptown = City.objects.annotate(gml=functions.AsGML('point', precision=9)).get(name='Pueblo') | ||||
|  | ||||
|         if oracle: | ||||
|             # No precision parameter for Oracle :-/ | ||||
|             gml_regex = re.compile( | ||||
|                 r'^<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml">' | ||||
|                 r'<gml:coordinates decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ ' | ||||
|                 r'</gml:coordinates></gml:Point>' | ||||
|             ) | ||||
|         elif spatialite and connection.ops.spatial_version < (3, 0, 0): | ||||
|             # Spatialite before 3.0 has extra colon in SrsName | ||||
|             gml_regex = re.compile( | ||||
|                 r'^<gml:Point SrsName="EPSG::4326"><gml:coordinates decimal="\." ' | ||||
|                 r'cs="," ts=" ">-104.609251\d+,38.255001</gml:coordinates></gml:Point>' | ||||
|             ) | ||||
|         else: | ||||
|             gml_regex = re.compile( | ||||
|                 r'^<gml:Point srsName="EPSG:4326"><gml:coordinates>' | ||||
|                 r'-104\.60925\d+,38\.255001</gml:coordinates></gml:Point>' | ||||
|             ) | ||||
|  | ||||
|         self.assertTrue(gml_regex.match(ptown.gml)) | ||||
|  | ||||
|         if postgis: | ||||
|             self.assertIn( | ||||
|                 '<gml:pos srsDimension="2">', | ||||
|                 City.objects.annotate(gml=functions.AsGML('point', version=3)).get(name='Pueblo').gml | ||||
|             ) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_AsKML_function") | ||||
|     def test_askml(self): | ||||
|         # Should throw a TypeError when trying to obtain KML from a | ||||
|         # non-geometry field. | ||||
|         with self.assertRaises(TypeError): | ||||
|             City.objects.annotate(kml=functions.AsKML('name')) | ||||
|  | ||||
|         # Ensuring the KML is as expected. | ||||
|         ptown = City.objects.annotate(kml=functions.AsKML('point', precision=9)).get(name='Pueblo') | ||||
|         self.assertEqual('<Point><coordinates>-104.609252,38.255001</coordinates></Point>', ptown.kml) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_AsSVG_function") | ||||
|     def test_assvg(self): | ||||
|         with self.assertRaises(TypeError): | ||||
|             City.objects.annotate(svg=functions.AsSVG('point', precision='foo')) | ||||
|         # SELECT AsSVG(geoapp_city.point, 0, 8) FROM geoapp_city WHERE name = 'Pueblo'; | ||||
|         svg1 = 'cx="-104.609252" cy="-38.255001"' | ||||
|         # Even though relative, only one point so it's practically the same except for | ||||
|         # the 'c' letter prefix on the x,y values. | ||||
|         svg2 = svg1.replace('c', '') | ||||
|         self.assertEqual(svg1, City.objects.annotate(svg=functions.AsSVG('point')).get(name='Pueblo').svg) | ||||
|         self.assertEqual(svg2, City.objects.annotate(svg=functions.AsSVG('point', relative=5)).get(name='Pueblo').svg) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_BoundingCircle_function") | ||||
|     def test_bounding_circle(self): | ||||
|         qs = Country.objects.annotate(circle=functions.BoundingCircle('mpoly')).order_by('name') | ||||
|         self.assertAlmostEqual(qs[0].circle.area, 168.89, 2) | ||||
|         self.assertAlmostEqual(qs[1].circle.area, 135.95, 2) | ||||
|  | ||||
|         qs = Country.objects.annotate(circle=functions.BoundingCircle('mpoly', num_seg=12)).order_by('name') | ||||
|         self.assertAlmostEqual(qs[0].circle.area, 168.44, 2) | ||||
|         self.assertAlmostEqual(qs[1].circle.area, 135.59, 2) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Centroid_function") | ||||
|     def test_centroid(self): | ||||
|         qs = State.objects.exclude(poly__isnull=True).annotate(centroid=functions.Centroid('poly')) | ||||
|         for state in qs: | ||||
|             tol = 0.1  # High tolerance due to oracle | ||||
|             self.assertTrue(state.poly.centroid.equals_exact(state.centroid, tol)) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Difference_function") | ||||
|     def test_difference(self): | ||||
|         geom = Point(5, 23, srid=4326) | ||||
|         qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) | ||||
|         for c in qs: | ||||
|             self.assertEqual(c.mpoly.difference(geom), c.difference) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Difference_function") | ||||
|     def test_difference_mixed_srid(self): | ||||
|         """Testing with mixed SRID (Country has default 4326).""" | ||||
|         geom = Point(556597.4, 2632018.6, srid=3857)  # Spherical mercator | ||||
|         qs = Country.objects.annotate(difference=functions.Difference('mpoly', geom)) | ||||
|         for c in qs: | ||||
|             self.assertEqual(c.mpoly.difference(geom), c.difference) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Envelope_function") | ||||
|     def test_envelope(self): | ||||
|         countries = Country.objects.annotate(envelope=functions.Envelope('mpoly')) | ||||
|         for country in countries: | ||||
|             self.assertIsInstance(country.envelope, Polygon) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_ForceRHR_function") | ||||
|     def test_force_rhr(self): | ||||
|         rings = ( | ||||
|             ((0, 0), (5, 0), (0, 5), (0, 0)), | ||||
|             ((1, 1), (1, 3), (3, 1), (1, 1)), | ||||
|         ) | ||||
|         rhr_rings = ( | ||||
|             ((0, 0), (0, 5), (5, 0), (0, 0)), | ||||
|             ((1, 1), (3, 1), (1, 3), (1, 1)), | ||||
|         ) | ||||
|         State.objects.create(name='Foo', poly=Polygon(*rings)) | ||||
|         st = State.objects.annotate(force_rhr=functions.ForceRHR('poly')).get(name='Foo') | ||||
|         self.assertEqual(rhr_rings, st.force_rhr.coords) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_GeoHash_function") | ||||
|     def test_geohash(self): | ||||
|         # Reference query: | ||||
|         # SELECT ST_GeoHash(point) FROM geoapp_city WHERE name='Houston'; | ||||
|         # SELECT ST_GeoHash(point, 5) FROM geoapp_city WHERE name='Houston'; | ||||
|         ref_hash = '9vk1mfq8jx0c8e0386z6' | ||||
|         h1 = City.objects.annotate(geohash=functions.GeoHash('point')).get(name='Houston') | ||||
|         h2 = City.objects.annotate(geohash=functions.GeoHash('point', precision=5)).get(name='Houston') | ||||
|         self.assertEqual(ref_hash, h1.geohash) | ||||
|         self.assertEqual(ref_hash[:5], h2.geohash) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Intersection_function") | ||||
|     def test_intersection(self): | ||||
|         geom = Point(5, 23, srid=4326) | ||||
|         qs = Country.objects.annotate(inter=functions.Intersection('mpoly', geom)) | ||||
|         for c in qs: | ||||
|             self.assertEqual(c.mpoly.intersection(geom), c.inter) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_MemSize_function") | ||||
|     def test_memsize(self): | ||||
|         ptown = City.objects.annotate(size=functions.MemSize('point')).get(name='Pueblo') | ||||
|         self.assertTrue(20 <= ptown.size <= 40)  # Exact value may depend on PostGIS version | ||||
|  | ||||
|     @skipUnlessDBFeature("has_NumGeom_function") | ||||
|     def test_num_geom(self): | ||||
|         # Both 'countries' only have two geometries. | ||||
|         for c in Country.objects.annotate(num_geom=functions.NumGeometries('mpoly')): | ||||
|             self.assertEqual(2, c.num_geom) | ||||
|  | ||||
|         qs = City.objects.filter(point__isnull=False).annotate(num_geom=functions.NumGeometries('point')) | ||||
|         for city in qs: | ||||
|             # Oracle and PostGIS 2.0+ will return 1 for the number of | ||||
|             # geometries on non-collections, whereas PostGIS < 2.0.0 | ||||
|             # will return None. | ||||
|             if postgis and connection.ops.spatial_version < (2, 0, 0): | ||||
|                 self.assertIsNone(city.num_geom) | ||||
|             else: | ||||
|                 self.assertEqual(1, city.num_geom) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_NumPoint_function") | ||||
|     def test_num_points(self): | ||||
|         coords = [(-95.363151, 29.763374), (-95.448601, 29.713803)] | ||||
|         Track.objects.create(name='Foo', line=LineString(coords)) | ||||
|         qs = Track.objects.annotate(num_points=functions.NumPoints('line')) | ||||
|         self.assertEqual(qs.first().num_points, 2) | ||||
|         if spatialite: | ||||
|             # Spatialite can only count points on LineStrings | ||||
|             return | ||||
|  | ||||
|         for c in Country.objects.annotate(num_points=functions.NumPoints('mpoly')): | ||||
|             self.assertEqual(c.mpoly.num_points, c.num_points) | ||||
|  | ||||
|         if not oracle: | ||||
|             # Oracle cannot count vertices in Point geometries. | ||||
|             for c in City.objects.annotate(num_points=functions.NumPoints('point')): | ||||
|                 self.assertEqual(1, c.num_points) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_PointOnSurface_function") | ||||
|     def test_point_on_surface(self): | ||||
|         # Reference values. | ||||
|         if oracle: | ||||
|             # SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) | ||||
|             # FROM GEOAPP_COUNTRY; | ||||
|             ref = {'New Zealand': fromstr('POINT (174.616364 -36.100861)', srid=4326), | ||||
|                    'Texas': fromstr('POINT (-103.002434 36.500397)', srid=4326), | ||||
|                    } | ||||
|         else: | ||||
|             # Using GEOSGeometry to compute the reference point on surface values | ||||
|             # -- since PostGIS also uses GEOS these should be the same. | ||||
|             ref = {'New Zealand': Country.objects.get(name='New Zealand').mpoly.point_on_surface, | ||||
|                    'Texas': Country.objects.get(name='Texas').mpoly.point_on_surface | ||||
|                    } | ||||
|  | ||||
|         qs = Country.objects.annotate(point_on_surface=functions.PointOnSurface('mpoly')) | ||||
|         for country in qs: | ||||
|             tol = 0.00001  # Spatialite might have WKT-translation-related precision issues | ||||
|             self.assertTrue(ref[country.name].equals_exact(country.point_on_surface, tol)) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Reverse_function") | ||||
|     def test_reverse_geom(self): | ||||
|         coords = [(-95.363151, 29.763374), (-95.448601, 29.713803)] | ||||
|         Track.objects.create(name='Foo', line=LineString(coords)) | ||||
|         track = Track.objects.annotate(reverse_geom=functions.Reverse('line')).get(name='Foo') | ||||
|         coords.reverse() | ||||
|         self.assertEqual(tuple(coords), track.reverse_geom.coords) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Scale_function") | ||||
|     def test_scale(self): | ||||
|         xfac, yfac = 2, 3 | ||||
|         tol = 5  # The low precision tolerance is for SpatiaLite | ||||
|         qs = Country.objects.annotate(scaled=functions.Scale('mpoly', xfac, yfac)) | ||||
|         for country in qs: | ||||
|             for p1, p2 in zip(country.mpoly, country.scaled): | ||||
|                 for r1, r2 in zip(p1, p2): | ||||
|                     for c1, c2 in zip(r1.coords, r2.coords): | ||||
|                         self.assertAlmostEqual(c1[0] * xfac, c2[0], tol) | ||||
|                         self.assertAlmostEqual(c1[1] * yfac, c2[1], tol) | ||||
|         # Test float/Decimal values | ||||
|         qs = Country.objects.annotate(scaled=functions.Scale('mpoly', 1.5, Decimal('2.5'))) | ||||
|         self.assertGreater(qs[0].scaled.area, qs[0].mpoly.area) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_SnapToGrid_function") | ||||
|     def test_snap_to_grid(self): | ||||
|         # Let's try and break snap_to_grid() with bad combinations of arguments. | ||||
|         for bad_args in ((), range(3), range(5)): | ||||
|             with self.assertRaises(ValueError): | ||||
|                 Country.objects.annotate(snap=functions.SnapToGrid('mpoly', *bad_args)) | ||||
|         for bad_args in (('1.0',), (1.0, None), tuple(map(six.text_type, range(4)))): | ||||
|             with self.assertRaises(TypeError): | ||||
|                 Country.objects.annotate(snap=functions.SnapToGrid('mpoly', *bad_args)) | ||||
|  | ||||
|         # Boundary for San Marino, courtesy of Bjorn Sandvik of thematicmapping.org | ||||
|         # from the world borders dataset he provides. | ||||
|         wkt = ('MULTIPOLYGON(((12.41580 43.95795,12.45055 43.97972,12.45389 43.98167,' | ||||
|                '12.46250 43.98472,12.47167 43.98694,12.49278 43.98917,' | ||||
|                '12.50555 43.98861,12.51000 43.98694,12.51028 43.98277,' | ||||
|                '12.51167 43.94333,12.51056 43.93916,12.49639 43.92333,' | ||||
|                '12.49500 43.91472,12.48778 43.90583,12.47444 43.89722,' | ||||
|                '12.46472 43.89555,12.45917 43.89611,12.41639 43.90472,' | ||||
|                '12.41222 43.90610,12.40782 43.91366,12.40389 43.92667,' | ||||
|                '12.40500 43.94833,12.40889 43.95499,12.41580 43.95795)))') | ||||
|         Country.objects.create(name='San Marino', mpoly=fromstr(wkt)) | ||||
|  | ||||
|         # Because floating-point arithmetic isn't exact, we set a tolerance | ||||
|         # to pass into GEOS `equals_exact`. | ||||
|         tol = 0.000000001 | ||||
|  | ||||
|         # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.1)) FROM "geoapp_country" | ||||
|         # WHERE "geoapp_country"."name" = 'San Marino'; | ||||
|         ref = fromstr('MULTIPOLYGON(((12.4 44,12.5 44,12.5 43.9,12.4 43.9,12.4 44)))') | ||||
|         self.assertTrue( | ||||
|             ref.equals_exact( | ||||
|                 Country.objects.annotate( | ||||
|                     snap=functions.SnapToGrid('mpoly', 0.1) | ||||
|                 ).get(name='San Marino').snap, | ||||
|                 tol | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.05, 0.23)) FROM "geoapp_country" | ||||
|         # WHERE "geoapp_country"."name" = 'San Marino'; | ||||
|         ref = fromstr('MULTIPOLYGON(((12.4 43.93,12.45 43.93,12.5 43.93,12.45 43.93,12.4 43.93)))') | ||||
|         self.assertTrue( | ||||
|             ref.equals_exact( | ||||
|                 Country.objects.annotate( | ||||
|                     snap=functions.SnapToGrid('mpoly', 0.05, 0.23) | ||||
|                 ).get(name='San Marino').snap, | ||||
|                 tol | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.5, 0.17, 0.05, 0.23)) FROM "geoapp_country" | ||||
|         # WHERE "geoapp_country"."name" = 'San Marino'; | ||||
|         ref = fromstr( | ||||
|             'MULTIPOLYGON(((12.4 43.87,12.45 43.87,12.45 44.1,12.5 44.1,12.5 43.87,12.45 43.87,12.4 43.87)))' | ||||
|         ) | ||||
|         self.assertTrue( | ||||
|             ref.equals_exact( | ||||
|                 Country.objects.annotate( | ||||
|                     snap=functions.SnapToGrid('mpoly', 0.05, 0.23, 0.5, 0.17) | ||||
|                 ).get(name='San Marino').snap, | ||||
|                 tol | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_SymDifference_function") | ||||
|     def test_sym_difference(self): | ||||
|         geom = Point(5, 23, srid=4326) | ||||
|         qs = Country.objects.annotate(sym_difference=functions.SymDifference('mpoly', geom)) | ||||
|         for country in qs: | ||||
|             # Ordering might differ in collections | ||||
|             self.assertSetEqual(set(g.wkt for g in country.mpoly.sym_difference(geom)), | ||||
|                                 set(g.wkt for g in country.sym_difference)) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Transform_function") | ||||
|     def test_transform(self): | ||||
|         # Pre-transformed points for Houston and Pueblo. | ||||
|         ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774) | ||||
|         prec = 3  # Precision is low due to version variations in PROJ and GDAL. | ||||
|  | ||||
|         # Asserting the result of the transform operation with the values in | ||||
|         #  the pre-transformed points. | ||||
|         h = City.objects.annotate(pt=functions.Transform('point', ptown.srid)).get(name='Pueblo') | ||||
|         self.assertEqual(2774, h.pt.srid) | ||||
|         self.assertAlmostEqual(ptown.x, h.pt.x, prec) | ||||
|         self.assertAlmostEqual(ptown.y, h.pt.y, prec) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Translate_function") | ||||
|     def test_translate(self): | ||||
|         xfac, yfac = 5, -23 | ||||
|         qs = Country.objects.annotate(translated=functions.Translate('mpoly', xfac, yfac)) | ||||
|         for c in qs: | ||||
|             for p1, p2 in zip(c.mpoly, c.translated): | ||||
|                 for r1, r2 in zip(p1, p2): | ||||
|                     for c1, c2 in zip(r1.coords, r2.coords): | ||||
|                         # The low precision is for SpatiaLite | ||||
|                         self.assertAlmostEqual(c1[0] + xfac, c2[0], 5) | ||||
|                         self.assertAlmostEqual(c1[1] + yfac, c2[1], 5) | ||||
|  | ||||
|     # Some combined function tests | ||||
|     @skipUnlessDBFeature( | ||||
|         "has_Difference_function", "has_Intersection_function", | ||||
|         "has_SymDifference_function", "has_Union_function") | ||||
|     def test_diff_intersection_union(self): | ||||
|         "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." | ||||
|         geom = Point(5, 23, srid=4326) | ||||
|         qs = Country.objects.all().annotate( | ||||
|             difference=functions.Difference('mpoly', geom), | ||||
|             sym_difference=functions.SymDifference('mpoly', geom), | ||||
|             union=functions.Union('mpoly', geom), | ||||
|         ) | ||||
|  | ||||
|         # XXX For some reason SpatiaLite does something screwey with the Texas geometry here.  Also, | ||||
|         # XXX it doesn't like the null intersection. | ||||
|         if spatialite: | ||||
|             qs = qs.exclude(name='Texas') | ||||
|         else: | ||||
|             qs = qs.annotate(intersection=functions.Intersection('mpoly', geom)) | ||||
|  | ||||
|         if oracle: | ||||
|             # Should be able to execute the queries; however, they won't be the same | ||||
|             # as GEOS (because Oracle doesn't use GEOS internally like PostGIS or | ||||
|             # SpatiaLite). | ||||
|             return | ||||
|         for c in qs: | ||||
|             self.assertEqual(c.mpoly.difference(geom), c.difference) | ||||
|             if not spatialite: | ||||
|                 self.assertEqual(c.mpoly.intersection(geom), c.intersection) | ||||
|             # Ordering might differ in collections | ||||
|             self.assertSetEqual(set(g.wkt for g in c.mpoly.sym_difference(geom)), | ||||
|                                 set(g.wkt for g in c.sym_difference)) | ||||
|             self.assertSetEqual(set(g.wkt for g in c.mpoly.union(geom)), | ||||
|                                 set(g.wkt for g in c.union)) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Union_function") | ||||
|     def test_union(self): | ||||
|         geom = Point(-95.363151, 29.763374, srid=4326) | ||||
|         ptown = City.objects.annotate(union=functions.Union('point', geom)).get(name='Dallas') | ||||
|         tol = 0.00001 | ||||
|         expected = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)', srid=4326) | ||||
|         self.assertTrue(expected.equals_exact(ptown.union, tol)) | ||||
| @@ -6,6 +6,7 @@ from __future__ import unicode_literals | ||||
| import os | ||||
| from unittest import skipUnless | ||||
|  | ||||
| from django.contrib.gis.db.models.functions import Area, Distance | ||||
| from django.contrib.gis.gdal import HAS_GDAL | ||||
| from django.contrib.gis.geos import HAS_GEOS | ||||
| from django.contrib.gis.measure import D | ||||
| @@ -101,3 +102,30 @@ class GeographyTest(TestCase): | ||||
|         tol = 5 | ||||
|         z = Zipcode.objects.area().get(code='77002') | ||||
|         self.assertAlmostEqual(z.area.sq_m, ref_area, tol) | ||||
|  | ||||
|  | ||||
| @skipUnlessDBFeature("gis_enabled") | ||||
| class GeographyFunctionTests(TestCase): | ||||
|     fixtures = ['initial'] | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic") | ||||
|     def test_distance_function(self): | ||||
|         """ | ||||
|         Testing Distance() support on non-point geography fields. | ||||
|         """ | ||||
|         ref_dists = [0, 4891.20, 8071.64, 9123.95] | ||||
|         htown = City.objects.get(name='Houston') | ||||
|         qs = Zipcode.objects.annotate(distance=Distance('poly', htown.point)) | ||||
|         for z, ref in zip(qs, ref_dists): | ||||
|             self.assertAlmostEqual(z.distance.m, ref, 2) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Area_function", "supports_distance_geodetic") | ||||
|     def test_geography_area(self): | ||||
|         """ | ||||
|         Testing that Area calculations work on geography columns. | ||||
|         """ | ||||
|         # SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002'; | ||||
|         ref_area = 5439100.95415646 if oracle else 5439084.70637573 | ||||
|         tol = 5 | ||||
|         z = Zipcode.objects.annotate(area=Area('poly')).get(code='77002') | ||||
|         self.assertAlmostEqual(z.area.sq_m, ref_area, tol) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user