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 1909171502 [GH-2552] Add ST_OrientedEnvelope (#2553)
1909171502 is described below

commit 1909171502fe16cd3d7dec6128b9b29e372819da
Author: Joonas Pessi <[email protected]>
AuthorDate: Sun Dec 7 02:40:33 2025 +0200

    [GH-2552] Add ST_OrientedEnvelope (#2553)
---
 .../java/org/apache/sedona/common/Functions.java   |  5 ++
 .../org/apache/sedona/common/FunctionsTest.java    | 70 ++++++++++++++++++++++
 docs/api/flink/Function.md                         | 20 +++++++
 docs/api/snowflake/vector-data/Function.md         | 14 +++++
 docs/api/sql/Function.md                           | 20 +++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../apache/sedona/flink/expressions/Functions.java | 10 ++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 10 ++++
 python/sedona/spark/sql/st_functions.py            | 12 ++++
 python/tests/sql/test_dataframe_api.py             | 12 ++++
 python/tests/sql/test_function.py                  | 11 ++++
 .../sedona/snowflake/snowsql/TestFunctions.java    |  8 +++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  |  8 +++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  |  5 ++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    |  9 +++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  1 +
 .../sql/sedona_sql/expressions/Functions.scala     |  8 +++
 .../sql/sedona_sql/expressions/st_functions.scala  |  5 ++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |  9 +++
 .../org/apache/sedona/sql/functionTestScala.scala  | 17 ++++++
 20 files changed, 255 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 0976316be9..0be88a9b1b 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -36,6 +36,7 @@ import org.apache.sedona.common.sphere.Spheroid;
 import org.apache.sedona.common.subDivide.GeometrySubDivider;
 import org.apache.sedona.common.utils.*;
 import org.locationtech.jts.algorithm.Angle;
+import org.locationtech.jts.algorithm.MinimumAreaRectangle;
 import org.locationtech.jts.algorithm.MinimumBoundingCircle;
 import org.locationtech.jts.algorithm.Orientation;
 import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
@@ -1227,6 +1228,10 @@ public class Functions {
     return circle;
   }
 
+  public static Geometry orientedEnvelope(Geometry geometry) {
+    return MinimumAreaRectangle.getMinimumRectangle(geometry);
+  }
+
   public static InscribedCircle maximumInscribedCircle(Geometry geometry) {
     // Calculating the tolerance
     Envelope envelope = geometry.getEnvelopeInternal();
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 71983f7344..8098944865 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -756,6 +756,76 @@ public class FunctionsTest extends TestBase {
     assertEquals(3857, centroid.getSRID());
   }
 
+  @Test
+  public void orientedEnvelope() throws ParseException {
+    Geometry axisAlignedRect = Constructors.geomFromWKT("POLYGON ((0 0, 4 0, 4 
2, 0 2, 0 0))", 0);
+    String actual = 
Functions.asWKT(Functions.orientedEnvelope(axisAlignedRect));
+    String expected = "POLYGON ((0 0, 0 2, 4 2, 4 0, 0 0))";
+    assertEquals(expected, actual);
+
+    Geometry rotatedSquare = Constructors.geomFromWKT("POLYGON ((1 0, 2 1, 1 
2, 0 1, 1 0))", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(rotatedSquare));
+    expected = "POLYGON ((1 0, 0 1, 1 2, 2 1, 1 0))";
+    assertEquals(expected, actual);
+
+    Geometry diagonalPolygon = Constructors.geomFromWKT("POLYGON ((0 0, 1 0, 5 
4, 4 4, 0 0))", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(diagonalPolygon));
+    expected = "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))";
+    assertEquals(expected, actual);
+
+    Geometry narrowRect = Constructors.geomFromWKT("POLYGON ((0 0, 10 0, 10 1, 
0 1, 0 0))", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(narrowRect));
+    expected = "POLYGON ((0 0, 0 1, 10 1, 10 0, 0 0))";
+    assertEquals(expected, actual);
+
+    Geometry triangle = Constructors.geomFromWKT("POLYGON ((0 0, 4 0, 2 3, 0 
0))", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(triangle));
+    expected = "POLYGON ((4 0, 0 0, 0 3, 4 3, 4 0))";
+    assertEquals(expected, actual);
+
+    Geometry irregularPolygon =
+        Constructors.geomFromWKT("POLYGON ((0 0, 3 1, 5 0, 4 4, 1 3, 0 0))", 
0);
+    actual =
+        
Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(irregularPolygon),
 2));
+    expected = "POLYGON ((5 0, 0.29 -1.18, -0.71 2.82, 4 4, 5 0))";
+    assertEquals(expected, actual);
+
+    Geometry point = Constructors.geomFromWKT("POINT (1 2)", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(point));
+    expected = "POINT (1 2)";
+    assertEquals(expected, actual);
+
+    Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0)", 0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(line));
+    expected = "LINESTRING (0 0, 10 0)";
+    assertEquals(expected, actual);
+
+    Geometry diagonalLine = Constructors.geomFromWKT("LINESTRING (0 0, 5 5)", 
0);
+    actual = Functions.asWKT(Functions.orientedEnvelope(diagonalLine));
+    expected = "LINESTRING (0 0, 5 5)";
+    assertEquals(expected, actual);
+
+    Geometry empty = Constructors.geomFromWKT("POLYGON EMPTY", 0);
+    Geometry orientedEmpty = Functions.orientedEnvelope(empty);
+    assertTrue(orientedEmpty.isEmpty());
+
+    Geometry geomWithSRID = Constructors.geomFromWKT("POLYGON ((0 0, 1 0, 1 1, 
0 1, 0 0))", 4326);
+    Geometry orientedWithSRID = Functions.orientedEnvelope(geomWithSRID);
+    assertEquals(4326, orientedWithSRID.getSRID());
+
+    Geometry multiPoint = Constructors.geomFromWKT("MULTIPOINT ((0 0), (-1 
-1), (3 2))", 0);
+    actual = 
Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(multiPoint),
 2));
+    expected = "POLYGON ((-1 -1, -1.12 -0.84, 2.88 2.16, 3 2, -1 -1))";
+    assertEquals(expected, actual);
+
+    Geometry linestring = Constructors.geomFromWKT("LINESTRING (55 75, 125 
150)", 0);
+    Geometry pointGeom = Constructors.geomFromWKT("POINT (20 80)", 0);
+    Geometry collection = linestring.union(pointGeom);
+    actual = 
Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(collection),
 2));
+    expected = "POLYGON ((125 150, 138.08 130.38, 33.08 60.38, 20 80, 125 
150))";
+    assertEquals(expected, actual);
+  }
+
   @Test
   public void getGoogleS2CellIDsPoint() {
     Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 2));
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 98a7dc1ba1..d74552617f 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -1310,6 +1310,26 @@ Output:
 POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
 ```
 
+## ST_OrientedEnvelope
+
+Introduction: Returns the minimum-area rotated rectangle enclosing a geometry. 
The rectangle may be rotated relative to the coordinate axes. Degenerate inputs 
may result in a Point or LineString being returned.
+
+Format: `ST_OrientedEnvelope(geom: Geometry)`
+
+Since: `v1.8.1`
+
+Example:
+
+```sql
+SELECT ST_OrientedEnvelope(ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 4, 0 
0))'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))
+```
+
 ## ST_Expand
 
 Introduction: Returns a geometry expanded from the bounding box of the input. 
The expansion can be specified in two ways:
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index 14d9ca417e..28eb433c3b 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -1040,6 +1040,20 @@ SELECT ST_Envelope(polygondf.countyshape)
 FROM polygondf
 ```
 
+## ST_OrientedEnvelope
+
+Introduction: Returns the minimum-area rotated rectangle enclosing a geometry. 
The rectangle may be rotated relative to the coordinate axes. Degenerate inputs 
may result in a Point or LineString being returned.
+
+Format: `ST_OrientedEnvelope(geometry: Geometry)`
+
+SQL example:
+
+```sql
+SELECT ST_OrientedEnvelope(ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 4, 0 
0))'))
+```
+
+Output: `POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))`
+
 ## ST_Expand
 
 Introduction: Returns a geometry expanded from the bounding box of the input. 
The expansion can be specified in two ways:
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 447707c329..9c61520f6d 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -3490,6 +3490,26 @@ Output:
 3
 ```
 
+## ST_OrientedEnvelope
+
+Introduction: Returns the minimum-area rotated rectangle enclosing a geometry. 
The rectangle may be rotated relative to the coordinate axes. Degenerate inputs 
may result in a Point or LineString being returned.
+
+Format: `ST_OrientedEnvelope(geom: Geometry)`
+
+Since: `v1.8.1`
+
+SQL Example
+
+```sql
+SELECT ST_OrientedEnvelope(ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 4, 0 
0))'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))
+```
+
 ## ST_Perimeter
 
 Introduction: 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). For other types, it 
returns 0. To measure lines, use [ST_Length](#st_length).
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 af62005483..dcb593e9c3 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -73,6 +73,7 @@ public class Catalog {
       new Functions.ST_CrossesDateLine(),
       new Functions.ST_Expand(),
       new Functions.ST_Envelope(),
+      new Functions.ST_OrientedEnvelope(),
       new Functions.ST_Difference(),
       new Functions.ST_Dimension(),
       new Functions.ST_Distance(),
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 211d7db796..39fb3da564 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
@@ -289,6 +289,16 @@ public class Functions {
     }
   }
 
+  public static class ST_OrientedEnvelope extends ScalarFunction {
+    @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+    public Geometry eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+            Object o) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.orientedEnvelope(geom);
+    }
+  }
+
   public static class ST_Expand extends ScalarFunction {
     @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
     public Geometry eval(
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 afa520af14..d1095c98e7 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -409,6 +409,16 @@ public class FunctionTest extends TestBase {
         first(linestringTable).getField(0).toString());
   }
 
+  @Test
+  public void testOrientedEnvelope() {
+    Table polygonTable =
+        tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 
4, 0 0))') as geom");
+    Table resultTable =
+        
polygonTable.select(call(Functions.ST_OrientedEnvelope.class.getSimpleName(), 
$("geom")));
+    Geometry result = (Geometry) first(resultTable).getField(0);
+    assertEquals("POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))", 
result.toString());
+  }
+
   @Test
   public void testExpand() {
     Table baseTable =
diff --git a/python/sedona/spark/sql/st_functions.py 
b/python/sedona/spark/sql/st_functions.py
index 69a8c5ffef..bc6b2d5e20 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -1595,6 +1595,18 @@ def ST_NumInteriorRing(geometry: ColumnOrName) -> Column:
     return _call_st_function("ST_NumInteriorRing", geometry)
 
 
+@validate_argument_types
+def ST_OrientedEnvelope(geometry: ColumnOrName) -> Column:
+    """Return the minimum rotated rectangle enclosing a geometry.
+
+    :param geometry: Geometry column to compute oriented envelope for.
+    :type geometry: ColumnOrName
+    :return: Minimum area rotated rectangle as a geometry column.
+    :rtype: Column
+    """
+    return _call_st_function("ST_OrientedEnvelope", geometry)
+
+
 @validate_argument_types
 def ST_PointN(geometry: ColumnOrName, n: Union[ColumnOrName, int]) -> Column:
     """Get the n-th point (starts at 1) for a geometry.
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 939411b5c1..3ea7ab2076 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -867,6 +867,13 @@ test_configurations = [
     (stf.ST_NumInteriorRings, ("geom",), "geom_with_hole", "", 1),
     (stf.ST_NumInteriorRing, ("geom",), "geom_with_hole", "", 1),
     (stf.ST_NumPoints, ("line",), "linestring_geom", "", 6),
+    (
+        stf.ST_OrientedEnvelope,
+        ("geom",),
+        "diagonal_geom",
+        "",
+        "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))",
+    ),
     (stf.ST_PointN, ("line", 2), "linestring_geom", "", "POINT (1 0)"),
     (stf.ST_PointOnSurface, ("line",), "linestring_geom", "", "POINT (2 0)"),
     (
@@ -1378,6 +1385,7 @@ wrong_type_configurations = [
     (stf.ST_MinimumClearanceLine, (None,)),
     (stf.ST_MinimumBoundingCircle, (None,)),
     (stf.ST_MinimumBoundingRadius, (None,)),
+    (stf.ST_OrientedEnvelope, (None,)),
     (stf.ST_Multi, (None,)),
     (stf.ST_Normalize, (None,)),
     (stf.ST_NPoints, (None,)),
@@ -1629,6 +1637,10 @@ class TestDataFrameAPI(TestBase):
             return TestDataFrameAPI.spark.sql(
                 "SELECT ST_GeomFromWKT('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))') 
AS geom"
             )
+        elif request.param == "diagonal_geom":
+            return TestDataFrameAPI.spark.sql(
+                "SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 4, 0 0))') 
AS geom"
+            )
         elif request.param == "four_points":
             return TestDataFrameAPI.spark.sql(
                 "SELECT ST_GeomFromWKT('POINT (0 0)') AS p1, 
ST_GeomFromWKT('POINT (1 1)') AS p2, ST_GeomFromWKT('POINT (1 0)') AS p3, 
ST_GeomFromWKT('POINT (6 2)') AS p4"
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index b7a2f88172..1c35ce417f 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -2529,3 +2529,14 @@ class TestPredicateJoin(TestBase):
         actual_with_max = actual_df_with_max.take(1)[0][0]
         assert actual_with_max is not None
         assert actual_with_max.geom_type == "MultiLineString"
+
+    def test_st_oriented_envelope(self):
+        actual = self.spark.sql(
+            "SELECT ST_AsText(ST_OrientedEnvelope(ST_GeomFromText('POLYGON ((0 
0, 1 0, 5 4, 4 4, 0 0))')))"
+        ).take(1)[0][0]
+        assert actual == "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))"
+
+        actual = self.spark.sql(
+            "SELECT ST_AsText(ST_OrientedEnvelope(ST_GeomFromText('POINT (1 
2)')))"
+        ).take(1)[0][0]
+        assert actual == "POINT (1 2)"
diff --git 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
index af36809f0b..20ab767fad 100644
--- 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
+++ 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
@@ -391,6 +391,14 @@ public class TestFunctions extends TestBase {
         "POLYGON ((1 2, 1 4, 3 4, 3 2, 1 2))");
   }
 
+  @Test
+  public void test_ST_OrientedEnvelope() {
+    registerUDF("ST_OrientedEnvelope", byte[].class);
+    verifySqlSingleRes(
+        "select 
sedona.ST_AsText(sedona.ST_OrientedEnvelope(sedona.ST_GeomFromText('POLYGON ((0 
0, 1 0, 5 4, 4 4, 0 0))')))",
+        "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))");
+  }
+
   @Test
   public void test_ST_Expand() {
     registerUDF("ST_Expand", byte[].class, double.class);
diff --git 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
index e2efb34843..425a5e288e 100644
--- 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
+++ 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
@@ -375,6 +375,14 @@ public class TestFunctionsV2 extends TestBase {
         "POLYGON((1 2,1 4,3 4,3 2,1 2))");
   }
 
+  @Test
+  public void test_ST_OrientedEnvelope() {
+    registerUDFV2("ST_OrientedEnvelope", String.class);
+    verifySqlSingleRes(
+        "select 
ST_AsText(sedona.ST_OrientedEnvelope(ST_GeometryFromWKT('POLYGON ((0 0, 1 0, 5 
4, 4 4, 0 0))')))",
+        "POLYGON((0 0,4.5 4.5,5 4,0.5 -0.5,0 0))");
+  }
+
   @Test
   public void test_ST_Expand() {
     registerUDFV2("ST_Expand", String.class, double.class);
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
index 2832c6accd..3e2d0095a6 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
@@ -373,6 +373,11 @@ public class UDFs {
     return 
GeometrySerde.serialize(Functions.envelope(GeometrySerde.deserialize(geometry)));
   }
 
+  @UDFAnnotations.ParamMeta(argNames = {"geometry"})
+  public static byte[] ST_OrientedEnvelope(byte[] geometry) {
+    return 
GeometrySerde.serialize(Functions.orientedEnvelope(GeometrySerde.deserialize(geometry)));
+  }
+
   @UDFAnnotations.ParamMeta(argNames = {"geometry", "uniformDelta"})
   public static byte[] ST_Expand(byte[] geometry, double uniformDelta) {
     return GeometrySerde.serialize(
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
index 0f636f1f92..5d545f4b6c 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
@@ -519,6 +519,15 @@ public class UDFsV2 {
     return 
GeometrySerde.serGeoJson(Functions.envelope(GeometrySerde.deserGeoJson(geometry)));
   }
 
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry"},
+      argTypes = {"Geometry"},
+      returnTypes = "Geometry")
+  public static String ST_OrientedEnvelope(String geometry) {
+    return GeometrySerde.serGeoJson(
+        Functions.orientedEnvelope(GeometrySerde.deserGeoJson(geometry)));
+  }
+
   @UDFAnnotations.ParamMeta(
       argNames = {"geometry", "uniformDelta"},
       argTypes = {"Geometry", "double"},
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 e584e666e3..8b5882a989 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
@@ -208,6 +208,7 @@ object Catalog extends AbstractCatalog with Logging {
     function[ST_XMin](),
     function[ST_BuildArea](),
     function[ST_OrderingEquals](),
+    function[ST_OrientedEnvelope](),
     function[ST_CollectionExtract](defaultArgs = null),
     function[ST_Normalize](),
     function[ST_LineFromMultiPoint](),
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 954fb350ad..54424510ff 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
@@ -701,6 +701,14 @@ private[apache] case class 
ST_MinimumBoundingCircle(inputExpressions: Seq[Expres
   }
 }
 
+private[apache] case class ST_OrientedEnvelope(inputExpressions: 
Seq[Expression])
+    extends InferredExpression(Functions.orientedEnvelope _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 private[apache] case class ST_HasZ(inputExpressions: Seq[Expression])
     extends InferredExpression(Functions.hasZ _) {
 
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 6fd00a6b63..f34a128307 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
@@ -504,6 +504,11 @@ object st_functions {
   def ST_MinimumBoundingRadius(geometry: String): Column =
     wrapExpression[ST_MinimumBoundingRadius](geometry)
 
+  def ST_OrientedEnvelope(geometry: Column): Column =
+    wrapExpression[ST_OrientedEnvelope](geometry)
+  def ST_OrientedEnvelope(geometry: String): Column =
+    wrapExpression[ST_OrientedEnvelope](geometry)
+
   def ST_IsPolygonCCW(geometry: Column): Column = 
wrapExpression[ST_IsPolygonCCW](geometry)
   def ST_IsPolygonCCW(geometry: String): Column = 
wrapExpression[ST_IsPolygonCCW](geometry)
 
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 81bd279c00..8b8c8ca20c 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
@@ -1491,6 +1491,15 @@ class dataFrameAPITestScala extends TestBaseScala {
       assert(actualRadius == expectedRadius)
     }
 
+    it("Passed ST_OrientedEnvelope") {
+      val baseDf =
+        sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 5 4, 4 4, 
0 0))') AS geom")
+      val df = baseDf.select(ST_OrientedEnvelope("geom"))
+      val actual = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
+      val expected = "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))"
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_LineSegments") {
       val baseDf = sparkSession.sql(
         "SELECT ST_GeomFromWKT('LINESTRING(120 140, 60 120, 30 20)') AS line, 
ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 0, 0 0))') AS poly")
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 d905cb424b..a095fc1bfa 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
@@ -2373,6 +2373,21 @@ class functionTestScala
       .toList should contain theSameElementsAs List(0, 1, 1)
   }
 
+  it("Should pass ST_OrientedEnvelope") {
+    val testCases = Seq(
+      ("POLYGON ((0 0, 4 0, 4 2, 0 2, 0 0))", "POLYGON ((0 0, 0 2, 4 2, 4 0, 0 
0))"),
+      ("POLYGON ((0 0, 1 0, 5 4, 4 4, 0 0))", "POLYGON ((0 0, 4.5 4.5, 5 4, 
0.5 -0.5, 0 0))"),
+      ("POINT (1 2)", "POINT (1 2)"))
+
+    testCases.foreach { case (input, expected) =>
+      val actual = sparkSession
+        .sql(s"SELECT 
ST_AsText(ST_OrientedEnvelope(ST_GeomFromWKT('$input')))")
+        .first()
+        .getString(0)
+      assert(expected.equals(actual), s"Input: $input, Expected: $expected, 
Actual: $actual")
+    }
+  }
+
   it("Should pass ST_LineSegments") {
     val baseDf = sparkSession.sql(
       "SELECT ST_GeomFromWKT('LINESTRING(120 140, 60 120, 30 20)') AS line, 
ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 0, 0 0))') AS poly")
@@ -2733,6 +2748,8 @@ class functionTestScala
     assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_MinimumBoundingRadius(null)")
     assert(functionDf.first().get(0) == null)
+    functionDf = sparkSession.sql("select ST_OrientedEnvelope(null)")
+    assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_LineSubstring(null, 0, 0)")
     assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_LineInterpolatePoint(null, 0)")

Reply via email to