1
0
mirror of https://github.com/django/django.git synced 2024-12-23 01:25:58 +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 Natalia
parent 20f9f61805
commit 3958387872
8 changed files with 85 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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):

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.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,25 @@ class Reverse(GeoFunc):
arity = 1 arity = 1
class Rotate(GeomOutputGeoFunc):
def __init__(self, expression, angle, x=0.0, y=0.0, origin=None, **extra):
expressions = [
expression,
self._handle_param(angle, "angle", NUMERIC_TYPES),
]
if origin is not None:
if not isinstance(origin, Point):
raise TypeError("Param origin must be of type Point")
expressions.append(Value(origin.wkt, output_field=GeometryField()))
else:
expressions = expressions + [
self._handle_param(x, "x", NUMERIC_TYPES),
self._handle_param(y, "y", NUMERIC_TYPES),
]
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 = [

View File

@ -576,6 +576,32 @@ 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``
===========
.. class:: Rotate(expression, angle, x=0.0, y=0.0, origin=None, **extra)
.. versionadded:: 5.2
*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 specified point, defined by either
the ``x`` and ``y`` parameters or the ``origin`` parameter.
:param expression: A geographic field or expression to rotate.
:param angle: The angle in radians by which to rotate the geometry.
Positive values result in counterclockwise rotation,
negative values result in clockwise rotation.
:param x: The X-coordinate of the point around which the geometry will be rotated.
:param y: The Y-coordinate of the point around which the geometry will be rotated.
:param origin: A :class:`~django.contrib.gis.geos.Point` around which
to rotate. Takes precedence over ``x`` and ``y``.
:param extra: Additional arguments passed to the underlying function.
:returns: A geometry with coordinates rotated by the specified angle.
:rtype: :class:`~django.contrib.gis.geos.GEOSGeometry`
``Scale`` ``Scale``
========= =========

View File

@ -132,6 +132,10 @@ Minor features
:class:`~django.contrib.gis.db.models.functions.IsValid` database functions :class:`~django.contrib.gis.db.models.functions.IsValid` database functions
are now supported on MariaDB 11.7+. are now supported on MariaDB 11.7+.
* 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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -612,6 +612,37 @@ 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
qs_origin = Country.objects.annotate(
rotated=functions.Rotate("mpoly", angle=angle)
)
qs_xy_params = Country.objects.annotate(
rotated=functions.Rotate("mpoly", angle=angle, x=1, y=1)
)
qs_origin_param = Country.objects.annotate(
rotated=functions.Rotate("mpoly", angle=angle, origin=Point(0, 0))
)
for c_origin, c_xy_params, c_origin_point_param in zip(
qs_origin, qs_xy_params, qs_origin_param
):
for p1, p2 in zip(c_origin.mpoly, c_origin.rotated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertAlmostEqual(-c1[0], c2[0], 5)
self.assertAlmostEqual(-c1[1], c2[1], 5)
for p1, p2 in zip(c_xy_params.mpoly, c_xy_params.rotated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertAlmostEqual(-c1[0] + 2, c2[0], 5)
self.assertAlmostEqual(-c1[1] + 2, c2[1], 5)
for p1, p2 in zip(c_origin_point_param.mpoly, c_origin_point_param.rotated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertAlmostEqual(-c1[0], c2[0], 5)
self.assertAlmostEqual(-c1[1], c2[1], 5)
@skipUnlessDBFeature("has_Scale_function") @skipUnlessDBFeature("has_Scale_function")
def test_scale(self): def test_scale(self):
xfac, yfac = 2, 3 xfac, yfac = 2, 3