From 79c298c9ceb8e386f172330236f04bb0ae4f7fdd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niccol=C3=B2=20Mineo?= <niccolo@20tab.com>
Date: Fri, 13 Jan 2023 17:48:27 +0100
Subject: [PATCH] Fixed #34266 -- Added ClosestPoint GIS database functions.

---
 .../gis/db/backends/base/operations.py        |  1 +
 .../gis/db/backends/mysql/operations.py       |  1 +
 .../gis/db/backends/oracle/operations.py      |  1 +
 django/contrib/gis/db/models/functions.py     |  5 +++++
 docs/ref/contrib/gis/db-api.txt               |  1 +
 docs/ref/contrib/gis/functions.txt            | 21 +++++++++++++++----
 docs/releases/5.0.txt                         |  5 ++++-
 tests/gis_tests/geoapp/test_functions.py      | 12 +++++++++++
 8 files changed, 42 insertions(+), 5 deletions(-)

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 <https://postgis.net/docs/ST_ClosestPoint.html>`__,
+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() <django.contrib.gis.db.models.functions.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(