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 a473aef8f [SEDONA-617] Add ST_Rotate (#1497)
a473aef8f is described below

commit a473aef8fe38a6ab19d9b15d163edcea1fe78496
Author: Pranav Toggi <[email protected]>
AuthorDate: Thu Jun 27 02:52:17 2024 -0400

    [SEDONA-617] Add ST_Rotate (#1497)
    
    * init
    
    * update scala test
    
    * add scala dataframeapi tests
    
    * add tests for flink
    
    * add tests for python
    
    * add tests for snowflake
    
    * add java tests
    
    * add docs
    
    * fix typo
    
    * fix python test
    
    * fix snowflake test
    
    * add ST_ReducePrecision for snowflake tests
    
    * fix tests
    
    * fix python implementation
    
    * Fix snowflake implementation
    
    * fix typo
    
    * Fix snowflake test
    
    * Fix snowflake test
    
    * Fix whitespaces in tests
    
    * fix docs
---
 .../java/org/apache/sedona/common/Functions.java   | 56 ++++++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    | 22 ++++++
 docs/api/flink/Function.md                         | 26 +++++++
 docs/api/snowflake/vector-data/Function.md         | 24 ++++++
 docs/api/sql/Function.md                           | 26 +++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../apache/sedona/flink/expressions/Functions.java | 34 ++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 45 +++++++++++
 python/sedona/sql/st_functions.py                  | 28 +++++++
 python/tests/sql/test_dataframe_api.py             |  3 +
 .../sedona/snowflake/snowsql/TestFunctions.java    | 16 ++++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  | 16 ++++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  | 18 +++++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    | 27 +++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  1 +
 .../sql/sedona_sql/expressions/Functions.scala     | 10 +++
 .../sql/sedona_sql/expressions/st_functions.scala  | 15 ++++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  | 19 +++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 90 ++++++++++++++++++++++
 19 files changed, 477 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 4c65c1023..ebedb1762 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -38,6 +38,7 @@ import 
org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
 import org.locationtech.jts.algorithm.hull.ConcaveHull;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.locationtech.jts.geom.util.AffineTransformation;
 import org.locationtech.jts.geom.util.GeometryFixer;
 import org.locationtech.jts.io.ByteOrderValues;
 import org.locationtech.jts.io.gml2.GMLWriter;
@@ -2101,4 +2102,59 @@ public class Functions {
     // Creating a MultiPoint from the extracted coordinates
     return GEOMETRY_FACTORY.createMultiPointFromCoords(coordinates);
   }
+
+  /**
+   * Rotates a geometry by a given angle in radians.
+   *
+   * @param geometry The input geometry to rotate.
+   * @param angle The angle in radians to rotate the geometry.
+   * @return The rotated geometry.
+   */
+  public static Geometry rotate(Geometry geometry, double angle) {
+    if (geometry == null || geometry.isEmpty()) {
+      return geometry;
+    }
+    AffineTransformation rotation = 
AffineTransformation.rotationInstance(angle);
+    return rotation.transform(geometry);
+  }
+
+  /**
+   * Rotates a geometry by a given angle in radians around a given origin 
point (x, y).
+   *
+   * @param geometry The input geometry to rotate.
+   * @param angle The angle in radians to rotate the geometry.
+   * @param originX The x coordinate of the origin point around which to 
rotate.
+   * @param originY The y coordinate of the origin point around which to 
rotate.
+   * @return The rotated geometry.
+   */
+  public static Geometry rotate(Geometry geometry, double angle, double 
originX, double originY) {
+    if (geometry == null || geometry.isEmpty()) {
+      return geometry;
+    }
+    AffineTransformation rotation = 
AffineTransformation.rotationInstance(angle, originX, originY);
+    return rotation.transform(geometry);
+  }
+
+  /**
+   * Rotates a geometry by a given angle in radians around a given origin 
point.
+   *
+   * @param geometry The input geometry to rotate.
+   * @param angle The angle in radians to rotate the geometry.
+   * @param pointOrigin The origin point around which to rotate.
+   * @return The rotated geometry.
+   * @throws IllegalArgumentException if the pointOrigin is not a Point 
geometry.
+   */
+  public static Geometry rotate(Geometry geometry, double angle, Geometry 
pointOrigin) {
+    if (geometry == null || geometry.isEmpty()) {
+      return geometry;
+    }
+    if (pointOrigin == null || pointOrigin.isEmpty() || !(pointOrigin 
instanceof Point)) {
+      throw new IllegalArgumentException("The origin must be a non-empty Point 
geometry.");
+    }
+    Point origin = (Point) pointOrigin;
+    double originX = origin.getX();
+    double originY = origin.getY();
+    AffineTransformation rotation = 
AffineTransformation.rotationInstance(angle, originX, originY);
+    return rotation.transform(geometry);
+  }
 }
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 1140c0629..3d63fb922 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -3630,4 +3630,26 @@ public class FunctionsTest extends TestBase {
     String result1 = Functions.asEWKT(Functions.points(geometry3D));
     assertEquals("MULTIPOINT Z((0 0 1), (1 1 2), (2 2 3), (0 0 1))", result1);
   }
+
+  @Test
+  public void rotate() throws ParseException {
+    Geometry lineString = Constructors.geomFromEWKT("LINESTRING (50 160, 50 
50, 100 50)");
+    String result = Functions.asEWKT(Functions.rotate(lineString, Math.PI));
+    assertEquals(
+        "LINESTRING (-50.00000000000002 -160, -50.00000000000001 
-49.99999999999999, -100 -49.999999999999986)",
+        result);
+
+    lineString = Constructors.geomFromEWKT("LINESTRING (0 0, 1 0, 1 1, 0 0)");
+    Geometry pointOrigin = Constructors.geomFromEWKT("POINT (0 0)");
+    result = Functions.asEWKT(Functions.rotate(lineString, 10, pointOrigin));
+    assertEquals(
+        "LINESTRING (0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0)",
+        result);
+
+    lineString = Constructors.geomFromEWKT("LINESTRING (0 0, 1 0, 1 1, 0 0)");
+    result = Functions.asEWKT(Functions.rotate(lineString, 10, 0, 0));
+    assertEquals(
+        "LINESTRING (0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0)",
+        result);
+  }
 }
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index ec1705f93..2a77036db 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -3084,6 +3084,32 @@ Output:
 LINESTRING(0 0, 1 0)
 ```
 
+## ST_Rotate
+
+Introduction: Rotates a geometry by a specified angle in radians 
counter-clockwise around a given origin point. The origin for rotation can be 
specified as either a POINT geometry or x and y coordinates. If the origin is 
not specified, the geometry is rotated around POINT(0 0).
+
+Formats;
+
+`ST_Rotate (geometry: Geometry, angle: Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, originX: Double, originY: 
Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, pointOrigin: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_Rotate(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 
10, 0, 0)
+```
+
+Output:
+
+```
+SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0))
+```
+
 ## ST_S2CellIDs
 
 Introduction: Cover the geometry with Google S2 Cells, return the 
corresponding cell IDs with the given level.
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index 47ada173c..c067f6a1f 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -2317,6 +2317,30 @@ Result:
 +---------------------------------------------------------------+
 ```
 
+## ST_Rotate
+
+Introduction: Rotates a geometry by a specified angle in radians 
counter-clockwise around a given origin point. The origin for rotation can be 
specified as either a POINT geometry or x and y coordinates. If the origin is 
not specified, the geometry is rotated around POINT(0 0).
+
+Formats;
+
+`ST_Rotate (geometry: Geometry, angle: Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, originX: Double, originY: 
Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, pointOrigin: Geometry)`
+
+SQL Example:
+
+```sql
+SELECT ST_Rotate(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 
10, 0, 0)
+```
+
+Output:
+
+```
+SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0))
+```
+
 ## ST_S2CellIDs
 
 Introduction: Cover the geometry with Google S2 Cells, return the 
corresponding cell IDs with the given level.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 941af5e72..f2e1e9afb 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -3164,6 +3164,32 @@ Output:
 LINESTRING (3 6, 2 4, 1 2, 0 0)
 ```
 
+## ST_Rotate
+
+Introduction: Rotates a geometry by a specified angle in radians 
counter-clockwise around a given origin point. The origin for rotation can be 
specified as either a POINT geometry or x and y coordinates. If the origin is 
not specified, the geometry is rotated around POINT(0 0).
+
+Formats;
+
+`ST_Rotate (geometry: Geometry, angle: Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, originX: Double, originY: 
Double)`
+
+`ST_Rotate (geometry: Geometry, angle: Double, pointOrigin: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_Rotate(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 
10, 0, 0)
+```
+
+Output:
+
+```
+SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0))
+```
+
 ## ST_S2CellIDs
 
 Introduction: Cover the geometry with Google S2 Cells, return the 
corresponding cell IDs with the given level.
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 165205c52..c043a85d0 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -100,6 +100,7 @@ public class Catalog {
       new Functions.ST_PointOnSurface(),
       new Functions.ST_ReducePrecision(),
       new Functions.ST_Reverse(),
+      new Functions.ST_Rotate(),
       new Functions.ST_GeometryN(),
       new Functions.ST_InteriorRingN(),
       new Functions.ST_PointN(),
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 20965a634..87df74658 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
@@ -1869,4 +1869,38 @@ public class Functions {
       return org.apache.sedona.common.Functions.isValidReason(geom, flag);
     }
   }
+
+  public static class ST_Rotate 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 o1,
+        @DataTypeHint(value = "Double") Double angle) {
+      Geometry geom1 = (Geometry) o1;
+      return org.apache.sedona.common.Functions.rotate(geom1, angle);
+    }
+
+    @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+    public Geometry eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+            Object o1,
+        @DataTypeHint(value = "Double") Double angle,
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+            Object o2) {
+      Geometry geom1 = (Geometry) o1;
+      Geometry geom2 = (Geometry) o2;
+      return org.apache.sedona.common.Functions.rotate(geom1, angle, geom2);
+    }
+
+    @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+    public Geometry eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+            Object o1,
+        @DataTypeHint(value = "Double") Double angle,
+        @DataTypeHint(value = "Double") Double originX,
+        @DataTypeHint(value = "Double") Double originY) {
+      Geometry geom1 = (Geometry) o1;
+      return org.apache.sedona.common.Functions.rotate(geom1, angle, originX, 
originY);
+    }
+  }
 }
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 96ffb15b8..c00957722 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -2505,4 +2505,49 @@ public class FunctionTest extends TestBase {
         esriValidityReason); // Expecting an error related to interior 
disconnection as per ESRI
     // standards
   }
+
+  @Test
+  public void testRotate() {
+    Table tbl =
+        tableEnv.sqlQuery(
+            "SELECT ST_GeomFromEWKT('POLYGON ((0 0, 2 0, 1 1, 2 2, 0 2, 1 1, 0 
0))') AS geom1, ST_GeomFromEWKT('POINT (2 0)') AS geom2");
+    String actual =
+        (String)
+            first(
+                    tbl.select(call(Functions.ST_Rotate.class.getSimpleName(), 
$("geom1"), Math.PI))
+                        .as("geom")
+                        
.select(call(Functions.ST_AsEWKT.class.getSimpleName(), $("geom"))))
+                .getField(0);
+    String expected =
+        "POLYGON ((0 0, -2 0.0000000000000002, -1.0000000000000002 
-0.9999999999999999, -2.0000000000000004 -1.9999999999999998, 
-0.0000000000000002 -2, -1.0000000000000002 -0.9999999999999999, 0 0))";
+    assertEquals(expected, actual);
+
+    actual =
+        (String)
+            first(
+                    tbl.select(
+                            call(
+                                Functions.ST_Rotate.class.getSimpleName(),
+                                $("geom1"),
+                                50,
+                                $("geom2")))
+                        .as("geom")
+                        
.select(call(Functions.ST_AsEWKT.class.getSimpleName(), $("geom"))))
+                .getField(0);
+    expected =
+        "POLYGON ((0.0700679430157733 0.5247497074078575, 2 0, 
1.2974088252118154 1.227340882196042, 2.5247497074078575 1.9299320569842267, 
0.5948176504236309 2.454681764392084, 1.2974088252118154 1.227340882196042, 
0.0700679430157733 0.5247497074078575))";
+    assertEquals(expected, actual);
+
+    actual =
+        (String)
+            first(
+                    tbl.select(
+                            call(Functions.ST_Rotate.class.getSimpleName(), 
$("geom1"), 50, 2, 0))
+                        .as("geom")
+                        
.select(call(Functions.ST_AsEWKT.class.getSimpleName(), $("geom"))))
+                .getField(0);
+    expected =
+        "POLYGON ((0.0700679430157733 0.5247497074078575, 2 0, 
1.2974088252118154 1.227340882196042, 2.5247497074078575 1.9299320569842267, 
0.5948176504236309 2.454681764392084, 1.2974088252118154 1.227340882196042, 
0.0700679430157733 0.5247497074078575))";
+    assertEquals(expected, actual);
+  }
 }
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index a8ca7aa44..8b5d1ef75 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1979,3 +1979,31 @@ def ST_IsCollection(geometry: ColumnOrName) -> Column:
     :rtype: Column
     """
     return _call_st_function("ST_IsCollection", geometry)
+
+
+@validate_argument_types
+def ST_Rotate(geometry: ColumnOrName, angle: Union[ColumnOrName, float], 
originX: Union[ColumnOrName, float] = None,
+              originY: Union[ColumnOrName, float] = None, pointOrigin: 
ColumnOrName = None) -> Column:
+    """Return a counter-clockwise rotated geometry along the specified origin.
+
+    :param geometry: Geometry column or name.
+    :type geometry: ColumnOrName
+    :param angle: Rotation angle in radians.
+    :type angle: Union[ColumnOrName, float]
+    :param originX: Optional x-coordinate of the origin.
+    :type originX: Union[ColumnOrName, float]
+    :param originY: Optional y-coordinate of the origin.
+    :type originY: Union[ColumnOrName, float]
+    :param pointOrigin: Optional origin point for rotation.
+    :type pointOrigin: ColumnOrName
+    :return: Returns the rotated geometry.
+    :rtype: Column
+    """
+    if pointOrigin is not None:
+        args = (geometry, angle, pointOrigin)
+    elif originX is not None and originY is not None:
+        args = (geometry, angle, originX, originY)
+    else:
+        args = (geometry, angle)
+
+    return _call_st_function("ST_Rotate", args)
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index c62b6eee2..1a97bb6ac 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -199,6 +199,8 @@ test_configurations = [
     (stf.ST_ReducePrecision, ("geom", 1), "precision_reduce_point", "", "POINT 
(0.1 0.2)"),
     (stf.ST_RemovePoint, ("line", 1), "linestring_geom", "", "LINESTRING (0 0, 
2 0, 3 0, 4 0, 5 0)"),
     (stf.ST_Reverse, ("line",), "linestring_geom", "", "LINESTRING (5 0, 4 0, 
3 0, 2 0, 1 0, 0 0)"),
+    (stf.ST_Rotate, ("line", 10.0), "linestring_geom", 
"ST_ReducePrecision(geom, 2)", "LINESTRING (0 0, -0.84 -0.54, -1.68 -1.09, 
-2.52 -1.63, -3.36 -2.18, -4.2 -2.72)"),
+    (stf.ST_Rotate, ("line", 10.0, 0.0, 0.0), "linestring_geom", 
"ST_ReducePrecision(geom, 2)", "LINESTRING (0 0, -0.84 -0.54, -1.68 -1.09, 
-2.52 -1.63, -3.36 -2.18, -4.2 -2.72)"),
     (stf.ST_S2CellIDs, ("point", 30), "point_geom", "", [1153451514845492609]),
     (stf.ST_S2ToGeom, (lambda: f.expr("array(1154047404513689600)"),), "null", 
"ST_ReducePrecision(geom[0], 5)", "POLYGON ((0 2.46041, 2.46041 2.46041, 
2.46041 0, 0 0, 0 2.46041))"),
     (stf.ST_SetPoint, ("line", 1, lambda: f.expr("ST_Point(1.0, 1.0)")), 
"linestring_geom", "", "LINESTRING (0 0, 1 1, 2 0, 3 0, 4 0, 5 0)"),
@@ -413,6 +415,7 @@ wrong_type_configurations = [
     (stf.ST_RemovePoint, ("", None)),
     (stf.ST_RemovePoint, ("", 1.0)),
     (stf.ST_Reverse, (None,)),
+    (stf.ST_Rotate, (None,None,)),
     (stf.ST_S2CellIDs, (None, 2)),
     (stf.ST_S2ToGeom, (None,)),
     (stf.ST_SetPoint, (None, 1, "")),
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 2e4bb0f55..c95ab35a3 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
@@ -1164,4 +1164,20 @@ public class TestFunctions extends TestBase {
         "SELECT 
sedona.ST_AsText(sedona.ST_Translate(sedona.ST_GeomFromText('GEOMETRYCOLLECTION(MULTIPOLYGON
 (((1 0 0, 1 1 0, 2 1 0, 2 0 0, 1 0 0)), ((1 2 0, 3 4 0, 3 5 0, 1 2 0))), 
POINT(1 1 1), LINESTRING EMPTY))'), 2, 2, 3))",
         "GEOMETRYCOLLECTION Z(MULTIPOLYGON Z(((3 2 3, 3 3 3, 4 3 3, 4 2 3, 3 2 
3)), ((3 4 3, 5 6 3, 5 7 3, 3 4 3))), POINT Z(3 3 4), LINESTRING ZEMPTY)");
   }
+
+  @Test
+  public void test_ST_Rotate() {
+    registerUDF("ST_Rotate", byte[].class, double.class);
+    verifySqlSingleRes(
+        "SELECT 
sedona.ST_AsText(sedona.ST_Rotate(sedona.ST_GeomFromWKT('LINESTRING (0 0, 1 0, 
1 1, 0 0)'), 10))",
+        "LINESTRING (0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0)");
+    registerUDF("ST_Rotate", byte[].class, double.class, byte[].class);
+    verifySqlSingleRes(
+        "SELECT 
sedona.ST_AsText(sedona.ST_Rotate(sedona.ST_GeomFromWKT('LINESTRING (0 0, 1 0, 
1 1, 0 0)'), 10, sedona.ST_GeomFromWKT('POINT (0 0)')))",
+        "LINESTRING (0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0)");
+    registerUDF("ST_Rotate", byte[].class, double.class, double.class, 
double.class);
+    verifySqlSingleRes(
+        "SELECT 
sedona.ST_AsText(sedona.ST_Rotate(sedona.ST_GeomFromWKT('LINESTRING (0 0, 1 0, 
1 1, 0 0)'), 10, 0, 0))",
+        "LINESTRING (0 0, -0.8390715290764524 -0.5440211108893698, 
-0.2950504181870827 -1.383092639965822, 0 0)");
+  }
 }
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 1972c9272..4060e9447 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
@@ -1119,4 +1119,20 @@ public class TestFunctionsV2 extends TestBase {
         "SELECT ST_AsText(sedona.ST_Translate(ST_GeometryFromWKT('POINT(1 
3)'), 1, 2))",
         "POINT(2 5)");
   }
+
+  @Test
+  public void test_ST_Rotate() {
+    registerUDFV2("ST_Rotate", String.class, double.class);
+    verifySqlSingleRes(
+        "select 
ST_AsText(ST_ReducePrecision(sedona.ST_Rotate(ST_GeometryFromWKT('LINESTRING (0 
0, 1 0, 1 1, 0 0)'), 10),2))",
+        "LINESTRING(0 0,-0.84 -0.54,-0.3 -1.38,0 0)");
+    registerUDFV2("ST_Rotate", String.class, double.class, String.class);
+    verifySqlSingleRes(
+        "select 
ST_AsText(ST_ReducePrecision(sedona.ST_Rotate(ST_GeometryFromWKT('LINESTRING (0 
0, 1 0, 1 1, 0 0)'), 10, ST_GeometryFromWKT('POINT (0 0)')),2))",
+        "LINESTRING(0 0,-0.84 -0.54,-0.3 -1.38,0 0)");
+    registerUDFV2("ST_Rotate", String.class, double.class, double.class, 
double.class);
+    verifySqlSingleRes(
+        "select 
ST_AsText(ST_ReducePrecision(sedona.ST_Rotate(ST_GeometryFromWKT('LINESTRING (0 
0, 1 0, 1 1, 0 0)'), 10, 0, 0),2))",
+        "LINESTRING(0 0,-0.84 -0.54,-0.3 -1.38,0 0)");
+  }
 }
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 bf8d52a03..601c9cc06 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
@@ -1214,4 +1214,22 @@ public class UDFs {
     return GeometrySerde.serialize(
         Functions.translate(GeometrySerde.deserialize(geom), deltaX, deltaY, 
deltaZ));
   }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geom", "angle"})
+  public static byte[] ST_Rotate(byte[] geom, double angle) {
+    return 
GeometrySerde.serialize(Functions.rotate(GeometrySerde.deserialize(geom), 
angle));
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geom", "angle", "pointOrigin"})
+  public static byte[] ST_Rotate(byte[] geom, double angle, byte[] 
pointOrigin) {
+    return GeometrySerde.serialize(
+        Functions.rotate(
+            GeometrySerde.deserialize(geom), angle, 
GeometrySerde.deserialize(pointOrigin)));
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geom", "angle", "originX", "originY"})
+  public static byte[] ST_Rotate(byte[] geom, double angle, double originX, 
double originY) {
+    return GeometrySerde.serialize(
+        Functions.rotate(GeometrySerde.deserialize(geom), angle, originX, 
originY));
+  }
 }
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 3ff460a99..fa75a51b0 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
@@ -1437,4 +1437,31 @@ public class UDFsV2 {
     return GeometrySerde.serGeoJson(
         Functions.translate(GeometrySerde.deserGeoJson(geom), deltaX, deltaY, 
deltaZ));
   }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geom", "angle"},
+      argTypes = {"Geometry", "double"},
+      returnTypes = "Geometry")
+  public static String ST_Rotate(String geom, double angle) {
+    return 
GeometrySerde.serGeoJson(Functions.rotate(GeometrySerde.deserGeoJson(geom), 
angle));
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geom", "angle", "pointOrigin"},
+      argTypes = {"Geometry", "double", "Geometry"},
+      returnTypes = "Geometry")
+  public static String ST_Rotate(String geom, double angle, String 
pointOrigin) {
+    return GeometrySerde.serGeoJson(
+        Functions.rotate(
+            GeometrySerde.deserGeoJson(geom), angle, 
GeometrySerde.deserGeoJson(pointOrigin)));
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geom", "angle", "originX", "originY"},
+      argTypes = {"Geometry", "double", "double", "double"},
+      returnTypes = "Geometry")
+  public static String ST_Rotate(String geom, double angle, double originX, 
double originY) {
+    return GeometrySerde.serGeoJson(
+        Functions.rotate(GeometrySerde.deserGeoJson(geom), angle, originX, 
originY));
+  }
 }
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 8107f413d..2b9aefd84 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
@@ -226,6 +226,7 @@ object Catalog {
     function[ST_HausdorffDistance](-1),
     function[ST_DWithin](),
     function[ST_IsValidReason](),
+    function[ST_Rotate](),
     // Expression for rasters
     function[RS_NormalizedDifference](),
     function[RS_Mean](),
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 3caadda1b..16c325714 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
@@ -1604,3 +1604,13 @@ case class ST_IsValidReason(inputExpressions: 
Seq[Expression])
   protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
     copy(inputExpressions = newChildren)
 }
+
+case class ST_Rotate(inputExpressions: Seq[Expression])
+    extends InferredExpression(
+      inferrableFunction2(Functions.rotate),
+      inferrableFunction3(Functions.rotate),
+      inferrableFunction4(Functions.rotate)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
+    copy(inputExpressions = newChildren)
+}
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 e2f527021..cc7a756ee 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
@@ -447,6 +447,21 @@ object st_functions extends DataFrameAPI {
   def ST_Reverse(geometry: Column): Column = 
wrapExpression[ST_Reverse](geometry)
   def ST_Reverse(geometry: String): Column = 
wrapExpression[ST_Reverse](geometry)
 
+  def ST_Rotate(geometry: Column, angle: Column): Column =
+    wrapExpression[ST_Rotate](geometry, angle)
+  def ST_Rotate(geometry: String, angle: Double): Column =
+    wrapExpression[ST_Rotate](geometry, angle)
+
+  def ST_Rotate(geometry: Column, angle: Column, pointOrigin: Column): Column =
+    wrapExpression[ST_Rotate](geometry, angle, pointOrigin)
+  def ST_Rotate(geometry: String, angle: Double, pointOrigin: String): Column =
+    wrapExpression[ST_Rotate](geometry, angle, pointOrigin)
+
+  def ST_Rotate(geometry: Column, angle: Column, originX: Column, originY: 
Column): Column =
+    wrapExpression[ST_Rotate](geometry, angle, originX, originY)
+  def ST_Rotate(geometry: String, angle: Double, originX: Double, originY: 
Double): Column =
+    wrapExpression[ST_Rotate](geometry, angle, originX, originY)
+
   def ST_S2CellIDs(geometry: Column, level: Column): Column =
     wrapExpression[ST_S2CellIDs](geometry, level)
 
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 44ceee2db..81b849592 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
@@ -2174,5 +2174,24 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertEquals("POINT ZM(1 2 3 100)", point1)
       assertEquals("SRID=4326;POINT ZM(1 2 3 100)", point2)
     }
+
+    it("Passed ST_Rotate") {
+      val baseDf = sparkSession.sql(
+        "SELECT ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 2 0, 2 2, 0 2, 1 1, 
0 0))') AS geom1, ST_GeomFromText('POINT (2 2)') AS geom2")
+      var actual = baseDf.select(ST_AsEWKT(ST_Rotate("geom1", 
Math.PI))).first().get(0)
+      var expected =
+        "SRID=4326;POLYGON ((0 0, -2 0.0000000000000002, -2.0000000000000004 
-1.9999999999999998, -0.0000000000000002 -2, -1.0000000000000002 
-0.9999999999999999, 0 0))"
+      assert(expected.equals(actual))
+
+      actual = baseDf.select(ST_AsEWKT(ST_Rotate("geom1", 50, 
"geom2"))).first().get(0)
+      expected =
+        "SRID=4326;POLYGON ((-0.4546817643920842 0.5948176504236309, 
1.4752502925921425 0.0700679430157733, 2 2, 0.0700679430157733 
2.5247497074078575, 0.7726591178039579 1.2974088252118154, -0.4546817643920842 
0.5948176504236309))"
+      assert(expected.equals(actual))
+
+      actual = baseDf.select(ST_AsEWKT(ST_Rotate("geom1", 50, 2, 
2))).first().get(0)
+      expected =
+        "SRID=4326;POLYGON ((-0.4546817643920842 0.5948176504236309, 
1.4752502925921425 0.0700679430157733, 2 2, 0.0700679430157733 
2.5247497074078575, 0.7726591178039579 1.2974088252118154, -0.4546817643920842 
0.5948176504236309))"
+      assert(expected.equals(actual))
+    }
   }
 }
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 4bf957955..33054a39c 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
@@ -3339,4 +3339,94 @@ class functionTestScala
     }
   }
 
+  it("Should pass ST_Rotate") {
+    val geomTestCases = Map(
+      (
+        1,
+        "'LINESTRING (50 160, 50 50, 100 50)'",
+        "PI()",
+        "null",
+        "null") -> "'LINESTRING (-50.00000000000002 -160, -50.00000000000001 
-49.99999999999999, -100 -49.999999999999986)'",
+      (
+        2,
+        "'LINESTRING (50 160, 50 50, 100 50)'",
+        "PI()/6",
+        "50.0",
+        "160.0") -> "'LINESTRING (50 160, 104.99999999999999 
64.73720558371174, 148.30127018922192 89.73720558371173)'",
+      (
+        3,
+        "'SRID=4326;LINESTRING (50 160, 50 50, 100 50)'",
+        "PI()/6",
+        "null",
+        "null") -> "'SRID=4326;LINESTRING (-36.69872981077805 
163.5640646055102, 18.301270189221942 68.30127018922194, 61.60254037844388 
93.30127018922192)'",
+      (4, "'POINT EMPTY'", "PI()/6", "null", "null") -> "'POINT EMPTY'",
+      (
+        5,
+        "'LINESTRING (50 160 10, 50 50 10, 100 50 10)'",
+        "PI()",
+        "null",
+        "null") -> "'LINESTRING Z(-50.00000000000002 -160 10, 
-50.00000000000001 -49.99999999999999 10, -100 -49.999999999999986 10)'",
+      (
+        6,
+        "'SRID=4326;GEOMETRYCOLLECTION(POINT(10 10), LINESTRING (50 160, 50 
50, 100 50))'",
+        "PI()",
+        "null",
+        "null") -> "'SRID=4326;GEOMETRYCOLLECTION (POINT (-10.000000000000002 
-9.999999999999998), LINESTRING (-50.00000000000002 -160, -50.00000000000001 
-49.99999999999999, -100 -49.999999999999986))'",
+      (
+        7,
+        "'POINT (10 10)'",
+        "PI()/4",
+        "null",
+        "null") -> "'POINT (0.0000000000000009 14.142135623730951)'",
+      (
+        8,
+        "'SRID=0;POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))'",
+        "PI()/2",
+        "5.0",
+        "5.0") -> "'POLYGON ((10 -0.0000000000000003, 10 10, 0 10, 0 
0.0000000000000003, 10 -0.0000000000000003))'",
+      (
+        9,
+        "'MULTIPOINT ((1 1), (2 2), (3 3))'",
+        "PI()/2",
+        "null",
+        "null") -> "'MULTIPOINT ((-0.9999999999999999 1), (-1.9999999999999998 
2), (-3 3))'",
+      (
+        10,
+        "'MULTILINESTRING ((0 0, 10 0), (0 0, 0 10))'",
+        "PI()/4",
+        "null",
+        "null") -> "'MULTILINESTRING ((0 0, 7.0710678118654755 
7.071067811865475), (0 0, -7.071067811865475 7.0710678118654755))'",
+      (
+        11,
+        "'MULTIPOLYGON (((0 0, 5 0, 5 5, 0 5, 0 0)), ((10 10, 15 10, 15 15, 10 
15, 10 10)))'",
+        "PI()/2",
+        "null",
+        "null") -> "'MULTIPOLYGON (((0 0, 0.0000000000000003 5, -5 5, -5 
0.0000000000000003, 0 0)), ((-10 10, -9.999999999999998 15, -14.999999999999998 
15.000000000000002, -15 10.000000000000002, -10 10)))'")
+
+    for (((index, geom, angle, x, y), expectedResult) <- geomTestCases) {
+      val df = sparkSession.sql(s"""
+           |SELECT
+           |  ST_AsEWKT(
+           |    ST_Rotate(
+           |      ST_GeomFromEWKT($geom),
+           |      $angle${if (x != "null" && y != "null") s", $x, $y" else ""}
+           |    )
+           |  ) AS geom
+         """.stripMargin)
+
+      val actual = df.take(1)(0).get(0).asInstanceOf[String]
+      assert(actual == expectedResult.stripPrefix("'").stripSuffix("'"))
+    }
+
+    // Test invalid origin type
+    val invalidOriginDf = sparkSession.sql("""
+        |SELECT ST_Rotate(ST_GeomFromText('LINESTRING (50 160, 50 50, 100 
50)'), PI()/6, ST_GeomFromText('LINESTRING (0 0, 1 1)')) as result
+    """.stripMargin)
+
+    val exception = intercept[Exception] {
+      invalidOriginDf.collect()
+    }
+    exception.getMessage should include("The origin must be a non-empty Point 
geometry.")
+  }
+
 }


Reply via email to