From aefe624c62639c189845803f0935fc29e20b5fb8 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Fri, 24 Nov 2017 22:30:53 +0500 Subject: [PATCH] Fixed #28841 -- Added ForcePolygonCW GIS function and deprecated ForceRHR. --- .../gis/db/backends/mysql/operations.py | 8 ++-- .../gis/db/backends/oracle/operations.py | 6 +-- .../gis/db/backends/postgis/operations.py | 2 + .../gis/db/backends/spatialite/operations.py | 2 +- django/contrib/gis/db/models/functions.py | 13 +++++++ docs/internals/deprecation.txt | 2 + docs/ref/contrib/gis/functions.txt | 39 ++++++++++++++----- docs/releases/2.1.txt | 3 +- tests/gis_tests/geoapp/test_functions.py | 18 ++++++++- 9 files changed, 73 insertions(+), 20 deletions(-) diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index f336ea1fb7..3432444b1d 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/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'}) diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index a57ee16161..f6b05bdd74 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/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): diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 4fb9e61ceb..dd3fdb37b0 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/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 diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 38abe72a65..ad64c76887 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/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 diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index b3c8d521ca..45ce8193ee 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/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() diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 1b7d5f73a9..248a0c642e 100644 --- a/docs/internals/deprecation.txt +++ b/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 ` for more details on these changes. diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 66099c3317..fe8239d467 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/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 `__ + +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 `__ Accepts a single geographic field or expression and returns a modified version diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index 9d090b8244..bf69a59762 100644 --- a/docs/releases/2.1.txt +++ b/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: diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 33fe139fb0..2449650567 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/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)),