mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #15165 -- Prevented wrong results with perimeter on geodetic fields.
This commit is contained in:
		| @@ -26,9 +26,10 @@ class BaseSpatialFeatures(object): | ||||
|     supports_real_shape_operations = True | ||||
|     # Can geometry fields be null? | ||||
|     supports_null_geometries = True | ||||
|     # Can the `distance`/`length` functions be applied on geodetic coordinate systems? | ||||
|     # Can the the function be applied on geodetic coordinate systems? | ||||
|     supports_distance_geodetic = True | ||||
|     supports_length_geodetic = True | ||||
|     supports_perimeter_geodetic = False | ||||
|     # Is the database able to count vertices on polygons (with `num_points`)? | ||||
|     supports_num_points_poly = True | ||||
|  | ||||
|   | ||||
| @@ -7,3 +7,4 @@ class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures): | ||||
|     supports_add_srs_entry = False | ||||
|     supports_geometry_field_introspection = False | ||||
|     supports_geometry_field_unique_index = False | ||||
|     supports_perimeter_geodetic = True | ||||
|   | ||||
| @@ -206,6 +206,9 @@ class Difference(OracleToleranceMixin, GeoFuncWithGeoParam): | ||||
|  | ||||
|  | ||||
| class DistanceResultMixin(object): | ||||
|     def source_is_geography(self): | ||||
|         return self.get_source_fields()[0].geography and self.srid == 4326 | ||||
|  | ||||
|     def convert_value(self, value, expression, connection, context): | ||||
|         if value is None: | ||||
|             return None | ||||
| @@ -236,9 +239,7 @@ class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFuncWithGeoParam): | ||||
|  | ||||
|     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: | ||||
|         if self.source_is_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): | ||||
| @@ -297,9 +298,7 @@ class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc): | ||||
|  | ||||
|     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: | ||||
|         if self.source_is_geography(): | ||||
|             self.source_expressions.append(Value(self.spheroid)) | ||||
|         elif geo_field.geodetic(connection): | ||||
|             # Geometry fields with geodetic (lon/lat) coordinates need length_spheroid | ||||
| @@ -346,11 +345,20 @@ class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc): | ||||
|     arity = 1 | ||||
|  | ||||
|     def as_postgresql(self, compiler, connection): | ||||
|         geo_field = GeometryField(srid=self.srid)  # Fake field to get SRID info | ||||
|         if geo_field.geodetic(connection) and not self.source_is_geography(): | ||||
|             raise NotImplementedError("ST_Perimeter cannot use a non-projected non-geography field.") | ||||
|         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) | ||||
|  | ||||
|     def as_sqlite(self, compiler, connection): | ||||
|         geo_field = GeometryField(srid=self.srid)  # Fake field to get SRID info | ||||
|         if geo_field.geodetic(connection): | ||||
|             raise NotImplementedError("Perimeter cannot use a non-projected field.") | ||||
|         return super(Perimeter, self).as_sql(compiler, connection) | ||||
|  | ||||
|  | ||||
| class PointOnSurface(OracleToleranceMixin, GeoFunc): | ||||
|     arity = 1 | ||||
|   | ||||
| @@ -444,6 +444,8 @@ Distance_Sphere(geom1, geom2)                 |    N/A             |   OK (meter | ||||
|  | ||||
| Distance_Spheroid(geom1, geom2, spheroid)     |    N/A             |   OK (meters)    |    N/A | ||||
|  | ||||
| ST_Perimeter(geom1)                           |    OK              |   :-( (degrees)  |    OK | ||||
|  | ||||
|  | ||||
| ================================ | ||||
| Distance functions on Spatialite | ||||
| @@ -457,6 +459,8 @@ ST_Distance(geom1, geom2, use_ellipsoid=True)   |    N/A             |      OK ( | ||||
|  | ||||
| ST_Distance(geom1, geom2, use_ellipsoid=False)  |    N/A             |      OK (meters), less accurate, quick | ||||
|  | ||||
| Perimeter(geom1)                                |    OK              |      :-( (degrees) | ||||
|  | ||||
| '''  # NOQA | ||||
|  | ||||
|  | ||||
| @@ -688,6 +692,20 @@ class DistanceFunctionsTests(TestCase): | ||||
|         for city in qs: | ||||
|             self.assertEqual(0, city.perim.m) | ||||
|  | ||||
|     @skipUnlessDBFeature("has_Perimeter_function") | ||||
|     def test_perimeter_geodetic(self): | ||||
|         # Currently only Oracle supports calculating the perimeter on geodetic | ||||
|         # geometries (without being transformed). | ||||
|         qs1 = CensusZipcode.objects.annotate(perim=Perimeter('poly')) | ||||
|         if connection.features.supports_perimeter_geodetic: | ||||
|             self.assertAlmostEqual(qs1[0].perim.m, 18406.3818954314, 3) | ||||
|         else: | ||||
|             with self.assertRaises(NotImplementedError): | ||||
|                 list(qs1) | ||||
|         # But should work fine when transformed to projected coordinates | ||||
|         qs2 = CensusZipcode.objects.annotate(perim=Perimeter(Transform('poly', 32140))).filter(name='77002') | ||||
|         self.assertAlmostEqual(qs2[0].perim.m, 18404.355, 3) | ||||
|  | ||||
|     @skipUnlessDBFeature("supports_null_geometries", "has_Area_function", "has_Distance_function") | ||||
|     def test_measurement_null_fields(self): | ||||
|         """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user