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.")
+ }
+
}