mirror of
https://github.com/django/django.git
synced 2025-09-11 11:39:12 +00:00
Fixed #35705 -- Added Rotate GIS database function to rotate geometries.
This commit is contained in:
parent
f7017db92c
commit
51cab4ad51
@ -62,6 +62,7 @@ class BaseSpatialOperations:
|
|||||||
"Perimeter",
|
"Perimeter",
|
||||||
"PointOnSurface",
|
"PointOnSurface",
|
||||||
"Reverse",
|
"Reverse",
|
||||||
|
"Rotate",
|
||||||
"Scale",
|
"Scale",
|
||||||
"SnapToGrid",
|
"SnapToGrid",
|
||||||
"SymDifference",
|
"SymDifference",
|
||||||
|
@ -98,6 +98,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
"Perimeter",
|
"Perimeter",
|
||||||
"PointOnSurface",
|
"PointOnSurface",
|
||||||
"Reverse",
|
"Reverse",
|
||||||
|
"Rotate",
|
||||||
"Scale",
|
"Scale",
|
||||||
"SnapToGrid",
|
"SnapToGrid",
|
||||||
"Transform",
|
"Transform",
|
||||||
|
@ -129,6 +129,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
"LineLocatePoint",
|
"LineLocatePoint",
|
||||||
"MakeValid",
|
"MakeValid",
|
||||||
"MemSize",
|
"MemSize",
|
||||||
|
"Rotate",
|
||||||
"Scale",
|
"Scale",
|
||||||
"SnapToGrid",
|
"SnapToGrid",
|
||||||
"Translate",
|
"Translate",
|
||||||
|
@ -82,7 +82,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def unsupported_functions(self):
|
def unsupported_functions(self):
|
||||||
unsupported = {"GeometryDistance", "IsEmpty", "MemSize"}
|
unsupported = {"GeometryDistance", "IsEmpty", "MemSize", "Rotate"}
|
||||||
if not self.geom_lib_version():
|
if not self.geom_lib_version():
|
||||||
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
|
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
|
||||||
if self.spatial_version < (5, 1):
|
if self.spatial_version < (5, 1):
|
||||||
|
@ -3,6 +3,7 @@ from decimal import Decimal
|
|||||||
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
|
from django.contrib.gis.db.models.fields import BaseSpatialField, GeometryField
|
||||||
from django.contrib.gis.db.models.sql import AreaField, DistanceField
|
from django.contrib.gis.db.models.sql import AreaField, DistanceField
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
from django.contrib.gis.geos.point import Point
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.db import NotSupportedError
|
from django.db import NotSupportedError
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
@ -529,6 +530,19 @@ class Reverse(GeoFunc):
|
|||||||
arity = 1
|
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):
|
class Scale(SQLiteDecimalToFloatMixin, GeomOutputGeoFunc):
|
||||||
def __init__(self, expression, x, y, z=0.0, **extra):
|
def __init__(self, expression, x, y, z=0.0, **extra):
|
||||||
expressions = [
|
expressions = [
|
||||||
|
@ -420,6 +420,7 @@ Function PostGIS Oracle MariaDB MySQL
|
|||||||
:class:`Perimeter` X X X
|
:class:`Perimeter` X X X
|
||||||
:class:`PointOnSurface` X X X X
|
:class:`PointOnSurface` X X X X
|
||||||
:class:`Reverse` X X X
|
:class:`Reverse` X X X
|
||||||
|
:class:`Rotate` X
|
||||||
:class:`Scale` X X
|
:class:`Scale` X X
|
||||||
:class:`SnapToGrid` X X
|
:class:`SnapToGrid` X X
|
||||||
:class:`SymDifference` X X X X X
|
:class:`SymDifference` X X X X X
|
||||||
|
@ -28,10 +28,11 @@ Measurement Relationships Operations Edi
|
|||||||
:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForcePolygonCW` :class:`AsGeoJSON` :class:`IsEmpty`
|
: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:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`IsValid`
|
||||||
:class:`GeometryDistance` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`MemSize`
|
: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:`Length` :class:`ClosestPoint` :class:`Union` :class:`Rotate` :class:`AsSVG` :class:`NumGeometries`
|
||||||
:class:`Perimeter` :class:`Envelope` :class:`SnapToGrid` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
|
:class:`Perimeter` :class:`Envelope` :class:`Scale` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints`
|
||||||
:class:`LineLocatePoint` :class:`Transform` :class:`FromWKT` :class:`AsWKT`
|
:class:`LineLocatePoint` :class:`SnapToGrid` :class:`FromWKT` :class:`AsWKT`
|
||||||
:class:`PointOnSurface` :class:`Translate` :class:`GeoHash`
|
:class:`PointOnSurface` :class:`Transform` :class:`GeoHash`
|
||||||
|
:class:`Translate`
|
||||||
========================= ======================== ====================== ======================= ================== ================== ======================
|
========================= ======================== ====================== ======================= ================== ================== ======================
|
||||||
|
|
||||||
``Area``
|
``Area``
|
||||||
@ -556,6 +557,19 @@ SpatiaLite
|
|||||||
Accepts a single geographic field or expression and returns a geometry with
|
Accepts a single geographic field or expression and returns a geometry with
|
||||||
reversed coordinates.
|
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``
|
``Scale``
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
@ -68,6 +68,10 @@ Minor features
|
|||||||
* The new :attr:`.GEOSGeometry.hasm` property checks whether the geometry has
|
* The new :attr:`.GEOSGeometry.hasm` property checks whether the geometry has
|
||||||
the M dimension.
|
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`
|
:mod:`django.contrib.messages`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -612,6 +612,41 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
|
|||||||
coords.reverse()
|
coords.reverse()
|
||||||
self.assertEqual(tuple(coords), track.reverse_geom.coords)
|
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")
|
@skipUnlessDBFeature("has_Scale_function")
|
||||||
def test_scale(self):
|
def test_scale(self):
|
||||||
xfac, yfac = 2, 3
|
xfac, yfac = 2, 3
|
||||||
|
Loading…
x
Reference in New Issue
Block a user