Skip to content

Commit 1a2a0dc

Browse files
committed
feat: Add ST_RotateX
1 parent 43cab48 commit 1a2a0dc

File tree

21 files changed

+232
-0
lines changed

21 files changed

+232
-0
lines changed

common/src/main/java/org/apache/sedona/common/Functions.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2213,6 +2213,15 @@ public static Geometry points(Geometry geometry) {
22132213
return geometry.getFactory().createMultiPointFromCoords(coordinates);
22142214
}
22152215

2216+
public static Geometry rotateX(Geometry geometry, double angle) {
2217+
if (GeomUtils.isAnyGeomEmpty(geometry)) {
2218+
return geometry;
2219+
}
2220+
double sinAngle = Math.sin(angle);
2221+
double cosAngle = Math.cos(angle);
2222+
return affine(geometry, 1, 0, 0, 0, cosAngle, -sinAngle, 0, sinAngle, cosAngle, 0, 0, 0);
2223+
}
2224+
22162225
/**
22172226
* Rotates a geometry by a given angle in radians.
22182227
*

common/src/test/java/org/apache/sedona/common/FunctionsTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3803,6 +3803,21 @@ public void points() throws ParseException {
38033803
assertEquals("MULTIPOINT Z((0 0 1), (1 1 2), (2 2 3), (0 0 1))", result1);
38043804
}
38053805

3806+
@Test
3807+
public void rotateX() throws ParseException {
3808+
Geometry lineString = Constructors.geomFromEWKT("LINESTRING (50 160, 50 50, 100 50)");
3809+
String actual = Functions.asEWKT(Functions.rotateX(lineString, Math.PI));
3810+
String expected = "LINESTRING (50 -160, 50 -50, 100 -50)";
3811+
assertEquals(expected, actual);
3812+
3813+
lineString = Constructors.geomFromWKT("LINESTRING(1 2 3, 1 1 1)", 1234);
3814+
Geometry geomActual = Functions.rotateX(lineString, Math.PI / 2);
3815+
actual = Functions.asWKT(geomActual);
3816+
expected = "LINESTRING Z(1 -3 2, 1 -0.9999999999999999 1)";
3817+
assertEquals(1234, geomActual.getSRID());
3818+
assertEquals(expected, actual);
3819+
}
3820+
38063821
@Test
38073822
public void rotate() throws ParseException {
38083823
Geometry lineString = Constructors.geomFromEWKT("LINESTRING (50 160, 50 50, 100 50)");

docs/api/flink/Function.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,6 +3232,26 @@ Output:
32323232
SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, -0.2950504181870827 -1.383092639965822, 0 0))
32333233
```
32343234

3235+
## ST_RotateX
3236+
3237+
Introduction: Performs a counter-clockwise rotation of the specified geometry around the X-axis by the given angle measured in radians.
3238+
3239+
Format: `ST_RotateX(geometry: Geometry, angle: Double)`
3240+
3241+
Since: `v1.6.?`
3242+
3243+
SQL Example:
3244+
3245+
```sql
3246+
SELECT ST_RotateX(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 10)
3247+
```
3248+
3249+
Output:
3250+
3251+
```
3252+
SRID=4326;POLYGON ((0 0, 1 0, 1 -0.8390715290764524, 0 0))
3253+
```
3254+
32353255
## ST_S2CellIDs
32363256

32373257
Introduction: Cover the geometry with Google S2 Cells, return the corresponding cell IDs with the given level.

docs/api/snowflake/vector-data/Function.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,6 +2473,24 @@ Output:
24732473
SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, -0.2950504181870827 -1.383092639965822, 0 0))
24742474
```
24752475

2476+
## ST_RotateX
2477+
2478+
Introduction: Performs a counter-clockwise rotation of the specified geometry around the X-axis by the given angle measured in radians.
2479+
2480+
Format: `ST_RotateX(geometry: Geometry, angle: Double)`
2481+
2482+
SQL Example:
2483+
2484+
```sql
2485+
SELECT ST_RotateX(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 10)
2486+
```
2487+
2488+
Output:
2489+
2490+
```
2491+
SRID=4326;POLYGON ((0 0, 1 0, 1 -0.8390715290764524, 0 0))
2492+
```
2493+
24762494
## ST_S2CellIDs
24772495

24782496
Introduction: Cover the geometry with Google S2 Cells, return the corresponding cell IDs with the given level.

docs/api/sql/Function.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3312,6 +3312,26 @@ Output:
33123312
SRID=4326;POLYGON ((0 0, -0.8390715290764524 -0.5440211108893698, -0.2950504181870827 -1.383092639965822, 0 0))
33133313
```
33143314

3315+
## ST_RotateX
3316+
3317+
Introduction: Performs a counter-clockwise rotation of the specified geometry around the X-axis by the given angle measured in radians.
3318+
3319+
Format: `ST_RotateX(geometry: Geometry, angle: Double)`
3320+
3321+
Since: `v1.6.?`
3322+
3323+
SQL Example:
3324+
3325+
```sql
3326+
SELECT ST_RotateX(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 10)
3327+
```
3328+
3329+
Output:
3330+
3331+
```
3332+
SRID=4326;POLYGON ((0 0, 1 0, 1 -0.8390715290764524, 0 0))
3333+
```
3334+
33153335
## ST_S2CellIDs
33163336

33173337
Introduction: Cover the geometry with Google S2 Cells, return the corresponding cell IDs with the given level.

flink/src/main/java/org/apache/sedona/flink/Catalog.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public static UserDefinedFunction[] getFuncs() {
102102
new Functions.ST_ReducePrecision(),
103103
new Functions.ST_Reverse(),
104104
new Functions.ST_Rotate(),
105+
new Functions.ST_RotateX(),
105106
new Functions.ST_GeometryN(),
106107
new Functions.ST_InteriorRingN(),
107108
new Functions.ST_PointN(),

flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,16 @@ public String eval(
19261926
}
19271927
}
19281928

1929+
public static class ST_RotateX extends ScalarFunction {
1930+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
1931+
public Geometry eval(
1932+
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o,
1933+
@DataTypeHint(value = "Double") Double angle) {
1934+
Geometry geom = (Geometry) o;
1935+
return org.apache.sedona.common.Functions.rotateX(geom, angle);
1936+
}
1937+
}
1938+
19291939
public static class ST_Rotate extends ScalarFunction {
19301940
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
19311941
public Geometry eval(

flink/src/test/java/org/apache/sedona/flink/FunctionTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,6 +2609,22 @@ public void testIsValidReason() {
26092609
// standards
26102610
}
26112611

2612+
@Test
2613+
public void testRotateX() {
2614+
Table tbl =
2615+
tableEnv.sqlQuery(
2616+
"SELECT ST_GeomFromEWKT('POLYGON ((0 0, 2 0, 1 1, 2 2, 0 2, 1 1, 0 0))') AS geom");
2617+
String actual =
2618+
(String)
2619+
first(
2620+
tbl.select(call(Functions.ST_RotateX.class.getSimpleName(), $("geom"), Math.PI))
2621+
.as("geom")
2622+
.select(call(Functions.ST_AsEWKT.class.getSimpleName(), $("geom"))))
2623+
.getField(0);
2624+
String expected = "POLYGON ((0 0, 2 0, 1 -1, 2 -2, 0 -2, 1 -1, 0 0))";
2625+
assertEquals(expected, actual);
2626+
}
2627+
26122628
@Test
26132629
public void testRotate() {
26142630
Table tbl =

python/sedona/sql/st_functions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,20 @@ def ST_IsCollection(geometry: ColumnOrName) -> Column:
20202020
return _call_st_function("ST_IsCollection", geometry)
20212021

20222022

2023+
@validate_argument_types
2024+
def ST_RotateX(geometry: ColumnOrName, angle: Union[ColumnOrName, float]) -> Column:
2025+
"""Returns geometry rotated by the given angle in X axis
2026+
2027+
@param geometry: Geometry column or name
2028+
:type geometry: ColumnOrName
2029+
@param angle: Rotation angle in radians
2030+
:type angle: float
2031+
@return: X-axis rotated geometry
2032+
"""
2033+
2034+
return _call_st_function("ST_RotateX", (geometry, angle))
2035+
2036+
20232037
@validate_argument_types
20242038
def ST_Rotate(geometry: ColumnOrName, angle: Union[ColumnOrName, float], originX: Union[ColumnOrName, float] = None,
20252039
originY: Union[ColumnOrName, float] = None, pointOrigin: ColumnOrName = None) -> Column:

python/tests/sql/test_dataframe_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
(stf.ST_ReducePrecision, ("geom", 1), "precision_reduce_point", "", "POINT (0.1 0.2)"),
206206
(stf.ST_RemovePoint, ("line", 1), "linestring_geom", "", "LINESTRING (0 0, 2 0, 3 0, 4 0, 5 0)"),
207207
(stf.ST_Reverse, ("line",), "linestring_geom", "", "LINESTRING (5 0, 4 0, 3 0, 2 0, 1 0, 0 0)"),
208+
(stf.ST_RotateX, ("line", 10.0), "4D_line", "ST_ReducePrecision(geom, 2)", "LINESTRING Z (1 -0.3 -1.383092639965822, 2 -0.59 -2.766185279931644, 3 -0.89 -4.149277919897466, -1 0.3 1.383092639965822)"),
208209
(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)"),
209210
(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)"),
210211
(stf.ST_S2CellIDs, ("point", 30), "point_geom", "", [1153451514845492609]),
@@ -427,6 +428,7 @@
427428
(stf.ST_RemovePoint, ("", 1.0)),
428429
(stf.ST_Reverse, (None,)),
429430
(stf.ST_Rotate, (None,None,)),
431+
(stf.ST_Rotate, (None,None)),
430432
(stf.ST_S2CellIDs, (None, 2)),
431433
(stf.ST_S2ToGeom, (None,)),
432434
(stf.ST_SetPoint, (None, 1, "")),

python/tests/sql/test_function.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,17 @@ def test_st_add_point(self):
949949
]
950950
assert (collected_geometries[0] == "LINESTRING (0 0, 1 1, 1 0, 21 52)")
951951

952+
def test_st_rotate_x(self):
953+
baseDf = self.spark.sql("SELECT ST_GeomFromWKT('LINESTRING (50 160, 50 50, 100 50)') as geom1, ST_GeomFromWKT('LINESTRING(1 2 3, 1 1 1)') AS geom2")
954+
955+
actual = baseDf.selectExpr("ST_RotateX(geom1, PI())").first()[0].wkt
956+
expected = "LINESTRING (50 -160, 50 -50, 100 -50)"
957+
assert expected == actual
958+
959+
actual = baseDf.selectExpr("ST_RotateX(geom2, PI() / 2)").first()[0].wkt
960+
expected = "LINESTRING Z (1 -3 2, 1 -0.9999999999999999 1)"
961+
assert expected == actual
962+
952963
def test_st_remove_point(self):
953964
result_and_expected = [
954965
[self.calculate_st_remove("Linestring(0 0, 1 1, 1 0, 0 0)", 0), "LINESTRING (1 1, 1 0, 0 0)"],

snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,14 @@ public void test_ST_Translate() {
12071207
"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)");
12081208
}
12091209

1210+
@Test
1211+
public void test_ST_RotateX() {
1212+
registerUDF("ST_RotateX", byte[].class, double.class);
1213+
verifySqlSingleRes(
1214+
"SELECT sedona.ST_AsText(sedona.ST_RotateX(sedona.ST_GeomFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 0)'), 10))",
1215+
"LINESTRING (0 0, 1 0, 1 -0.8390715290764524, 0 0)");
1216+
}
1217+
12101218
@Test
12111219
public void test_ST_Rotate() {
12121220
registerUDF("ST_Rotate", byte[].class, double.class);

snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,14 @@ public void test_ST_Translate() {
11621162
"POINT(2 5)");
11631163
}
11641164

1165+
@Test
1166+
public void test_ST_RotateX() {
1167+
registerUDFV2("ST_RotateX", String.class, double.class);
1168+
verifySqlSingleRes(
1169+
"select ST_AsText(ST_ReducePrecision(sedona.ST_RotateX(ST_GeometryFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 0)'), 10),2))",
1170+
"LINESTRING(0 0,1 0,1 -0.84,0 0)");
1171+
}
1172+
11651173
@Test
11661174
public void test_ST_Rotate() {
11671175
registerUDFV2("ST_Rotate", String.class, double.class);

snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,11 @@ public static byte[] ST_Translate(byte[] geom, double deltaX, double deltaY, dou
12501250
Functions.translate(GeometrySerde.deserialize(geom), deltaX, deltaY, deltaZ));
12511251
}
12521252

1253+
@UDFAnnotations.ParamMeta(argNames = {"geometry", "angle"})
1254+
public static byte[] ST_RotateX(byte[] geometry, double angle) {
1255+
return GeometrySerde.serialize(Functions.rotateX(GeometrySerde.deserialize(geometry), angle));
1256+
}
1257+
12531258
@UDFAnnotations.ParamMeta(argNames = {"geom", "angle"})
12541259
public static byte[] ST_Rotate(byte[] geom, double angle) {
12551260
return GeometrySerde.serialize(Functions.rotate(GeometrySerde.deserialize(geom), angle));

snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,14 @@ public static String ST_Translate(String geom, double deltaX, double deltaY, dou
14901490
Functions.translate(GeometrySerde.deserGeoJson(geom), deltaX, deltaY, deltaZ));
14911491
}
14921492

1493+
@UDFAnnotations.ParamMeta(
1494+
argNames = {"geometry", "angle"},
1495+
argTypes = {"Geometry", "double"},
1496+
returnTypes = "Geometry")
1497+
public static String ST_RotateX(String geometry, double angle) {
1498+
return GeometrySerde.serGeoJson(Functions.rotateX(GeometrySerde.deserGeoJson(geometry), angle));
1499+
}
1500+
14931501
@UDFAnnotations.ParamMeta(
14941502
argNames = {"geom", "angle"},
14951503
argTypes = {"Geometry", "double"},

spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ object Catalog {
230230
function[ST_DWithin](),
231231
function[ST_IsValidReason](),
232232
function[ST_Rotate](),
233+
function[ST_RotateX](),
233234
// Expression for rasters
234235
function[RS_NormalizedDifference](),
235236
function[RS_Mean](),

spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,13 @@ case class ST_IsValidReason(inputExpressions: Seq[Expression])
16911691
copy(inputExpressions = newChildren)
16921692
}
16931693

1694+
case class ST_RotateX(inputExpressions: Seq[Expression])
1695+
extends InferredExpression(inferrableFunction2(Functions.rotateX)) {
1696+
1697+
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
1698+
copy(inputExpressions = newChildren)
1699+
}
1700+
16941701
case class ST_Rotate(inputExpressions: Seq[Expression])
16951702
extends InferredExpression(
16961703
inferrableFunction2(Functions.rotate),

spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ object st_functions extends DataFrameAPI {
471471
def ST_Reverse(geometry: Column): Column = wrapExpression[ST_Reverse](geometry)
472472
def ST_Reverse(geometry: String): Column = wrapExpression[ST_Reverse](geometry)
473473

474+
def ST_RotateX(geometry: Column, angle: Column): Column =
475+
wrapExpression[ST_RotateX](geometry, angle)
476+
def ST_RotateX(geometry: String, angle: Double): Column =
477+
wrapExpression[ST_RotateX](geometry, angle)
478+
def ST_RotateX(geometry: String, angle: String): Column =
479+
wrapExpression[ST_RotateX](geometry, angle)
480+
474481
def ST_Rotate(geometry: Column, angle: Column): Column =
475482
wrapExpression[ST_Rotate](geometry, angle)
476483
def ST_Rotate(geometry: String, angle: Double): Column =

spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class PreserveSRIDSuite extends TestBaseScala with TableDrivenPropertyChecks {
109109
("ST_BoundingDiagonal(geom1)", 1000),
110110
("ST_DelaunayTriangles(geom4)", 1000),
111111
("ST_Rotate(geom1, 10)", 1000),
112+
("ST_RotateX(geom1, 10)", 1000),
112113
("ST_Collect(geom1, geom2, geom3)", 1000),
113114
("ST_GeneratePoints(geom1, 3)", 1000))
114115

spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,26 @@ class dataFrameAPITestScala extends TestBaseScala {
22262226
assertEquals("SRID=4326;POINT ZM(1 2 3 100)", point2)
22272227
}
22282228

2229+
it("Should pass ST_RotateX") {
2230+
val geomTestCases = Map(
2231+
(
2232+
1,
2233+
"'LINESTRING (50 160, 50 50, 100 50)'",
2234+
Math.PI) -> "'LINESTRING (50 -160, 50 -50, 100 -50)'",
2235+
(
2236+
2,
2237+
"'LINESTRING(1 2 3, 1 1 1)'",
2238+
Math.PI / 2) -> "'LINESTRING Z(1 -3 2, 1 -0.9999999999999999 1)'")
2239+
2240+
for (((index, geom, angle), expectedResult) <- geomTestCases) {
2241+
val baseDf = sparkSession.sql(s"SELECT ST_GeomFromEWKT($geom) as geom")
2242+
val df = baseDf.select(ST_AsEWKT(ST_RotateX("geom", angle)))
2243+
2244+
val actual = df.take(1)(0).get(0).asInstanceOf[String]
2245+
assert(actual == expectedResult.stripPrefix("'").stripSuffix("'"))
2246+
}
2247+
}
2248+
22292249
it("Passed ST_Rotate") {
22302250
val baseDf = sparkSession.sql(
22312251
"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")

spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,37 @@ class functionTestScala
33913391
}
33923392
}
33933393

3394+
it("Should pass ST_RotateX") {
3395+
sparkSession
3396+
.sql(
3397+
"SELECT ST_AsEWKT(ST_RotateX(ST_GeomFromEWKT('SRID=4326;POLYGON ((0 0, 1 0, 1 1, 0 0))'), 10)) ")
3398+
.show(truncate = false)
3399+
val geomTestCases = Map(
3400+
(
3401+
1,
3402+
"'LINESTRING (50 160, 50 50, 100 50)'",
3403+
"PI()") -> "'LINESTRING (50 -160, 50 -50, 100 -50)'",
3404+
(
3405+
2,
3406+
"'LINESTRING(1 2 3, 1 1 1)'",
3407+
"PI()/2") -> "'LINESTRING Z(1 -3 2, 1 -0.9999999999999999 1)'")
3408+
3409+
for (((index, geom, angle), expectedResult) <- geomTestCases) {
3410+
val df = sparkSession.sql(s"""
3411+
|SELECT
3412+
| ST_AsEWKT(
3413+
| ST_RotateX(
3414+
| ST_GeomFromEWKT($geom),
3415+
| $angle
3416+
| )
3417+
| ) AS geom
3418+
""".stripMargin)
3419+
3420+
val actual = df.take(1)(0).get(0).asInstanceOf[String]
3421+
assert(actual == expectedResult.stripPrefix("'").stripSuffix("'"))
3422+
}
3423+
}
3424+
33943425
it("Should pass ST_Rotate") {
33953426
val geomTestCases = Map(
33963427
(

0 commit comments

Comments
 (0)