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.
+
+
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_ShortestLine(
+ ST_GeomFromText('POINT (0 0)'),
+ ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+
+
+```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.
+
+
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_ShortestLine(
+ ST_GeomFromText('POINT (0 0)'),
+ ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+
+
+```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.
+
+
+
+Format: `ST_ShortestLine(geom1: Geometry, geom2: Geometry)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_ShortestLine(
+ ST_GeomFromText('POINT (0 0)'),
+ ST_GeomFromText('POINT (3 4)')
+)
+```
+
+Output:
+
+```
+LINESTRING (0 0, 3 4)
+```
+
+SQL Example:
+
+
+
+```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",