Browse Source

Fixed #28841 -- Added ForcePolygonCW GIS function and deprecated ForceRHR.

Sergey Fedoseev 7 years ago
parent
commit
aefe624c62

+ 4 - 4
django/contrib/gis/db/backends/mysql/operations.py

@@ -57,10 +57,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
     @cached_property
     def unsupported_functions(self):
         unsupported = {
-            'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle', 'ForceRHR',
-            'LineLocatePoint', 'MakeValid', 'MemSize', 'Perimeter',
-            'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid', 'Transform',
-            'Translate',
+            'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle',
+            'ForcePolygonCW', 'ForceRHR', 'LineLocatePoint', 'MakeValid',
+            'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale',
+            'SnapToGrid', 'Transform', 'Translate',
         }
         if self.connection.mysql_version < (5, 7, 5):
             unsupported.update({'AsGeoJSON', 'GeoHash', 'IsValid'})

+ 3 - 3
django/contrib/gis/db/backends/oracle/operations.py

@@ -105,9 +105,9 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
     }
 
     unsupported_functions = {
-        'AsGeoJSON', 'AsKML', 'AsSVG', 'Azimuth', 'Envelope', 'ForceRHR',
-        'GeoHash', 'LineLocatePoint', 'MakeValid', 'MemSize', 'Scale',
-        'SnapToGrid', 'Translate',
+        'AsGeoJSON', 'AsKML', 'AsSVG', 'Azimuth', 'Envelope',
+        'ForcePolygonCW', 'ForceRHR', 'GeoHash', 'LineLocatePoint',
+        'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', 'Translate',
     }
 
     def geo_quote_name(self, name):

+ 2 - 0
django/contrib/gis/db/backends/postgis/operations.py

@@ -158,6 +158,8 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
                 'LengthSpheroid': 'ST_length_spheroid',
                 'MemSize': 'ST_mem_size',
             })
+        if self.spatial_version < (2, 4, 0):
+            function_names['ForcePolygonCW'] = 'ST_ForceRHR'
         return function_names
 
     @cached_property

+ 1 - 1
django/contrib/gis/db/backends/spatialite/operations.py

@@ -78,7 +78,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
 
     @cached_property
     def unsupported_functions(self):
-        unsupported = {'BoundingCircle', 'ForceRHR', 'MemSize'}
+        unsupported = {'BoundingCircle', 'ForcePolygonCW', 'ForceRHR', 'MemSize'}
         if not self.lwgeom_version():
             unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'}
         return unsupported

+ 13 - 0
django/contrib/gis/db/models/functions.py

@@ -1,3 +1,4 @@
+import warnings
 from decimal import Decimal
 
 from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
@@ -10,6 +11,7 @@ from django.db.models import (
 from django.db.models.expressions import Func, Value
 from django.db.models.functions import Cast
 from django.db.utils import NotSupportedError
+from django.utils.deprecation import RemovedInDjango30Warning
 from django.utils.functional import cached_property
 
 NUMERIC_TYPES = (int, float, Decimal)
@@ -274,9 +276,20 @@ class Envelope(GeomOutputGeoFunc):
     arity = 1
 
 
+class ForcePolygonCW(GeomOutputGeoFunc):
+    arity = 1
+
+
 class ForceRHR(GeomOutputGeoFunc):
     arity = 1
 
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            'ForceRHR is deprecated in favor of ForcePolygonCW.',
+            RemovedInDjango30Warning, stacklevel=2,
+        )
+        super().__init__(*args, **kwargs)
+
 
 class GeoHash(GeoFunc):
     output_field = TextField()

+ 2 - 0
docs/internals/deprecation.txt

@@ -29,6 +29,8 @@ details on these changes.
 * The ``field_name`` keyword argument of ``QuerySet.earliest()`` and
   ``latest()`` will be removed.
 
+* ``django.contrib.gis.db.models.functions.ForceRHR`` will be removed.
+
 See the :ref:`Django 2.1 release notes <deprecated-features-2.1>` for more
 details on these changes.
 

+ 29 - 10
docs/ref/contrib/gis/functions.txt

@@ -20,17 +20,18 @@ get a ``NotImplementedError`` exception.
 
 Function's summary:
 
-==================  ========================   ======================  ===================  ==================  =====================
-Measurement         Relationships              Operations              Editors              Output format       Miscellaneous
-==================  ========================   ======================  ===================  ==================  =====================
-:class:`Area`       :class:`Azimuth`           :class:`Difference`     :class:`ForceRHR`    :class:`AsGeoJSON`  :class:`IsValid`
-:class:`Distance`   :class:`BoundingCircle`    :class:`Intersection`   :class:`MakeValid`   :class:`AsGML`      :class:`MemSize`
-:class:`Length`     :class:`Centroid`          :class:`SymDifference`  :class:`Reverse`     :class:`AsKML`      :class:`NumGeometries`
-:class:`Perimeter`  :class:`Envelope`          :class:`Union`          :class:`Scale`       :class:`AsSVG`      :class:`NumPoints`
-..                  :class:`LineLocatePoint`                           :class:`SnapToGrid`  :class:`GeoHash`
-..                  :class:`PointOnSurface`                            :class:`Transform`
+==================  ========================   ======================  =======================  ==================  =====================
+Measurement         Relationships              Operations              Editors                  Output format       Miscellaneous
+==================  ========================   ======================  =======================  ==================  =====================
+:class:`Area`       :class:`Azimuth`           :class:`Difference`     :class:`ForcePolygonCW`  :class:`AsGeoJSON`  :class:`IsValid`
+:class:`Distance`   :class:`BoundingCircle`    :class:`Intersection`   :class:`ForceRHR`        :class:`AsGML`      :class:`MemSize`
+:class:`Length`     :class:`Centroid`          :class:`SymDifference`  :class:`MakeValid`       :class:`AsKML`      :class:`NumGeometries`
+:class:`Perimeter`  :class:`Envelope`          :class:`Union`          :class:`Reverse`         :class:`AsSVG`      :class:`NumPoints`
+..                  :class:`LineLocatePoint`                           :class:`Scale`           :class:`GeoHash`
+..                  :class:`PointOnSurface`                            :class:`SnapToGrid`
+..                                                                     :class:`Transform`
 ..                                                                     :class:`Translate`
-==================  ========================   ======================  ===================  ==================  =====================
+==================  ========================   ======================  =======================  ==================  =====================
 
 ``Area``
 ========
@@ -271,11 +272,29 @@ SpatiaLite
 Accepts a single geographic field or expression and returns the geometry
 representing the bounding box of the geometry.
 
+``ForcePolygonCW``
+==================
+
+.. class:: ForcePolygonCW(expression, **extra)
+
+.. versionadded:: 2.1
+
+*Availability*: `PostGIS <https://postgis.net/docs/ST_ForcePolygonCW.html>`__
+
+Accepts a single geographic field or expression and returns a modified version
+of the polygon/multipolygon in which all exterior rings are oriented clockwise
+and all interior rings are oriented counterclockwise. Non-polygonal geometries
+are returned unchanged.
+
 ``ForceRHR``
 ============
 
 .. class:: ForceRHR(expression, **extra)
 
+.. deprecated:: 2.1
+
+    Use :class:`ForcePolygonCW` instead.
+
 *Availability*: `PostGIS <https://postgis.net/docs/ST_ForceRHR.html>`__
 
 Accepts a single geographic field or expression and returns a modified version

+ 2 - 1
docs/releases/2.1.txt

@@ -239,7 +239,8 @@ Features deprecated in 2.1
 Miscellaneous
 -------------
 
-* ...
+* The ``ForceRHR`` GIS function is deprecated in favor of the new
+  :class:`~django.contrib.gis.db.models.functions.ForcePolygonCW` function.
 
 .. _removed-features-2.1:
 

+ 17 - 1
tests/gis_tests/geoapp/test_functions.py

@@ -10,7 +10,8 @@ from django.contrib.gis.geos import (
 from django.contrib.gis.measure import Area
 from django.db import NotSupportedError, connection
 from django.db.models import Sum
-from django.test import TestCase, skipUnlessDBFeature
+from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
+from django.utils.deprecation import RemovedInDjango30Warning
 
 from ..utils import FuncTestMixin, mysql, oracle, postgis, spatialite
 from .models import City, Country, CountryWebMercator, State, Track
@@ -215,7 +216,22 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
         for country in countries:
             self.assertIsInstance(country.envelope, Polygon)
 
+    @skipUnlessDBFeature("has_ForcePolygonCW_function")
+    def test_force_polygon_cw(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_polygon_cw=functions.ForcePolygonCW('poly')).get(name='Foo')
+        self.assertEqual(rhr_rings, st.force_polygon_cw.coords)
+
     @skipUnlessDBFeature("has_ForceRHR_function")
+    @ignore_warnings(category=RemovedInDjango30Warning)
     def test_force_rhr(self):
         rings = (
             ((0, 0), (5, 0), (0, 5), (0, 0)),