diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py index 287841b29e..fafdf60743 100644 --- a/django/contrib/gis/db/backends/base/operations.py +++ b/django/contrib/gis/db/backends/base/operations.py @@ -42,6 +42,7 @@ class BaseSpatialOperations: "Azimuth", "BoundingCircle", "Centroid", + "ClosestPoint", "Difference", "Distance", "Envelope", diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 0799a6b28f..886db605cd 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -75,6 +75,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): "AsSVG", "Azimuth", "BoundingCircle", + "ClosestPoint", "ForcePolygonCW", "GeometryDistance", "IsEmpty", diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index c191d0b1f7..b58af886de 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -121,6 +121,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): "AsKML", "AsSVG", "Azimuth", + "ClosestPoint", "ForcePolygonCW", "GeoHash", "GeometryDistance", diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index d886f8916e..19da355d28 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -280,6 +280,11 @@ class Centroid(OracleToleranceMixin, GeomOutputGeoFunc): arity = 1 +class ClosestPoint(GeomOutputGeoFunc): + arity = 2 + geom_param_pos = (0, 1) + + class Difference(OracleToleranceMixin, GeomOutputGeoFunc): arity = 2 geom_param_pos = (0, 1) diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index d215eb1ecb..f8c0d83960 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -356,6 +356,7 @@ Function PostGIS Oracle MariaDB MySQL :class:`Azimuth` X X (LWGEOM/RTTOPO) :class:`BoundingCircle` X X :class:`Centroid` X X X X X +:class:`ClosestPoint` X X :class:`Difference` X X X X X :class:`Distance` X X X X X :class:`Envelope` X X X X X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index a07feebfb7..963e9ed4bd 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -26,10 +26,10 @@ 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:`Envelope` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumGeometries` -:class:`Perimeter` :class:`LineLocatePoint` :class:`SnapToGrid` :class:`FromWKB` :class:`AsWKB` :class:`NumPoints` - :class:`PointOnSurface` :class:`Transform` :class:`FromWKT` :class:`AsWKT` - :class:`Translate` :class:`GeoHash` +: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` ========================= ======================== ====================== ======================= ================== ================== ====================== ``Area`` @@ -237,6 +237,19 @@ The ``num_seg`` parameter is used only on PostGIS. Accepts a single geographic field or expression and returns the ``centroid`` value of the geometry. +``ClosestPoint`` +================ + +.. versionadded:: 5.0 + +.. class:: ClosestPoint(expr1, expr2, **extra) + +*Availability*: `PostGIS `__, +SpatiaLite + +Accepts two geographic fields or expressions and returns the 2-dimensional +point on geometry A that is closest to geometry B. + ``Difference`` ============== diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index a1dc6a1c1c..d1a4cba870 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -64,7 +64,10 @@ Minor features :mod:`django.contrib.gis` ~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The new + :class:`ClosestPoint() ` + function returns a 2-dimensional point on the geometry that is closest to + another geometry. :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 59b0410aca..e9e2a5dcae 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -456,6 +456,18 @@ class GISFunctionsTests(FuncTestMixin, TestCase): ): qs.get(area__lt=500000) + @skipUnlessDBFeature("has_ClosestPoint_function") + def test_closest_point(self): + qs = Country.objects.annotate( + closest_point=functions.ClosestPoint("mpoly", functions.Centroid("mpoly")) + ) + for country in qs: + self.assertIsInstance(country.closest_point, Point) + self.assertEqual( + country.mpoly.intersection(country.closest_point), + country.closest_point, + ) + @skipUnlessDBFeature("has_LineLocatePoint_function") def test_line_locate_point(self): pos_expr = functions.LineLocatePoint(