From 0bc3d2ac13a9dd251e9d52c72a0998a9b1d1a4b2 Mon Sep 17 00:00:00 2001 From: Furqaanahmed Khan Date: Thu, 19 Sep 2024 13:51:22 -0400 Subject: [PATCH] feat: add ST_Project --- .../org/apache/sedona/common/Functions.java | 37 ++++++++++++++++ .../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 +++++++++++++++++++ .../java/org/apache/sedona/flink/Catalog.java | 1 + .../sedona/flink/expressions/Functions.java | 23 ++++++++++ .../org/apache/sedona/flink/FunctionTest.java | 15 +++++++ python/sedona/sql/st_functions.py | 15 +++++++ python/tests/sql/test_dataframe_api.py | 7 +++- python/tests/sql/test_function.py | 13 ++++++ .../snowflake/snowsql/TestFunctions.java | 8 ++++ .../snowflake/snowsql/TestFunctionsV2.java | 8 ++++ .../apache/sedona/snowflake/snowsql/UDFs.java | 12 ++++++ .../sedona/snowflake/snowsql/UDFsV2.java | 18 ++++++++ .../org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../sedona_sql/expressions/Functions.scala | 10 +++++ .../sedona_sql/expressions/st_functions.scala | 13 ++++++ .../apache/sedona/sql/PreserveSRIDSuite.scala | 1 + .../sedona/sql/dataFrameAPITestScala.scala | 19 ++++++++- .../apache/sedona/sql/functionTestScala.scala | 19 +++++++++ 21 files changed, 377 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 e4f2a088c8..bd1a7d27dc 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.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,42 @@ public static Integer dimension(Geometry geometry) { 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())); + } + } + + int orbit = (int) Math.floor(azimuth / Angle.PI_TIMES_2); + azimuth -= Angle.PI_TIMES_2 * orbit; + 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 b22e09e19b..dbec7c6517 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 void dimensionGeometryEmpty() { 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 b5c11c37a6..82444ad312 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 7848f7bcfd..671588c99a 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 46063b9c71..bac7727134 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 61d88a5c0d..82b8cbc4c5 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 static UserDefinedFunction[] getFuncs() { 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 38e41fce2d..85f634083e 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 Geometry eval( } } + 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 e08813da78..ccbb586f69 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 void testPointOnSurface() { 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 fccc1c5653..734ff1ccc2 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -1099,6 +1099,21 @@ 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) -> Column: """Create a polygon geometry from a linestring describing the exterior ring as well as an array of linestrings describing holes. diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 08fa955f74..2c44315f9d 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 @@ -189,6 +189,8 @@ (stf.ST_Points, ("line",), "linestring_geom", "ST_Normalize(geom)", "MULTIPOINT (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)"), (stf.ST_Polygon, ("geom", 4236), "closed_linestring_geom", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"), (stf.ST_Polygonize, ("geom",), "noded_linework", "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",), "closed_linestring_geom", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"), (stf.ST_MinimumClearance, ("geom",), "invalid_geom", "", 2.0), (stf.ST_MinimumClearanceLine, ("geom",), "invalid_geom", "", "LINESTRING (5 3, 3 3)"), @@ -429,6 +431,9 @@ (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 f139c6963e..7746d47b5e 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -1223,6 +1223,19 @@ def test_st_polygonize(self): 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 40bc28b8b2..93782d9cb7 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 void test_ST_Polygonize() { 4.0); } + @Test + public void test_ST_Project() { + registerUDF("ST_Project", byte[].class); + verifySqlSingleRes( + "select sedona.ST_AsWKT(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 b3c21f7c04..0ca75b2f7c 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 void test_ST_Polygonize() { 4.0); } + @Test + public void test_ST_Project() { + registerUDFV2("ST_Project", String.class); + verifySqlSingleRes( + "select ST_AsWKT(sedona.ST_Project(ST_GeomFromWKT('POINT (0 0)'), 1000, 10))", + "POINT(-544.0211108893703 -839.0715290764522)"); + } + @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 a5622039eb..40cd2585bd 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 static byte[] ST_PolygonFromEnvelope(double minX, double minY, double max 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 75109cb4aa..f0c43981df 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 static String ST_Polygonize(String geometry) { 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 80ffdabb71..2c06931105 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 0e64d5930c..e309dcc7f9 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 0101978b10..4699f43ff5 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 74d80b8e4f..17c0f2e98b 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 2f8faeb0c8..981f88fd5b 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 cb034376f1..a9ca133b5c 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,25 @@ 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) + + sparkSession + .sql("select ST_AsText(ST_Project(ST_GeomFromText('POLYGON ((1 5, 1 1, 3 3, 5 3, 1 5))'), 25, radians(270), true))") + .show(false) + } + it("Passed ST_MakeValid On Invalid Polygon") { val df = sparkSession.sql(