diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 0680a8d0af..013ffa74f6 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -18,6 +18,7 @@ from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance from django.db.backends.oracle.operations import DatabaseOperations from django.utils import six +from django.utils.functional import cached_property DEFAULT_TOLERANCE = '0.05' @@ -85,6 +86,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): function_names = { 'Area': 'SDO_GEOM.SDO_AREA', + 'BoundingCircle': 'SDO_GEOM.SDO_MBC', 'Centroid': 'SDO_GEOM.SDO_CENTROID', 'Difference': 'SDO_GEOM.SDO_DIFFERENCE', 'Distance': 'SDO_GEOM.SDO_DISTANCE', @@ -131,11 +133,15 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): truncate_params = {'relate': None} - unsupported_functions = { - 'AsGeoJSON', 'AsKML', 'AsSVG', 'BoundingCircle', 'Envelope', - 'ForceRHR', 'GeoHash', 'MakeValid', 'MemSize', 'Scale', - 'SnapToGrid', 'Translate', - } + @cached_property + def unsupported_functions(self): + unsupported = { + 'AsGeoJSON', 'AsKML', 'AsSVG', 'Envelope', 'ForceRHR', 'GeoHash', + 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', 'Translate', + } + if self.connection.oracle_full_version < '12.1.0.2': + unsupported.add('BoundingCircle') + return unsupported def geo_quote_name(self, name): return super(OracleOperations, self).geo_quote_name(name).upper() diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index b404397f95..dea042493a 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -201,10 +201,15 @@ class AsSVG(GeoFunc): super(AsSVG, self).__init__(*expressions, **extra) -class BoundingCircle(GeoFunc): +class BoundingCircle(OracleToleranceMixin, GeoFunc): def __init__(self, expression, num_seg=48, **extra): super(BoundingCircle, self).__init__(*[expression, num_seg], **extra) + def as_oracle(self, compiler, connection): + clone = self.copy() + clone.set_source_expressions([self.get_source_expressions()[0]]) + return super(BoundingCircle, clone).as_oracle(compiler, connection) + class Centroid(OracleToleranceMixin, GeoFunc): arity = 1 diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 670b23cd78..2ed758db28 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -374,38 +374,38 @@ Database functions The following table provides a summary of what geography-specific database functions are available on each spatial backend. -==================================== ======= ====== =========== ========== -Function 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 X -:class:`Difference` X X X (≥ 5.6.1) X -:class:`Distance` X X X (≥ 5.6.1) X -:class:`Envelope` X X X +==================================== ======= ============== =========== ========== +Function 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 X (≥ 12.1.0.2) +:class:`Centroid` X X X X +:class:`Difference` X X X (≥ 5.6.1) X +:class:`Distance` X X X (≥ 5.6.1) X +:class:`Envelope` X X X :class:`ForceRHR` X -:class:`GeoHash` X X (LWGEOM) -:class:`Intersection` X X X (≥ 5.6.1) X -:class:`IsValid` X X X (LWGEOM) -:class:`Length` X X X X -:class:`MakeValid` X X (LWGEOM) +:class:`GeoHash` X X (LWGEOM) +:class:`Intersection` X X X (≥ 5.6.1) X +:class:`IsValid` X X X (LWGEOM) +:class:`Length` X X X X +:class:`MakeValid` X X (LWGEOM) :class:`MemSize` X -:class:`NumGeometries` X X X X -:class:`NumPoints` X X X X -:class:`Perimeter` X X X -:class:`PointOnSurface` X X X -:class:`Reverse` X X X -:class:`Scale` X X -:class:`SnapToGrid` X X -:class:`SymDifference` X X X (≥ 5.6.1) X -:class:`Transform` X X X -:class:`Translate` X X -:class:`Union` X X X (≥ 5.6.1) X -==================================== ======= ====== =========== ========== +:class:`NumGeometries` X X X X +:class:`NumPoints` X X X X +:class:`Perimeter` X X X +:class:`PointOnSurface` X X X +:class:`Reverse` X X X +:class:`Scale` X X +:class:`SnapToGrid` X X +:class:`SymDifference` X X X (≥ 5.6.1) X +:class:`Transform` X X X +:class:`Translate` X X +:class:`Union` X X X (≥ 5.6.1) X +==================================== ======= ============== =========== ========== Aggregate Functions ------------------- diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 3a731c96f9..2917a658af 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -165,11 +165,18 @@ __ http://www.w3.org/Graphics/SVG/ .. class:: BoundingCircle(expression, num_seg=48, **extra) -*Availability*: `PostGIS `__ +*Availability*: `PostGIS `__, +`Oracle (≥ 12.1.0.2) `_ Accepts a single geographic field or expression and returns the smallest circle polygon that can fully contain the geometry. +The ``num_seg`` parameter is used only on PostGIS. + +.. versionchanged:: 1.11 + + Oracle support was added. + ``Centroid`` ============ diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 40467eca88..4eea7f0df8 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -165,6 +165,7 @@ Minor features * Added Oracle support for the :class:`~django.contrib.gis.db.models.functions.AsGML` function, + :class:`~django.contrib.gis.db.models.functions.BoundingCircle` function, :class:`~django.contrib.gis.db.models.functions.IsValid` function, and :lookup:`isvalid` lookup. diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 7b6281a4b9..023a6a087f 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -11,7 +11,7 @@ from django.db.models import Sum from django.test import TestCase, skipUnlessDBFeature from django.utils import six -from ..utils import mysql, oracle, spatialite +from ..utils import mysql, oracle, postgis, spatialite from .models import City, Country, CountryWebMercator, State, Track @@ -148,21 +148,25 @@ class GISFunctionsTests(TestCase): # num_seg is the number of segments per quarter circle. return (4 * num_seg) + 1 - # The weak precision in the assertions is because the BoundingCircle - # calculation changed on PostGIS 2.3. + expected_areas = (169, 136) if postgis else (171, 126) qs = Country.objects.annotate(circle=functions.BoundingCircle('mpoly')).order_by('name') - self.assertAlmostEqual(qs[0].circle.area, 169, 0) - self.assertAlmostEqual(qs[1].circle.area, 136, 0) - # By default num_seg=48. - self.assertEqual(qs[0].circle.num_points, circle_num_points(48)) - self.assertEqual(qs[1].circle.num_points, circle_num_points(48)) + self.assertAlmostEqual(qs[0].circle.area, expected_areas[0], 0) + self.assertAlmostEqual(qs[1].circle.area, expected_areas[1], 0) + if postgis: + # By default num_seg=48. + self.assertEqual(qs[0].circle.num_points, circle_num_points(48)) + self.assertEqual(qs[1].circle.num_points, circle_num_points(48)) qs = Country.objects.annotate(circle=functions.BoundingCircle('mpoly', num_seg=12)).order_by('name') - self.assertGreater(qs[0].circle.area, 168.4, 0) - self.assertLess(qs[0].circle.area, 169.5, 0) - self.assertAlmostEqual(qs[1].circle.area, 136, 0) - self.assertEqual(qs[0].circle.num_points, circle_num_points(12)) - self.assertEqual(qs[1].circle.num_points, circle_num_points(12)) + if postgis: + self.assertGreater(qs[0].circle.area, 168.4, 0) + self.assertLess(qs[0].circle.area, 169.5, 0) + self.assertAlmostEqual(qs[1].circle.area, 136, 0) + self.assertEqual(qs[0].circle.num_points, circle_num_points(12)) + self.assertEqual(qs[1].circle.num_points, circle_num_points(12)) + else: + self.assertAlmostEqual(qs[0].circle.area, expected_areas[0], 0) + self.assertAlmostEqual(qs[1].circle.area, expected_areas[1], 0) @skipUnlessDBFeature("has_Centroid_function") def test_centroid(self):