8000 Fixed #35705 -- Added Rotate GIS database function to rotate geometries. · django/django@51cab4a · GitHub
[go: up one dir, main page]

Skip to cont 8000 ent

Commit 51cab4a

Browse files
enpravafelixxm
authored andcommitted
Fixed #35705 -- Added Rotate GIS database function to rotate geometries.
1 parent f7017db commit 51cab4a

File tree

9 files changed

+76
-5
lines changed

9 files changed

+76
-5
lines changed

django/contrib/gis/db/backends/base/operations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def select_extent(self):
6262
"Perimeter",
6363
"PointOnSurface",
6464
"Reverse",
65+
"Rotate",
6566
"Scale",
6667
"SnapToGrid",
6768
"SymDifference",

django/contrib/gis/db/backends/mysql/operations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def unsupported_functions(self):
9898
"Perimeter",
9999
"PointOnSurface",
100100
"Reverse",
101+
"Rotate",
101102
"Scale",
102103
"SnapToGrid",
103104
"Transform",

django/contrib/gis/db/backends/oracle/operations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
129129
"LineLocatePoint",
130130
"MakeValid",
131131
"MemSize",
132+
"Rotate",
132133
"Scale",
133134
"SnapToGrid",
134135
"Translate",

django/contrib/gis/db/backends/spatialite/operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
8282

8383
@cached_property
8484
def unsupported_functions(self):
85-
unsupported = {"GeometryDistance", "IsEmpty", "MemSize"}
85+
unsupported = {"GeometryDistance", "IsEmpty", "MemSize", "Rotate"}
8686
if not self.geom_lib_version():
8787
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
8888
if self.spatial_version < (5, 1):

django/contrib/gis/db/models/functions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
44
from django.contrib.gis.db.models.sql import AreaField, DistanceField
55
from django.contrib.gis.geos import GEOSGeometry
6+
from django.contrib.gis.geos.point import Point
67
from django.core.exceptions import FieldError
78
from django.db import NotSupportedError
89
from django.db.models import (
@@ -529,6 +530,19 @@ class Reverse(GeoFunc):
529530
arity = 1
530531

531532

533+
class Rotate(GeomOutputGeoFunc):
534+
def __init__(self, expression, angle, origin=None, **extra):
535+
expressions = [
536+
expression,
537+
self._handle_param(angle, "angle", NUMERIC_TYPES),
538+
]
539+
if origin is not None:
540+
if not isinstance(origin, Point):
541+
raise TypeError("origin argument must be a Point")
542+
expressions.append(Value(origin.wkt, output_field=GeometryField()))
543+
super().__init__(*expressions, **extra)
544+
545+
532546
class Scale(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
533547
def __init__(self, expression, x, y, z=0.0, **extra):
534548
expressions = [

docs/ref/contrib/gis/db-api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ Function PostGIS Oracle MariaDB MySQL
420420
:class:`Perimeter` X X X
421421
:class:`PointOnSurface` X X X X
422422
:class:`Reverse` X X X
423+
:class:`Rotate` X
423424
:class:`Scale` X X
424425
:class:`SnapToGrid` X X
425426
:class:`SymDifference` X X X X X

docs/ref/contrib/gis/functions.txt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ Measurement Relationships Operations Edi
2828
:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForcePolygonCW` :class:`AsGeoJSON` :class:`IsEmpty`
2929
:class:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`IsValid`
3030
:class:`GeometryDistance` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`MemSize`
31-
:class:`Length` :class:`ClosestPoint` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumGeometries`
32-
:class:`Perimeter` :class:`Envelope` :class:`SnapToGrid` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
33-
:class:`LineLocatePoint` :class:`Transform` :class:`FromWKT` :class:`AsWKT`
34-
:class:`PointOnSurface` :class:`Translate` :class:`GeoHash`
31+
:class:`Length` :class:`ClosestPoint` :class:`Union` :class:`Rotate` :class:`AsSVG` :class:`NumGeometries`
32+
:class:`Perimeter` :class:`Envelope` :class:`Scale` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
33+
:class:`LineLocatePoint` :class:`SnapToGrid` :class:`FromWKT` :class:`AsWKT`
34+
:class:`PointOnSurface` :class:`Transform` :class:`GeoHash`
35+
:class:`Translate`
3536
========================= ======================== ====================== ======================= ================== ================== ======================
3637

3738
``Area``
@@ -556,6 +557,19 @@ SpatiaLite
556557
Accepts a single geographic field or expression and returns a geometry with
557558
reversed coordinates.
558559

560+
``Rotate``
561+
==========
562+
563+
.. versionadded:: 6.0
564+
565+
.. class:: Rotate(expression, angle, origin=None, **extra)
566+
567+
*Availability*: `PostGIS <https://postgis.net/docs/ST_Rotate.html>`__
568+
569+
Rotates a geometry by a specified ``angle`` around the origin. Optionally, the
570+
rotation can be performed around a point, defined by the ``origin``
571+
parameter.
572+
559573
``Scale``
560574
=========
561575

docs/releases/6.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ Minor features
6868
* The new :attr:`.GEOSGeometry.hasm` property checks whether the geometry has
6969
the M dimension.
7070

71+
* The new :class:`~django.contrib.gis.db.models.functions.Rotate` database
72+
function rotates a geometry by a specified angle around the origin or a
73+
specified point.
74+
7175
:mod:`django.contrib.messages`
7276
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7377

tests/gis_tests/geoapp/test_functions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,41 @@ def test_reverse_geom(self):
612612
coords.reverse()
613613
self.assertEqual(tuple(coords), track.reverse_geom.coords)
614614

615+
@skipUnlessDBFeature("has_Rotate_function")
616+
def test_rotate(self):
617+
angle = math.pi
618+
tests = [
619+
{"angle": angle},
620+
{"angle": angle, "origin": Point(0, 0)},
621+
{"angle": angle, "origin": Point(1, 1)},
622+
]
623+
for params in tests:
624+
with self.subTest(params=params):
625+
qs = Country.objects.annotate(
626+
rotated=functions.Rotate("mpoly", **params)
627+
)
628+
for country in qs:
629+
for p1, p2 in zip(country.mpoly, country.rotated):
630+
for r1, r2 in zip(p1, p2):
631+
for c1, c2 in zip(r1.coords, r2.coords):
632+
origin = params.get("origin")
633+
if origin is None:
634+
origin = Point(0, 0)
635+
self.assertAlmostEqual(-c1[0] + 2 * origin.x, c2[0], 5)
636+
self.assertAlmostEqual(-c1[1] + 2 * origin.y, c2[1], 5)
637+
638+
@skipUnlessDBFeature("has_Rotate_function")
639+
def test_rotate_invalid_params(self):
640+
angle = math.pi
641+
bad_params_tests = [
642+
{"angle": angle, "origin": 0},
643+
{"angle": angle, "origin": [0, 0]},
644+
]
645+
msg = "origin argument must be a Point"
646+
for params in bad_params_tests:
647+
with self.subTest(params=params), self.assertRaisesMessage(TypeError, msg):
648+
functions.Rotate("mpoly", **params)
649+
615650
@skipUnlessDBFeature("has_Scale_function")
616651
def test_scale(self):
617652
xfac, yfac = 2, 3

0 commit comments

Comments
 (0)
0