1
0
mirror of https://github.com/django/django.git synced 2025-03-12 02:12:38 +00:00

Fixed #35705 -- Added Rotate GIS database function to rotate geometries.

This commit is contained in:
enprava 2024-09-08 20:32:13 +02:00 committed by Mariusz Felisiak
parent f7017db92c
commit 51cab4ad51
9 changed files with 76 additions and 5 deletions

View File

@ -62,6 +62,7 @@ class BaseSpatialOperations:
"Perimeter",
"PointOnSurface",
"Reverse",
"Rotate",
"Scale",
"SnapToGrid",
"SymDifference",

View File

@ -98,6 +98,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
"Perimeter",
"PointOnSurface",
"Reverse",
"Rotate",
"Scale",
"SnapToGrid",
"Transform",

View File

@ -129,6 +129,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
"LineLocatePoint",
"MakeValid",
"MemSize",
"Rotate",
"Scale",
"SnapToGrid",
"Translate",

View File

@ -82,7 +82,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
@cached_property
def unsupported_functions(self):
unsupported = {"GeometryDistance", "IsEmpty", "MemSize"}
unsupported = {"GeometryDistance", "IsEmpty", "MemSize", "Rotate"}
if not self.geom_lib_version():
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
if self.spatial_version < (5, 1):

View File

@ -3,6 +3,7 @@ from decimal import Decimal
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
from django.contrib.gis.db.models.sql import AreaField, DistanceField
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos.point import Point
from django.core.exceptions import FieldError
from django.db import NotSupportedError
from django.db.models import (
@ -529,6 +530,19 @@ class Reverse(GeoFunc):
arity = 1
class Rotate(GeomOutputGeoFunc):
def __init__(self, expression, angle, origin=None, **extra):
expressions = [
expression,
self._handle_param(angle, "angle", NUMERIC_TYPES),
]
if origin is not None:
if not isinstance(origin, Point):
raise TypeError("origin argument must be a Point")
expressions.append(Value(origin.wkt, output_field=GeometryField()))
super().__init__(*expressions, **extra)
class Scale(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
def __init__(self, expression, x, y, z=0.0, **extra):
expressions = [

View File

@ -420,6 +420,7 @@ Function PostGIS Oracle MariaDB MySQL
:class:`Perimeter` X X X
:class:`PointOnSurface` X X X X
:class:`Reverse` X X X
:class:`Rotate` X
:class:`Scale` X X
:class:`SnapToGrid` X X
:class:`SymDifference` X X X X X

View File

@ -28,10 +28,11 @@ Measurement Relationships Operations Edi
:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForcePolygonCW` :class:`AsGeoJSON` :class:`IsEmpty`
:class:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`IsValid`
:class:`GeometryDistance` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`MemSize`
:class:`Length` :class:`ClosestPoint` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumGeometries`
:class:`Perimeter` :class:`Envelope` :class:`SnapToGrid` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
:class:`LineLocatePoint` :class:`Transform` :class:`FromWKT` :class:`AsWKT`
:class:`PointOnSurface` :class:`Translate` :class:`GeoHash`
:class:`Length` :class:`ClosestPoint` :class:`Union` :class:`Rotate` :class:`AsSVG` :class:`NumGeometries`
:class:`Perimeter` :class:`Envelope` :class:`Scale` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
:class:`LineLocatePoint` :class:`SnapToGrid` :class:`FromWKT` :class:`AsWKT`
:class:`PointOnSurface` :class:`Transform` :class:`GeoHash`
:class:`Translate`
========================= ======================== ====================== ======================= ================== ================== ======================
``Area``
@ -556,6 +557,19 @@ SpatiaLite
Accepts a single geographic field or expression and returns a geometry with
reversed coordinates.
``Rotate``
==========
.. versionadded:: 6.0
.. class:: Rotate(expression, angle, origin=None, **extra)
*Availability*: `PostGIS <https://postgis.net/docs/ST_Rotate.html>`__
Rotates a geometry by a specified ``angle`` around the origin. Optionally, the
rotation can be performed around a point, defined by the ``origin``
parameter.
``Scale``
=========

View File

@ -68,6 +68,10 @@ Minor features
* The new :attr:`.GEOSGeometry.hasm` property checks whether the geometry has
the M dimension.
* The new :class:`~django.contrib.gis.db.models.functions.Rotate` database
function rotates a geometry by a specified angle around the origin or a
specified point.
:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -612,6 +612,41 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
coords.reverse()
self.assertEqual(tuple(coords), track.reverse_geom.coords)
@skipUnlessDBFeature("has_Rotate_function")
def test_rotate(self):
angle = math.pi
tests = [
{"angle": angle},
{"angle": angle, "origin": Point(0, 0)},
{"angle": angle, "origin": Point(1, 1)},
]
for params in tests:
with self.subTest(params=params):
qs = Country.objects.annotate(
rotated=functions.Rotate("mpoly", **params)
)
for country in qs:
for p1, p2 in zip(country.mpoly, country.rotated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
origin = params.get("origin")
if origin is None:
origin = Point(0, 0)
self.assertAlmostEqual(-c1[0] + 2 * origin.x, c2[0], 5)
self.assertAlmostEqual(-c1[1] + 2 * origin.y, c2[1], 5)
@skipUnlessDBFeature("has_Rotate_function")
def test_rotate_invalid_params(self):
angle = math.pi
bad_params_tests = [
{"angle": angle, "origin": 0},
{"angle": angle, "origin": [0, 0]},
]
msg = "origin argument must be a Point"
for params in bad_params_tests:
with self.subTest(params=params), self.assertRaisesMessage(TypeError, msg):
functions.Rotate("mpoly", **params)
@skipUnlessDBFeature("has_Scale_function")
def test_scale(self):
xfac, yfac = 2, 3