This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new 6beb6e50d4 [GH-2798] Add ST_ShortestLine function (#2806)
6beb6e50d4 is described below

commit 6beb6e50d4c4b3089d8df7451e3a1ec9a4305ad7
Author: Jia Yu <[email protected]>
AuthorDate: Tue Mar 31 14:47:29 2026 -0700

    [GH-2798] Add ST_ShortestLine function (#2806)
---
 .../java/org/apache/sedona/common/Functions.java   |  9 +++
 .../org/apache/sedona/common/FunctionsTest.java    | 55 +++++++++++++++++++
 docs/api/flink/Geometry-Functions.md               |  1 +
 .../flink/Measurement-Functions/ST_ShortestLine.md | 64 ++++++++++++++++++++++
 .../snowflake/vector-data/Geometry-Functions.md    |  1 +
 .../Measurement-Functions/ST_ShortestLine.md       | 62 +++++++++++++++++++++
 docs/api/sql/Geometry-Functions.md                 |  1 +
 .../sql/Measurement-Functions/ST_ShortestLine.md   | 64 ++++++++++++++++++++++
 docs/image/ST_ShortestLine/ST_ShortestLine.svg     | 36 ++++++++++++
 .../ST_ShortestLine_point_linestring.svg           | 50 +++++++++++++++++
 .../ST_ShortestLine_point_point.svg                | 38 +++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../apache/sedona/flink/expressions/Functions.java | 22 ++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 10 ++++
 python/sedona/spark/sql/st_functions.py            | 14 +++++
 python/tests/sql/test_dataframe_api.py             | 10 ++++
 python/tests/sql/test_function.py                  | 17 ++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  1 +
 .../sql/sedona_sql/expressions/Functions.scala     |  8 +++
 .../sql/sedona_sql/expressions/st_functions.scala  |  3 +
 .../org/apache/sedona/sql/PreserveSRIDSuite.scala  |  1 +
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |  8 +++
 .../org/apache/sedona/sql/functionTestScala.scala  | 24 ++++++++
 23 files changed, 500 insertions(+)

diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java 
b/common/src/main/java/org/apache/sedona/common/Functions.java
index 0c7f487b65..fdec8913c3 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1115,6 +1115,15 @@ public class Functions {
     }
   }
 
+  public static Geometry shortestLine(Geometry left, Geometry right) {
+    if (left.isEmpty() || right.isEmpty()) {
+      return null;
+    }
+    DistanceOp distanceOp = new DistanceOp(left, right);
+    Coordinate[] closestPoints = distanceOp.nearestPoints();
+    return left.getFactory().createLineString(closestPoints);
+  }
+
   public static Geometry delaunayTriangle(Geometry geometry) {
     return delaunayTriangle(geometry, 0.0, 0);
   }
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java 
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 455ea784c0..e48f62c546 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -4195,6 +4195,61 @@ public class FunctionsTest extends TestBase {
     assertEquals(expected, e2.getMessage());
   }
 
+  @Test
+  public void shortestLinePointToPoint() {
+    Point point1 = GEOMETRY_FACTORY.createPoint(new Coordinate(0, 0));
+    Point point2 = GEOMETRY_FACTORY.createPoint(new Coordinate(3, 4));
+    String expected = "LINESTRING (0 0, 3 4)";
+    String actual = Functions.shortestLine(point1, point2).toText();
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void shortestLinePointToLineString() {
+    Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(160, 40));
+    LineString lineString =
+        GEOMETRY_FACTORY.createLineString(
+            coordArray(10, 30, 50, 50, 30, 110, 70, 90, 180, 140, 130, 190));
+    Geometry result = Functions.shortestLine(point, lineString);
+    assertNotNull(result);
+    assertEquals("LineString", result.getGeometryType());
+    // First point should be on the point geometry, second on the linestring
+    assertEquals(160.0, result.getCoordinates()[0].x, 1e-6);
+    assertEquals(40.0, result.getCoordinates()[0].y, 1e-6);
+  }
+
+  @Test
+  public void shortestLinePolygonToPolygon() {
+    Polygon polygonA =
+        GEOMETRY_FACTORY.createPolygon(coordArray(190, 150, 20, 10, 160, 70, 
190, 150));
+    Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(80, 160));
+    Geometry polygonB = Functions.buffer(point, 30);
+    Geometry result = Functions.shortestLine(polygonA, polygonB);
+    assertNotNull(result);
+    assertEquals("LineString", result.getGeometryType());
+    // The length of the shortest line should equal the distance between the 
polygons
+    assertEquals(Functions.distance(polygonA, polygonB), result.getLength(), 
1e-6);
+  }
+
+  @Test
+  public void shortestLineEmptyGeometry() {
+    Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1));
+    LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+    assertNull(Functions.shortestLine(point, emptyLineString));
+
+    Polygon emptyPolygon = GEOMETRY_FACTORY.createPolygon();
+    assertNull(Functions.shortestLine(emptyPolygon, emptyLineString));
+  }
+
+  @Test
+  public void shortestLineSameGeometry() {
+    Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(5, 5));
+    Geometry result = Functions.shortestLine(point, point);
+    assertNotNull(result);
+    assertEquals("LINESTRING (5 5, 5 5)", result.toText());
+    assertEquals(0.0, result.getLength(), 1e-6);
+  }
+
   @Test
   public void testZmFlag() throws ParseException {
     int _2D = 0, _3DM = 1, _3DZ = 2, _4D = 3;
diff --git a/docs/api/flink/Geometry-Functions.md 
b/docs/api/flink/Geometry-Functions.md
index a5f31304b5..d0c3f42d5c 100644
--- a/docs/api/flink/Geometry-Functions.md
+++ b/docs/api/flink/Geometry-Functions.md
@@ -201,6 +201,7 @@ These functions compute measurements of distance, area, 
length, and angles.
 | [ST_MinimumClearanceLine](Measurement-Functions/ST_MinimumClearanceLine.md) 
| Geometry | This function returns a two-point LineString geometry representing 
the minimum clearance distance of the input geometry. If the input geometry 
does not have a defined minimum clearance, such as for... | v1.6.1 |
 | [ST_Perimeter](Measurement-Functions/ST_Perimeter.md) | Double | This 
function calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... | v1.7.0 |
 | [ST_Perimeter2D](Measurement-Functions/ST_Perimeter2D.md) | Double | This 
function calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... | v1.7.1 |
+| [ST_ShortestLine](Measurement-Functions/ST_ShortestLine.md) | Geometry | 
Returns the shortest LineString between two geometries. The line starts on 
geom1 and ends on geom2. If either geometry is empty, returns null. | v1.9.0 |
 
 ## Geometry Processing
 
diff --git a/docs/api/flink/Measurement-Functions/ST_ShortestLine.md 
b/docs/api/flink/Measurement-Functions/ST_ShortestLine.md
new file mode 100644
index 0000000000..04631551c0
--- /dev/null
+++ b/docs/api/flink/Measurement-Functions/ST_ShortestLine.md
@@ -0,0 +1,64 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_ShortestLine
+
+Introduction: Returns the shortest LineString between two geometries. The line 
starts on geom1 and ends on geom2. If either geometry is empty, the function 
returns null.
+
+![ST_ShortestLine](../../../image/ST_ShortestLine/ST_ShortestLine.svg 
"ST_ShortestLine")
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+![ST_ShortestLine Point to 
Point](../../../image/ST_ShortestLine/ST_ShortestLine_point_point.svg 
"ST_ShortestLine Point to Point")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 0)'),
+        ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+![ST_ShortestLine Point to 
LineString](../../../image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg 
"ST_ShortestLine Point to LineString")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 1)'),
+        ST_GeomFromText('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 1, 0 0)
+```
diff --git a/docs/api/snowflake/vector-data/Geometry-Functions.md 
b/docs/api/snowflake/vector-data/Geometry-Functions.md
index f51f00f2d8..e22c5d0a30 100644
--- a/docs/api/snowflake/vector-data/Geometry-Functions.md
+++ b/docs/api/snowflake/vector-data/Geometry-Functions.md
@@ -193,6 +193,7 @@ These functions compute measurements of distance, area, 
length, and angles.
 | [ST_MinimumClearanceLine](Measurement-Functions/ST_MinimumClearanceLine.md) 
| This function returns a two-point LineString geometry representing the 
minimum clearance distance of the input geometry. If the input geometry does 
not have a defined minimum clearance, such as for... |
 | [ST_Perimeter](Measurement-Functions/ST_Perimeter.md) | This function 
calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... |
 | [ST_Perimeter2D](Measurement-Functions/ST_Perimeter2D.md) | This function 
calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... |
+| [ST_ShortestLine](Measurement-Functions/ST_ShortestLine.md) | Returns the 
shortest LineString between two geometries. The line starts on geom1 and ends 
on geom2. If either geometry is empty, returns null. |
 
 ## Geometry Processing
 
diff --git 
a/docs/api/snowflake/vector-data/Measurement-Functions/ST_ShortestLine.md 
b/docs/api/snowflake/vector-data/Measurement-Functions/ST_ShortestLine.md
new file mode 100644
index 0000000000..ae6b369aad
--- /dev/null
+++ b/docs/api/snowflake/vector-data/Measurement-Functions/ST_ShortestLine.md
@@ -0,0 +1,62 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_ShortestLine
+
+Introduction: Returns the shortest LineString between two geometries. The line 
starts on geom1 and ends on geom2. If either geometry is empty, the function 
returns null.
+
+![ST_ShortestLine](../../../../image/ST_ShortestLine/ST_ShortestLine.svg 
"ST_ShortestLine")
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+SQL Example:
+
+![ST_ShortestLine Point to 
Point](../../../../image/ST_ShortestLine/ST_ShortestLine_point_point.svg 
"ST_ShortestLine Point to Point")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 0)'),
+        ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+![ST_ShortestLine Point to 
LineString](../../../../image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg
 "ST_ShortestLine Point to LineString")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 1)'),
+        ST_GeomFromText('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 1, 0 0)
+```
diff --git a/docs/api/sql/Geometry-Functions.md 
b/docs/api/sql/Geometry-Functions.md
index a5076679ff..14ce382530 100644
--- a/docs/api/sql/Geometry-Functions.md
+++ b/docs/api/sql/Geometry-Functions.md
@@ -202,6 +202,7 @@ These functions compute measurements of distance, area, 
length, and angles.
 | [ST_MinimumClearanceLine](Measurement-Functions/ST_MinimumClearanceLine.md) 
| Geometry | This function returns a two-point LineString geometry representing 
the minimum clearance distance of the input geometry. If the input geometry 
does not have a defined minimum clearance, such as for... | v1.6.1 |
 | [ST_Perimeter](Measurement-Functions/ST_Perimeter.md) | Double | This 
function calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... | v1.7.0 |
 | [ST_Perimeter2D](Measurement-Functions/ST_Perimeter2D.md) | Double | This 
function calculates the 2D perimeter of a given geometry. It supports Polygon, 
MultiPolygon, and GeometryCollection geometries (as long as the 
GeometryCollection contains polygonal geometries)... | v1.7.1 |
+| [ST_ShortestLine](Measurement-Functions/ST_ShortestLine.md) | Geometry | 
Returns the shortest LineString between two geometries. The line starts on 
geom1 and ends on geom2. If either geometry is empty, returns null. | v1.9.0 |
 
 ## Geometry Processing
 
diff --git a/docs/api/sql/Measurement-Functions/ST_ShortestLine.md 
b/docs/api/sql/Measurement-Functions/ST_ShortestLine.md
new file mode 100644
index 0000000000..04631551c0
--- /dev/null
+++ b/docs/api/sql/Measurement-Functions/ST_ShortestLine.md
@@ -0,0 +1,64 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_ShortestLine
+
+Introduction: Returns the shortest LineString between two geometries. The line 
starts on geom1 and ends on geom2. If either geometry is empty, the function 
returns null.
+
+![ST_ShortestLine](../../../image/ST_ShortestLine/ST_ShortestLine.svg 
"ST_ShortestLine")
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+![ST_ShortestLine Point to 
Point](../../../image/ST_ShortestLine/ST_ShortestLine_point_point.svg 
"ST_ShortestLine Point to Point")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 0)'),
+        ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+![ST_ShortestLine Point to 
LineString](../../../image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg 
"ST_ShortestLine Point to LineString")
+
+```sql
+SELECT ST_ShortestLine(
+        ST_GeomFromText('POINT (0 1)'),
+        ST_GeomFromText('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 1, 0 0)
+```
diff --git a/docs/image/ST_ShortestLine/ST_ShortestLine.svg 
b/docs/image/ST_ShortestLine/ST_ShortestLine.svg
new file mode 100644
index 0000000000..e7294fce25
--- /dev/null
+++ b/docs/image/ST_ShortestLine/ST_ShortestLine.svg
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 500 300" width="500" 
height="300" style="background:#ffffff">
+  <defs>
+    <style>
+      text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+    </style>
+    <marker id="arrowG" markerWidth="8" markerHeight="6" refX="8" refY="3" 
orient="auto">
+      <path d="M0,0 L8,3 L0,6" fill="#2ecc71" />
+    </marker>
+    <marker id="arrowGrev" markerWidth="8" markerHeight="6" refX="0" refY="3" 
orient="auto">
+      <path d="M8,0 L0,3 L8,6" fill="#2ecc71" />
+    </marker>
+  </defs>
+  <text x="250" y="22" text-anchor="middle" font-size="16" font-weight="bold" 
fill="#333333">ST_ShortestLine</text>
+  <!-- Geometry A: polygon -->
+  <path d="M 100 200 L 200 200 L 200 100 L 100 100 L 100 200 Z" 
fill="rgba(74,144,217,0.15)" stroke="#4a90d9" stroke-width="2.5" 
fill-rule="evenodd" />
+  <text x="150" y="160" text-anchor="middle" font-size="15" font-weight="bold" 
fill="#4a90d9">A</text>
+  <!-- Geometry B: triangle -->
+  <path d="M 340 80 L 400 200 L 280 200 Z" fill="rgba(217,74,74,0.15)" 
stroke="#d94a4a" stroke-width="2.5" fill-rule="evenodd" />
+  <text x="340" y="175" text-anchor="middle" font-size="15" font-weight="bold" 
fill="#d94a4a">B</text>
+  <!-- Shortest line between them -->
+  <line x1="200" y1="164" x2="296" y2="164" stroke="#2ecc71" stroke-width="3" 
marker-start="url(#arrowGrev)" marker-end="url(#arrowG)" />
+  <!-- Endpoints -->
+  <circle cx="200" cy="164" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="200" cy="164" r="2.5" fill="#2ecc71" />
+  <circle cx="296" cy="164" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="296" cy="164" r="2.5" fill="#2ecc71" />
+  <!-- Caption -->
+  <text x="250" y="292" text-anchor="middle" font-size="13" fill="#333333" 
font-style="italic">Returns the shortest LineString connecting A and B</text>
+  <!-- Legend -->
+  <rect x="73" y="255" width="12" height="12" rx="2" fill="#4a90d9" 
stroke="#4a90d9" stroke-width="1" />
+  <text x="89" y="266" font-size="13" fill="#333333">Geometry A</text>
+  <rect x="183" y="255" width="12" height="12" rx="2" fill="#d94a4a" 
stroke="#d94a4a" stroke-width="1" />
+  <text x="199" y="266" font-size="13" fill="#333333">Geometry B</text>
+  <rect x="293" y="255" width="12" height="12" rx="2" fill="#2ecc71" 
stroke="#2ecc71" stroke-width="1" />
+  <text x="309" y="266" font-size="13" fill="#333333">Shortest line</text>
+</svg>
diff --git a/docs/image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg 
b/docs/image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg
new file mode 100644
index 0000000000..e8b9eb10c3
--- /dev/null
+++ b/docs/image/ST_ShortestLine/ST_ShortestLine_point_linestring.svg
@@ -0,0 +1,50 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 500 300" width="500" 
height="300" style="background:#ffffff">
+  <defs>
+    <style>
+      text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+    </style>
+    <marker id="arrowG" markerWidth="8" markerHeight="6" refX="8" refY="3" 
orient="auto">
+      <path d="M0,0 L8,3 L0,6" fill="#2ecc71" />
+    </marker>
+    <marker id="arrowGrev" markerWidth="8" markerHeight="6" refX="0" refY="3" 
orient="auto">
+      <path d="M8,0 L0,3 L8,6" fill="#2ecc71" />
+    </marker>
+  </defs>
+  <text x="250" y="22" text-anchor="middle" font-size="16" font-weight="bold" 
fill="#333333">ST_ShortestLine — Point to LineString</text>
+  <!-- Point A at (0,1) mapped to canvas ~(80,110) -->
+  <circle cx="80" cy="110" r="7" fill="rgba(74,144,217,0.3)" stroke="#4a90d9" 
stroke-width="2.5" />
+  <circle cx="80" cy="110" r="3" fill="#4a90d9" />
+  <text x="80" y="95" text-anchor="middle" font-size="13" fill="#4a90d9">POINT 
(0 1)</text>
+  <!-- LineString B from (0,0) to (5,0) mapped to canvas (80,200) to (480,200) 
-->
+  <polyline points="80,200 160,200 240,200 320,200 400,200 480,200" 
fill="none" stroke="#d94a4a" stroke-width="2.5" stroke-linecap="round" />
+  <!-- Vertices of linestring -->
+  <circle cx="80" cy="200" r="3" fill="#d94a4a" />
+  <circle cx="160" cy="200" r="3" fill="#d94a4a" />
+  <circle cx="240" cy="200" r="3" fill="#d94a4a" />
+  <circle cx="320" cy="200" r="3" fill="#d94a4a" />
+  <circle cx="400" cy="200" r="3" fill="#d94a4a" />
+  <circle cx="480" cy="200" r="3" fill="#d94a4a" />
+  <!-- Vertex labels -->
+  <text x="80" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(0,0)</text>
+  <text x="160" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(1,0)</text>
+  <text x="240" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(2,0)</text>
+  <text x="320" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(3,0)</text>
+  <text x="400" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(4,0)</text>
+  <text x="480" y="222" text-anchor="middle" font-size="11" 
fill="#d94a4a">(5,0)</text>
+  <!-- Shortest line from (0,1) to (0,0) -->
+  <line x1="80" y1="110" x2="80" y2="200" stroke="#2ecc71" stroke-width="3" 
marker-start="url(#arrowGrev)" marker-end="url(#arrowG)" />
+  <!-- Endpoints -->
+  <circle cx="80" cy="110" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="80" cy="110" r="2.5" fill="#2ecc71" />
+  <circle cx="80" cy="200" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="80" cy="200" r="2.5" fill="#2ecc71" />
+  <!-- Caption -->
+  <text x="250" y="292" text-anchor="middle" font-size="13" fill="#333333" 
font-style="italic">LINESTRING (0 1, 0 0)</text>
+  <!-- Legend -->
+  <rect x="73" y="255" width="12" height="12" rx="2" fill="#4a90d9" 
stroke="#4a90d9" stroke-width="1" />
+  <text x="89" y="266" font-size="13" fill="#333333">Point A</text>
+  <rect x="183" y="255" width="12" height="12" rx="2" fill="#d94a4a" 
stroke="#d94a4a" stroke-width="1" />
+  <text x="199" y="266" font-size="13" fill="#333333">LineString B</text>
+  <rect x="323" y="255" width="12" height="12" rx="2" fill="#2ecc71" 
stroke="#2ecc71" stroke-width="1" />
+  <text x="339" y="266" font-size="13" fill="#333333">Shortest line</text>
+</svg>
diff --git a/docs/image/ST_ShortestLine/ST_ShortestLine_point_point.svg 
b/docs/image/ST_ShortestLine/ST_ShortestLine_point_point.svg
new file mode 100644
index 0000000000..bbef32af64
--- /dev/null
+++ b/docs/image/ST_ShortestLine/ST_ShortestLine_point_point.svg
@@ -0,0 +1,38 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 500 300" width="500" 
height="300" style="background:#ffffff">
+  <defs>
+    <style>
+      text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+    </style>
+    <marker id="arrowG" markerWidth="8" markerHeight="6" refX="8" refY="3" 
orient="auto">
+      <path d="M0,0 L8,3 L0,6" fill="#2ecc71" />
+    </marker>
+    <marker id="arrowGrev" markerWidth="8" markerHeight="6" refX="0" refY="3" 
orient="auto">
+      <path d="M8,0 L0,3 L8,6" fill="#2ecc71" />
+    </marker>
+  </defs>
+  <text x="250" y="22" text-anchor="middle" font-size="16" font-weight="bold" 
fill="#333333">ST_ShortestLine — Point to Point</text>
+  <!-- Point A at (0,0) mapped to canvas ~(120,200) -->
+  <circle cx="120" cy="200" r="7" fill="rgba(74,144,217,0.3)" stroke="#4a90d9" 
stroke-width="2.5" />
+  <circle cx="120" cy="200" r="3" fill="#4a90d9" />
+  <text x="120" y="230" text-anchor="middle" font-size="13" 
fill="#4a90d9">POINT (0 0)</text>
+  <!-- Point B at (3,4) mapped to canvas ~(360,80) -->
+  <circle cx="360" cy="80" r="7" fill="rgba(217,74,74,0.3)" stroke="#d94a4a" 
stroke-width="2.5" />
+  <circle cx="360" cy="80" r="3" fill="#d94a4a" />
+  <text x="360" y="64" text-anchor="middle" font-size="13" 
fill="#d94a4a">POINT (3 4)</text>
+  <!-- Shortest line -->
+  <line x1="120" y1="200" x2="360" y2="80" stroke="#2ecc71" stroke-width="3" 
marker-start="url(#arrowGrev)" marker-end="url(#arrowG)" />
+  <!-- Endpoints -->
+  <circle cx="120" cy="200" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="120" cy="200" r="2.5" fill="#2ecc71" />
+  <circle cx="360" cy="80" r="5" fill="white" stroke="#2ecc71" 
stroke-width="2.5" />
+  <circle cx="360" cy="80" r="2.5" fill="#2ecc71" />
+  <!-- Caption -->
+  <text x="250" y="292" text-anchor="middle" font-size="13" fill="#333333" 
font-style="italic">LINESTRING (0 0, 3 4)</text>
+  <!-- Legend -->
+  <rect x="73" y="255" width="12" height="12" rx="2" fill="#4a90d9" 
stroke="#4a90d9" stroke-width="1" />
+  <text x="89" y="266" font-size="13" fill="#333333">Point A</text>
+  <rect x="183" y="255" width="12" height="12" rx="2" fill="#d94a4a" 
stroke="#d94a4a" stroke-width="1" />
+  <text x="199" y="266" font-size="13" fill="#333333">Point B</text>
+  <rect x="293" y="255" width="12" height="12" rx="2" fill="#2ecc71" 
stroke="#2ecc71" stroke-width="1" />
+  <text x="309" y="266" font-size="13" fill="#333333">Shortest line</text>
+</svg>
diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java 
b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 2d3646c43c..a10d9f1577 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -69,6 +69,7 @@ public class Catalog {
       new Functions.ST_Buffer(),
       new Functions.ST_BestSRID(),
       new Functions.ST_ClosestPoint(),
+      new Functions.ST_ShortestLine(),
       new Functions.ST_Centroid(),
       new Functions.ST_Collect(),
       new Functions.ST_CollectionExtract(),
diff --git 
a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java 
b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index 635cf86a2d..cf5e5267c7 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -287,6 +287,28 @@ public class Functions {
     }
   }
 
+  public static class ST_ShortestLine extends ScalarFunction {
+    @DataTypeHint(
+        value = "RAW",
+        rawSerializer = GeometryTypeSerializer.class,
+        bridgedTo = Geometry.class)
+    public Geometry eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = GeometryTypeSerializer.class,
+                bridgedTo = Geometry.class)
+            Object g1,
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = GeometryTypeSerializer.class,
+                bridgedTo = Geometry.class)
+            Object g2) {
+      Geometry geom1 = (Geometry) g1;
+      Geometry geom2 = (Geometry) g2;
+      return org.apache.sedona.common.Functions.shortestLine(geom1, geom2);
+    }
+  }
+
   public static class ST_Centroid extends ScalarFunction {
     @DataTypeHint(
         value = "RAW",
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java 
b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index 078ad9baf5..f0d7ffa7f1 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -241,6 +241,16 @@ public class FunctionTest extends TestBase {
     assertEquals("POINT (160 40)", result.toString());
   }
 
+  @Test
+  public void testShortestLine() {
+    Table table =
+        tableEnv.sqlQuery(
+            "SELECT ST_GeomFromWKT('POINT (0 0)') AS g1, ST_GeomFromWKT('POINT 
(3 4)') as g2");
+    table = table.select(call(Functions.ST_ShortestLine.class.getSimpleName(), 
$("g1"), $("g2")));
+    Geometry result = (Geometry) first(table).getField(0);
+    assertEquals("LINESTRING (0 0, 3 4)", result.toString());
+  }
+
   @Test
   public void testCentroid() {
     Table polygonTable =
diff --git a/python/sedona/spark/sql/st_functions.py 
b/python/sedona/spark/sql/st_functions.py
index 0a52d8c4c2..f32d57e7ad 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -471,6 +471,20 @@ def ST_ClosestPoint(a: ColumnOrName, b: ColumnOrName) -> 
Column:
     return _call_st_function("ST_ClosestPoint", (a, b))
 
 
+@validate_argument_types
+def ST_ShortestLine(a: ColumnOrName, b: ColumnOrName) -> Column:
+    """Return the shortest line between two geometries.
+
+    :param a: One geometry column.
+    :type a: ColumnOrName
+    :param b: Other geometry column.
+    :type b: ColumnOrName
+    :return: Shortest LineString connecting the two geometries as a geometry 
column.
+    :rtype: Column
+    """
+    return _call_st_function("ST_ShortestLine", (a, b))
+
+
 @validate_argument_types
 def ST_ConcaveHull(
     geometry: ColumnOrName,
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 81dcd5055a..920550a1cf 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -444,6 +444,16 @@ test_configurations = [
         "",
         "POINT (0 1)",
     ),
+    (
+        stf.ST_ShortestLine,
+        (
+            "point",
+            "line",
+        ),
+        "point_and_line",
+        "",
+        "LINESTRING (0 1, 0 0)",
+    ),
     (
         stf.ST_CollectionExtract,
         ("geom",),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index 031978032d..48a24d0ce7 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1966,6 +1966,23 @@ class TestPredicateJoin(TestBase):
         actual = actual_df.take(1)[0][0]
         assert expected == actual
 
+    def test_st_shortest_line(self):
+        expected = "LINESTRING (0 1, 0 0)"
+        actual_df = self.spark.sql(
+            "select ST_AsText(ST_ShortestLine(ST_GeomFromText('POINT (0 1)'), "
+            "ST_GeomFromText('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)')))"
+        )
+        actual = actual_df.take(1)[0][0]
+        assert expected == actual
+
+    def test_st_shortest_line_empty(self):
+        actual_df = self.spark.sql(
+            "select ST_ShortestLine(ST_GeomFromText('POINT (0 1)'), "
+            "ST_GeomFromText('GEOMETRYCOLLECTION EMPTY'))"
+        )
+        actual = actual_df.take(1)[0][0]
+        assert actual is None
+
     def test_st_collect_on_array_type(self):
         # given
         geometry_df = self.spark.createDataFrame(
diff --git 
a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala 
b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 815d2c6eb2..c931694db4 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -136,6 +136,7 @@ object Catalog extends AbstractCatalog with Logging {
     function[ST_StartPoint](),
     function[ST_Snap](),
     function[ST_ClosestPoint](),
+    function[ST_ShortestLine](),
     function[ST_Boundary](),
     function[ST_HasZ](),
     function[ST_HasM](),
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index 0fab6060cc..0925dfb104 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1004,6 +1004,14 @@ private[apache] case class 
ST_ClosestPoint(inputExpressions: Seq[Expression])
   }
 }
 
+private[apache] case class ST_ShortestLine(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.shortestLine _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 private[apache] case class ST_IsPolygonCW(inputExpressions: Seq[Expression])
     extends InferredExpression(Functions.isPolygonCW _) {
   protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index f79fc78be5..07f2e78d3b 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -156,6 +156,9 @@ object st_functions {
   def ST_ClosestPoint(a: Column, b: Column): Column = 
wrapExpression[ST_ClosestPoint](a, b)
   def ST_ClosestPoint(a: String, b: String): Column = 
wrapExpression[ST_ClosestPoint](a, b)
 
+  def ST_ShortestLine(a: Column, b: Column): Column = 
wrapExpression[ST_ShortestLine](a, b)
+  def ST_ShortestLine(a: String, b: String): Column = 
wrapExpression[ST_ShortestLine](a, b)
+
   def ST_Collect(geoms: Column): Column = wrapExpression[ST_Collect](geoms)
   def ST_Collect(geoms: String): Column = wrapExpression[ST_Collect](geoms)
   def ST_Collect(geoms: Any*): Column = wrapVarArgExpression[ST_Collect](geoms)
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
index 749a1be48a..b9e5b7b29e 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
@@ -74,6 +74,7 @@ class PreserveSRIDSuite extends TestBaseScala with 
TableDrivenPropertyChecks {
       ("ST_RemoveRepeatedPoints(geom3, 1)", 1000),
       ("ST_SetPoint(geom3, 1, ST_Point(0.5, 0.5))", 1000),
       ("ST_ClosestPoint(geom1, geom2)", 1000),
+      ("ST_ShortestLine(geom1, geom2)", 1000),
       ("ST_FlipCoordinates(geom1)", 1000),
       ("ST_SubDivide(geom4, 5)", 1000),
       ("ST_Segmentize(geom4, 0.1)", 1000),
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index d31da962dc..f10f36e38a 100644
--- 
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -1619,6 +1619,14 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertEquals(expected, actual)
     }
 
+    it("Passed ST_ShortestLine") {
+      val polyDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT('POINT (0 1)') as g1, 
ST_GeomFromWKT('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)') as g2")
+      val df = polyDf.select(ST_ShortestLine("g1", "g2"))
+      val actual = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
+      assertEquals("LINESTRING (0 1, 0 0)", actual)
+    }
+
     it("Passed ST_AsEWKT") {
       val baseDf = sparkSession.sql("SELECT ST_SetSRID(ST_Point(0.0, 0.0), 
4326) AS point")
       val df = baseDf.select(ST_AsEWKT("point"))
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 377e598b48..8d311bae90 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -3023,6 +3023,30 @@ class functionTestScala
     }
   }
 
+  it("should pass ST_ShortestLine") {
+    // Point to point
+    var df =
+      sparkSession.sql(
+        "SELECT ST_ShortestLine(ST_GeomFromWKT('POINT (0 0)'), 
ST_GeomFromWKT('POINT (3 4)'))")
+    var actual = df.take(1)(0).get(0).asInstanceOf[Geometry].toText
+    assertEquals("LINESTRING (0 0, 3 4)", actual)
+
+    // Point to linestring — first coordinate should be the point
+    df = sparkSession.sql(
+      "SELECT ST_ShortestLine(ST_GeomFromWKT('POINT (160 40)'), 
ST_GeomFromWKT('LINESTRING (10 30, 50 50, 30 110, 70 90, 180 140, 130 190)'))")
+    val result = df.take(1)(0).get(0).asInstanceOf[Geometry]
+    assertEquals("LineString", result.getGeometryType)
+    assertEquals(160.0, result.getCoordinates()(0).x, 1e-6)
+    assertEquals(40.0, result.getCoordinates()(0).y, 1e-6)
+  }
+
+  it("should return null for ST_ShortestLine with empty geometry") {
+    val df = sparkSession.sql(
+      "SELECT ST_ShortestLine(ST_GeomFromWKT('POINT (0 0)'), 
ST_GeomFromWKT('GEOMETRYCOLLECTION EMPTY'))")
+    val result = df.take(1)(0).get(0)
+    assert(result == null)
+  }
+
   it("Should pass ST_AreaSpheroid") {
     val geomTestCases = Map(
       ("'POINT (-0.56 51.3168)'") -> "0.0",


Reply via email to