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 91ee2d01b [SEDONA-656] Add ST_Project (#1594)
91ee2d01b is described below
commit 91ee2d01b1b05554eb6463ab9a4bbb7ad3846292
Author: Furqaan Khan <[email protected]>
AuthorDate: Mon Sep 23 21:42:11 2024 -0400
[SEDONA-656] Add ST_Project (#1594)
* feat: add ST_Project
* fix: snowflake tests
* fix: snowflake tests 2
* address comments
* chore: format python code
---
.../java/org/apache/sedona/common/Functions.java | 40 +++++++++++++++++++++
.../org/apache/sedona/common/FunctionsTest.java | 35 ++++++++++++++++++
docs/api/flink/Function.md | 42 ++++++++++++++++++++++
docs/api/snowflake/vector-data/Function.md | 40 +++++++++++++++++++++
docs/api/sql/Function.md | 42 ++++++++++++++++++++++
.../main/java/org/apache/sedona/flink/Catalog.java | 1 +
.../apache/sedona/flink/expressions/Functions.java | 23 ++++++++++++
.../java/org/apache/sedona/flink/FunctionTest.java | 15 ++++++++
python/sedona/sql/st_functions.py | 21 +++++++++++
python/tests/sql/test_dataframe_api.py | 19 +++++++++-
python/tests/sql/test_function.py | 17 +++++++++
.../sedona/snowflake/snowsql/TestFunctions.java | 8 +++++
.../sedona/snowflake/snowsql/TestFunctionsV2.java | 8 +++++
.../org/apache/sedona/snowflake/snowsql/UDFs.java | 12 +++++++
.../apache/sedona/snowflake/snowsql/UDFsV2.java | 18 ++++++++++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 +
.../sql/sedona_sql/expressions/Functions.scala | 10 ++++++
.../sql/sedona_sql/expressions/st_functions.scala | 13 +++++++
.../org/apache/sedona/sql/PreserveSRIDSuite.scala | 1 +
.../apache/sedona/sql/dataFrameAPITestScala.scala | 19 +++++++++-
.../org/apache/sedona/sql/functionTestScala.scala | 15 ++++++++
21 files changed, 398 insertions(+), 2 deletions(-)
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 e4f2a088c..038bfeebe 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -32,6 +32,7 @@ import org.apache.sedona.common.geometryObjects.Circle;
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.MinimumBoundingCircle;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
@@ -1353,6 +1354,45 @@ public class Functions {
return dimension;
}
+ public static Geometry project(Geometry point, double distance, double
azimuth, boolean lenient) {
+ if (!point.getClass().getSimpleName().equals("Point")) {
+ if (lenient) {
+ return point.getFactory().createPoint();
+ } else {
+ throw new IllegalArgumentException(
+ String.format(
+ "Input geometry is %s. It should be a Point type geometry",
+ point.getClass().getSimpleName()));
+ }
+ }
+
+ // Normalize azimuth if it is out of (-360, 360) range
+ // by calculating the number of orbits and subtracting it
+ int orbit = (int) Math.floor(azimuth / Angle.PI_TIMES_2);
+ azimuth -= Angle.PI_TIMES_2 * orbit;
+ // Convert azimuth to conventional slope
+ double slope = Angle.PI_TIMES_2 - azimuth + Angle.PI_OVER_2;
+ if (slope > Angle.PI_TIMES_2) slope -= Angle.PI_TIMES_2;
+ if (slope < -Angle.PI_TIMES_2) slope += Angle.PI_TIMES_2;
+
+ Coordinate projectedCoordinate = Angle.project(point.getCoordinate(),
slope, distance);
+
+ if (Functions.hasZ(point)) {
+ projectedCoordinate.setZ(point.getCoordinate().getZ());
+ }
+
+ if (Functions.hasM(point)) {
+ CoordinateXYZM projectedCoordinateM = new
CoordinateXYZM(projectedCoordinate);
+ projectedCoordinateM.setM(point.getCoordinate().getM());
+ return point.getFactory().createPoint(projectedCoordinateM);
+ }
+ return point.getFactory().createPoint(projectedCoordinate);
+ }
+
+ public static Geometry project(Geometry point, double distance, double
azimuth) {
+ return project(point, distance, azimuth, false);
+ }
+
/**
* get the coordinates of a geometry and transform to Google s2 cell id
*
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 b22e09e19..dbec7c651 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -658,6 +658,41 @@ public class FunctionsTest extends TestBase {
assertEquals(actualResult, expectedResult);
}
+ @Test
+ public void project() throws ParseException {
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(0, 0));
+ String actual = Functions.asWKT(Functions.project(point, 100000,
Math.toRadians(45.0)));
+ String expected = "POINT (70710.67811865476 70710.67811865475)";
+ assertEquals(expected, actual);
+
+ actual =
+ Functions.asWKT(Functions.project(Constructors.makeEnvelope(0, 1, 0,
1), 10, 10, true));
+ expected = "POINT EMPTY";
+ assertEquals(expected, actual);
+
+ point = Constructors.geomFromWKT("POINT Z(10 15 12)", 1111);
+ Geometry actualPoint = Functions.project(point, 1000,
Math.toRadians(300.0));
+ actual = Functions.asWKT(actualPoint);
+ expected = "POINT Z(-856.0254037844385 515.0000000000003 12)";
+ assertEquals(expected, actual);
+ assertEquals(1111, actualPoint.getSRID());
+
+ point = Constructors.geomFromWKT("POINT M(10 15 12)", 1111);
+ actual = Functions.asWKT(Functions.project(point, 1000,
Math.toRadians(300.0)));
+ expected = "POINT M(-856.0254037844385 515.0000000000003 12)";
+ assertEquals(expected, actual);
+
+ point = Constructors.geomFromWKT("POINT ZM(10 15 12 2)", 1111);
+ actual = Functions.asWKT(Functions.project(point, 1000,
Math.toRadians(300.0)));
+ expected = "POINT ZM(-856.0254037844385 515.0000000000003 12 2)";
+ assertEquals(expected, actual);
+
+ point = Constructors.geomFromWKT("POINT(2 -1)", 0);
+ actual = Functions.asWKT(Functions.project(point, 100,
Math.toRadians(470)));
+ expected = Functions.asWKT(Functions.project(point, 100,
Math.toRadians(110)));
+ assertEquals(expected, actual);
+ }
+
private static boolean intersects(Set<?> s1, Set<?> s2) {
Set<?> copy = new HashSet<>(s1);
copy.retainAll(s2);
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index b5c11c37a..82444ad31 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -3139,6 +3139,48 @@ Output:
GEOMETRYCOLLECTION (POLYGON ((0 2, 1 3, 2 4, 2 3, 2 2, 1 2, 0 2)), POLYGON ((2
2, 2 3, 2 4, 3 3, 4 2, 3 2, 2 2)))
```
+## ST_Project
+
+Introduction: Calculates a new point location given a starting point,
distance, and azimuth. The azimuth indicates the direction, expressed in
radians, and is measured in a clockwise manner starting from true north. The
system can handle azimuth values that are negative or exceed 2π (360 degrees).
The optional `lenient` parameter prevents an error if the input geometry is not
a Point. Its default value is `false`.
+
+Format:
+
+```
+ST_Project(point: Geometry, distance: Double, azimuth: Double, lenient:
Boolean = False)
+```
+
+```
+ST_Project(point: Geometry, distance: Double, Azimuth: Double)
+```
+
+Since: `v1.7.0`
+
+SQL Example:
+
+```sql
+SELECT ST_Project(ST_GeomFromText('POINT (10 15)'), 100, radians(90))
+```
+
+Output:
+
+```
+POINT (110 14.999999999999975)
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Project(
+ ST_GeomFromText('POLYGON ((1 5, 1 1, 3 3, 5 3, 1 5))'),
+ 25, radians(270), true)
+```
+
+Output:
+
+```
+POINT EMPTY
+```
+
## ST_ReducePrecision
Introduction: Reduce the decimals places in the coordinates of the geometry to
the given number of decimal places. The last decimal place will be rounded.
diff --git a/docs/api/snowflake/vector-data/Function.md
b/docs/api/snowflake/vector-data/Function.md
index 7848f7bcf..671588c99 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -2388,6 +2388,46 @@ Output:
GEOMETRYCOLLECTION (POLYGON ((0 2, 1 3, 2 4, 2 3, 2 2, 1 2, 0 2)), POLYGON ((2
2, 2 3, 2 4, 3 3, 4 2, 3 2, 2 2)))
```
+## ST_Project
+
+Introduction: Calculates a new point location given a starting point,
distance, and azimuth. The azimuth indicates the direction, expressed in
radians, and is measured in a clockwise manner starting from true north. The
system can handle azimuth values that are negative or exceed 2π (360 degrees).
The optional `lenient` parameter prevents an error if the input geometry is not
a Point. Its default value is `false`.
+
+Format:
+
+```
+ST_Project(point: Geometry, distance: Double, azimuth: Double, lenient:
Boolean = False)
+```
+
+```
+ST_Project(point: Geometry, distance: Double, Azimuth: Double)
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Project(ST_GeomFromText('POINT (10 15)'), 100, radians(90))
+```
+
+Output:
+
+```
+POINT (110 14.999999999999975)
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Project(
+ ST_GeomFromText('POLYGON ((1 5, 1 1, 3 3, 5 3, 1 5))'),
+ 25, radians(270), true)
+```
+
+Output:
+
+```
+POINT EMPTY
+```
+
## ST_ReducePrecision
Introduction: Reduce the decimals places in the coordinates of the geometry to
the given number of decimal places. The last decimal place will be rounded.
This function was called ST_PrecisionReduce in versions prior to v1.5.0.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 46063b9c7..bac772713 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -3219,6 +3219,48 @@ Output:
GEOMETRYCOLLECTION (POLYGON ((0 2, 1 3, 2 4, 2 3, 2 2, 1 2, 0 2)), POLYGON ((2
2, 2 3, 2 4, 3 3, 4 2, 3 2, 2 2)))
```
+## ST_Project
+
+Introduction: Calculates a new point location given a starting point,
distance, and azimuth. The azimuth indicates the direction, expressed in
radians, and is measured in a clockwise manner starting from true north. The
system can handle azimuth values that are negative or exceed 2π (360 degrees).
The optional `lenient` parameter prevents an error if the input geometry is not
a Point. Its default value is `false`.
+
+Format:
+
+```
+ST_Project(point: Geometry, distance: Double, azimuth: Double, lenient:
Boolean = False)
+```
+
+```
+ST_Project(point: Geometry, distance: Double, Azimuth: Double)
+```
+
+Since: `v1.7.0`
+
+SQL Example:
+
+```sql
+SELECT ST_Project(ST_GeomFromText('POINT (10 15)'), 100, radians(90))
+```
+
+Output:
+
+```
+POINT (110 14.999999999999975)
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Project(
+ ST_GeomFromText('POLYGON ((1 5, 1 1, 3 3, 5 3, 1 5))'),
+ 25, radians(270), true)
+```
+
+Output:
+
+```
+POINT EMPTY
+```
+
## ST_ReducePrecision
Introduction: Reduce the decimals places in the coordinates of the geometry to
the given number of decimal places. The last decimal place will be rounded.
This function was called ST_PrecisionReduce in versions prior to v1.5.0.
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 61d88a5c0..82b8cbc4c 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -161,6 +161,7 @@ public class Catalog {
new Functions.ST_Points(),
new Functions.ST_Polygon(),
new Functions.ST_Polygonize(),
+ new Functions.ST_Project(),
new Functions.ST_MakePolygon(),
new Functions.ST_MakeValid(),
new Functions.ST_MaxDistance(),
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 38e41fce2..85f634083 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
@@ -1190,6 +1190,29 @@ public class Functions {
}
}
+ public static class ST_Project 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 distance,
+ @DataTypeHint(value = "Double") Double azimuth,
+ @DataTypeHint("Boolean") Boolean lenient) {
+ Geometry point = (Geometry) o1;
+ return org.apache.sedona.common.Functions.project(point, distance,
azimuth, lenient);
+ }
+
+ @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 distance,
+ @DataTypeHint(value = "Double") Double azimuth) {
+ Geometry point = (Geometry) o1;
+ return org.apache.sedona.common.Functions.project(point, distance,
azimuth);
+ }
+ }
+
public static class ST_MakeValid 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 e08813da7..ccbb586f6 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -774,6 +774,21 @@ public class FunctionTest extends TestBase {
assertEquals("POINT (-117.99 32.01)", result.toString());
}
+ @Test
+ public void testProject() {
+ Table pointTable = createPointTable(testDataSize);
+ Table surfaceTable =
+ pointTable.select(
+ call(
+ Functions.ST_Project.class.getSimpleName(),
+ $(pointColNames[0]),
+ 100,
+ Math.toRadians(45)));
+ Geometry result = (Geometry) first(surfaceTable).getField(0);
+ String expected = "POINT (70.71067811865476 70.71067811865474)";
+ assertEquals(expected, result.toString());
+ }
+
@Test
public void testReducePrecision() {
Table polygonTable = tableEnv.sqlQuery("SELECT ST_GeomFromText('POINT(0.12
0.23)') AS geom");
diff --git a/python/sedona/sql/st_functions.py
b/python/sedona/sql/st_functions.py
index 05d90c2e8..e37ab4b56 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1210,6 +1210,27 @@ def ST_Polygonize(geometry: ColumnOrName) -> Column:
return _call_st_function("ST_Polygonize", (geometry))
+@validate_argument_types
+def ST_Project(
+ geom: ColumnOrName,
+ distance: Union[ColumnOrName, float],
+ azimuth: Union[ColumnOrName, float],
+ lenient: Optional[Union[ColumnOrName, bool]] = None,
+) -> Column:
+ """Calculates a new point location given a starting point, distance, and
direction (azimuth).
+
+ @param geom:
+ @param distance:
+ @param azimuth:
+ @param lenient:
+ @return:
+ """
+ args = (geom, distance, azimuth, lenient)
+ if lenient is None:
+ args = (geom, distance, azimuth)
+ return _call_st_function("ST_Project", args)
+
+
@validate_argument_types
def ST_MakePolygon(
line_string: ColumnOrName, holes: Optional[ColumnOrName] = None
diff --git a/python/tests/sql/test_dataframe_api.py
b/python/tests/sql/test_dataframe_api.py
index 5508e62c0..223bfc840 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
+from math import radians
from typing import Callable, Tuple
from pyspark.sql import functions as f, Row
@@ -755,6 +755,20 @@ test_configurations = [
"ST_Normalize(geom)",
"GEOMETRYCOLLECTION (POLYGON ((0 2, 1 3, 2 4, 2 3, 2 2, 1 2, 0 2)),
POLYGON ((2 2, 2 3, 2 4, 3 3, 4 2, 3 2, 2 2)))",
),
+ (
+ stf.ST_Project,
+ ("point", 10.0, radians(10)),
+ "point_geom",
+ "",
+ "POINT (1.7364817766693021 10.848077530122081)",
+ ),
+ (
+ stf.ST_Project,
+ ("geom", 10.0, radians(10), True),
+ "triangle_geom",
+ "",
+ "POINT EMPTY",
+ ),
(
stf.ST_MakePolygon,
("geom",),
@@ -1288,6 +1302,9 @@ wrong_type_configurations = [
(stf.ST_PointN, ("", None)),
(stf.ST_PointN, ("", 2.0)),
(stf.ST_PointOnSurface, (None,)),
+ (stf.ST_Project, (None, "", "", None)),
+ (stf.ST_Project, ("", None, "", None)),
+ (stf.ST_Project, ("", "", None, None)),
(stf.ST_ReducePrecision, (None, 1)),
(stf.ST_ReducePrecision, ("", None)),
(stf.ST_ReducePrecision, ("", 1.0)),
diff --git a/python/tests/sql/test_function.py
b/python/tests/sql/test_function.py
index 9100a04f8..028b4faac 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1735,6 +1735,23 @@ class TestPredicateJoin(TestBase):
for actual, expected in result:
assert actual == expected
+ def test_st_project(self):
+ baseDf = self.spark.sql("SELECT ST_GeomFromWKT('POINT(0 0)') as point")
+ actual = baseDf.selectExpr("ST_Project(point, 10,
radians(45))").first()[0].wkt
+ expected = "POINT (7.0710678118654755 7.071067811865475)"
+ assert expected == actual
+
+ actual = (
+ self.spark.sql(
+ "SELECT ST_Project(ST_MakeEnvelope(0, 1, 2, 0), 10,
radians(50), true)"
+ )
+ .first()[0]
+ .wkt
+ )
+
+ expected = "POINT EMPTY"
+ assert expected == actual
+
def test_st_make_polygon(self):
# Given
geometry_df = self.spark.createDataFrame(
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 40bc28b8b..79c5843ef 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
@@ -804,6 +804,14 @@ public class TestFunctions extends TestBase {
4.0);
}
+ @Test
+ public void test_ST_Project() {
+ registerUDF("ST_Project", byte[].class, double.class, double.class);
+ verifySqlSingleRes(
+ "select
sedona.ST_AsText(sedona.ST_Project(sedona.ST_GeomFromText('POINT (0 0)'), 1000,
10))",
+ "POINT (-544.0211108893703 -839.0715290764522)");
+ }
+
@Test
public void test_ST_PrecisionReduce() {
registerUDF("ST_PrecisionReduce", byte[].class, int.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 b3c21f7c0..d1941ea70 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
@@ -753,6 +753,14 @@ public class TestFunctionsV2 extends TestBase {
4.0);
}
+ @Test
+ public void test_ST_Project() {
+ registerUDFV2("ST_Project", String.class, double.class, double.class);
+ verifySqlSingleRes(
+ "select ST_AsWKT(sedona.ST_Project(ST_GeomFromWKT('POINT (0 0)'),
1000, 10))",
+ "POINT(-544.021110889 -839.071529076)");
+ }
+
@Test
public void test_ST_PrecisionReduce() {
registerUDFV2("ST_PrecisionReduce", String.class, int.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 a5622039e..40cd2585b 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
@@ -873,6 +873,18 @@ public class UDFs {
return GeometrySerde.serialize(Constructors.polygonFromEnvelope(minX,
minY, maxX, maxY));
}
+ @UDFAnnotations.ParamMeta(argNames = {"point", "distance", "azimuth"})
+ public static byte[] ST_Project(byte[] point, double distance, double
azimuth) {
+ return GeometrySerde.serialize(
+ Functions.project(GeometrySerde.deserialize(point), distance,
azimuth));
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"point", "distance", "azimuth",
"lenient"})
+ public static byte[] ST_Project(byte[] point, double distance, double
azimuth, boolean lenient) {
+ return GeometrySerde.serialize(
+ Functions.project(GeometrySerde.deserialize(point), distance, azimuth,
lenient));
+ }
+
@UDFAnnotations.ParamMeta(argNames = {"minX", "minY", "maxX", "maxY"})
public static byte[] ST_MakeEnvelope(double minX, double minY, double maxX,
double maxY) {
return GeometrySerde.serialize(Constructors.makeEnvelope(minX, minY, maxX,
maxY));
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 75109cb4a..f0c43981d 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
@@ -972,6 +972,24 @@ public class UDFsV2 {
return
GeometrySerde.serGeoJson(Functions.polygonize(GeometrySerde.deserGeoJson(geometry)));
}
+ @UDFAnnotations.ParamMeta(
+ argNames = {"point", "distance", "azimuth"},
+ argTypes = {"Geometry", "double", "double"},
+ returnTypes = "Geometry")
+ public static String ST_Project(String point, double distance, double
azimuth) {
+ return GeometrySerde.serGeoJson(
+ Functions.project(GeometrySerde.deserGeoJson(point), distance,
azimuth));
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"point", "distance", "azimuth", "lenient"},
+ argTypes = {"Geometry", "double", "double", "boolean"},
+ returnTypes = "Geometry")
+ public static String ST_Project(String point, double distance, double
azimuth, boolean lenient) {
+ return GeometrySerde.serGeoJson(
+ Functions.project(GeometrySerde.deserGeoJson(point), distance,
azimuth, lenient));
+ }
+
@UDFAnnotations.ParamMeta(
argNames = {"geometry", "precisionScale"},
argTypes = {"Geometry", "int"},
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 80ffdabb7..2c0693110 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
@@ -64,6 +64,7 @@ object Catalog {
function[ST_PointM](0),
function[ST_PointZM](0),
function[ST_PolygonFromEnvelope](),
+ function[ST_Project](),
function[ST_Contains](),
function[ST_Intersects](),
function[ST_Within](),
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 0e64d5930..e309dcc7f 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
@@ -1009,6 +1009,16 @@ case class ST_Polygonize(inputExpressions:
Seq[Expression])
}
}
+case class ST_Project(inputExpressions: Seq[Expression])
+ extends InferredExpression(
+ inferrableFunction4(Functions.project),
+ inferrableFunction3(Functions.project)) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
case class ST_MakePolygon(inputExpressions: Seq[Expression])
extends
InferredExpression(InferrableFunction.allowRightNull(Functions.makePolygon)) {
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 0101978b1..4699f43ff 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
@@ -369,6 +369,19 @@ object st_functions extends DataFrameAPI {
def ST_Polygonize(geoms: Column): Column =
wrapExpression[ST_Polygonize](geoms)
def ST_Polygonize(geoms: String): Column =
wrapExpression[ST_Polygonize](geoms)
+ def ST_Project(point: Column, distance: Column, azimuth: Column, lenient:
Column): Column =
+ wrapExpression[ST_Project](point, distance, azimuth, lenient)
+ def ST_Project(point: String, distance: String, azimuth: String, lenient:
String): Column =
+ wrapExpression[ST_Project](point, distance, azimuth, lenient)
+ def ST_Project(point: String, distance: Double, azimuth: Double, lenient:
Boolean): Column =
+ wrapExpression[ST_Project](point, distance, azimuth, lenient)
+ def ST_Project(point: Column, distance: Column, azimuth: Column): Column =
+ wrapExpression[ST_Project](point, distance, azimuth)
+ def ST_Project(point: String, distance: String, azimuth: String): Column =
+ wrapExpression[ST_Project](point, distance, azimuth)
+ def ST_Project(point: String, distance: Double, azimuth: Double): Column =
+ wrapExpression[ST_Project](point, distance, azimuth)
+
def ST_MakePolygon(lineString: Column): Column =
wrapExpression[ST_MakePolygon](lineString, null)
def ST_MakePolygon(lineString: String): Column =
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
index 74d80b8e4..17c0f2e98 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
@@ -78,6 +78,7 @@ class PreserveSRIDSuite extends TestBaseScala with
TableDrivenPropertyChecks {
("ST_Points(geom1)", 1000),
("ST_Polygon(ST_InteriorRingN(geom4, 0), 2000)", 2000),
("ST_Polygonize(geom5)", 1000),
+ ("ST_Project(geom5, 12, 12, true)", 1000),
("ST_MakePolygon(ST_ExteriorRing(geom4), ARRAY(ST_InteriorRingN(geom4,
0)))", 1000),
("ST_Difference(geom1, geom2)", 1000),
("ST_SymDifference(geom1, geom2)", 1000),
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 2f8faeb0c..981f88fd5 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
@@ -20,7 +20,7 @@ package org.apache.sedona.sql
import org.apache.commons.codec.binary.Hex
import org.apache.spark.sql.Row
-import org.apache.spark.sql.functions.{array, col, element_at, expr, lit}
+import org.apache.spark.sql.functions.{radians, col, element_at, expr, lit}
import org.apache.spark.sql.sedona_sql.expressions.InferredExpressionException
import org.apache.spark.sql.sedona_sql.expressions.st_aggregates._
import org.apache.spark.sql.sedona_sql.expressions.st_constructors._
@@ -760,6 +760,23 @@ class dataFrameAPITestScala extends TestBaseScala {
assert(actualResult.toText() == expectedResult)
}
+ it("Passed ST_Project") {
+ val baseDf = sparkSession.sql(
+ "SELECT ST_GeomFromWKT('POINT(0 0)') as point, ST_MakeEnvelope(0, 1,
2, 0) as poly")
+ var actual =
+ baseDf.select(ST_Project("point", 10,
Math.toRadians(45))).first().get(0).toString
+ var expected = "POINT (7.0710678118654755 7.071067811865475)"
+ assertEquals(expected, actual)
+
+ actual = baseDf
+ .select(ST_Project(ST_MakeEnvelope(0, 1, 2, 0), lit(10),
radians(lit(50)), lit(true)))
+ .first()
+ .get(0)
+ .toString()
+ expected = "POINT EMPTY"
+ assertEquals(expected, actual)
+ }
+
it("Passed `ST_MakePolygon`") {
val invalidDf =
sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 0, 1 0, 1 1, 0
0)') AS geom")
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 cb034376f..fa83fbf78 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
@@ -632,6 +632,21 @@ class functionTestScala
result.toText() == "GEOMETRYCOLLECTION (POLYGON ((20 90, 20 160, 70
190, 80 130, 70 70, 20 90)), POLYGON ((20 90, 70 70, 80 130, 160 160, 180 40,
30 20, 20 90), (80 60, 150 80, 120 130, 80 60)), POLYGON ((70 190, 160 160, 80
130, 70 190)), POLYGON ((80 60, 120 130, 150 80, 80 60)))")
}
+ it("Passed ST_Project") {
+ val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POINT(0 0)') as
point")
+ var actual = baseDf.selectExpr("ST_Project(point, 10,
radians(45))").first().get(0).toString
+ var expected = "POINT (7.0710678118654755 7.071067811865475)"
+ assertEquals(expected, actual)
+
+ actual = sparkSession
+ .sql("SELECT ST_Project(ST_MakeEnvelope(0, 1, 2, 0), 10, radians(50),
true)")
+ .first()
+ .get(0)
+ .toString
+ expected = "POINT EMPTY"
+ assertEquals(expected, actual)
+ }
+
it("Passed ST_MakeValid On Invalid Polygon") {
val df = sparkSession.sql(