This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch SEDONA-565 in repository https://gitbox.apache.org/repos/asf/sedona.git
commit 77b400944671b074cad766d7b444ee937780f5cb Author: Furqaan Khan <[email protected]> AuthorDate: Fri Mar 29 01:09:18 2024 -0400 [111] Add ST_ForceRHR (#140) --- docs/api/flink/Function.md | 20 ++++++++++++++++++++ docs/api/snowflake/vector-data/Function.md | 18 ++++++++++++++++++ docs/api/sql/Function.md | 20 ++++++++++++++++++++ .../main/java/org/apache/sedona/flink/Catalog.java | 1 + .../apache/sedona/flink/expressions/Functions.java | 8 ++++++++ .../java/org/apache/sedona/flink/FunctionTest.java | 8 ++++++++ python/sedona/sql/st_functions.py | 9 +++++++++ python/tests/sql/test_dataframe_api.py | 2 ++ python/tests/sql/test_function.py | 6 ++++++ .../sedona/snowflake/snowsql/TestFunctions.java | 9 +++++++++ .../sedona/snowflake/snowsql/TestFunctionsV2.java | 9 +++++++++ .../org/apache/sedona/snowflake/snowsql/UDFs.java | 9 +++++++++ .../org/apache/sedona/snowflake/snowsql/UDFsV2.java | 9 +++++++++ .../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../spark/sql/sedona_sql/expressions/Functions.scala | 7 +++++++ .../sql/sedona_sql/expressions/st_functions.scala | 3 +++ .../apache/sedona/sql/dataFrameAPITestScala.scala | 7 +++++++ .../org/apache/sedona/sql/functionTestScala.scala | 7 +++++++ 18 files changed, 153 insertions(+) diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index 5c6b2f6f3..f3c9ffea2 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -1274,6 +1274,26 @@ Output: POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) ``` +## ST_ForceRHR + +Introduction: Sets the orientation of polygon vertex orderings to follow the Right-Hand-Rule convention. The exterior ring will have a clockwise winding order, while any interior rings are oriented counter-clockwise. This ensures the area bounded by the polygon falls on the right-hand side relative to the ring directions. The function is an alias for [ST_ForcePolygonCW](#st_forcepolygoncw). + +Format: `ST_ForceRHR(geom: Geometry)` + +Since: `vTBD` + +SQL Example: + +```sql +SELECT ST_AsText(ST_ForceRHR(ST_GeomFromText('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))'))) +``` + +Output: + +``` +POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) +``` + ## ST_FrechetDistance Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries, diff --git a/docs/api/snowflake/vector-data/Function.md b/docs/api/snowflake/vector-data/Function.md index 6f02aaed1..f6401d170 100644 --- a/docs/api/snowflake/vector-data/Function.md +++ b/docs/api/snowflake/vector-data/Function.md @@ -1005,6 +1005,24 @@ Output: POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) ``` +## ST_ForceRHR + +Introduction: Sets the orientation of polygon vertex orderings to follow the Right-Hand-Rule convention. The exterior ring will have a clockwise winding order, while any interior rings are oriented counter-clockwise. This ensures the area bounded by the polygon falls on the right-hand side relative to the ring directions. The function is an alias for [ST_ForcePolygonCW](#st_forcepolygoncw). + +Format: `ST_ForceRHR(geom: Geometry)` + +SQL Example: + +```sql +SELECT ST_AsText(ST_ForceRHR(ST_GeomFromText('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))'))) +``` + +Output: + +``` +POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) +``` + ## ST_FrechetDistance Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries, diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index 814c9c376..671d4f696 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -1279,6 +1279,26 @@ Output: POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) ``` +## ST_ForceRHR + +Introduction: Sets the orientation of polygon vertex orderings to follow the Right-Hand-Rule convention. The exterior ring will have a clockwise winding order, while any interior rings are oriented counter-clockwise. This ensures the area bounded by the polygon falls on the right-hand side relative to the ring directions. The function is an alias for [ST_ForcePolygonCW](#st_forcepolygoncw). + +Format: `ST_ForceRHR(geom: Geometry)` + +Since: `vTBD` + +SQL Example: + +```sql +SELECT ST_AsText(ST_ForceRHR(ST_GeomFromText('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))'))) +``` + +Output: + +``` +POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)) +``` + ## ST_FrechetDistance Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries, 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 efd07c3ca..0948d0745 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -142,6 +142,7 @@ public class Catalog { new Functions.ST_NumPoints(), new Functions.ST_Force3D(), new Functions.ST_ForcePolygonCW(), + new Functions.ST_ForceRHR(), new Functions.ST_NRings(), new Functions.ST_IsPolygonCCW(), new Functions.ST_ForcePolygonCCW(), 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 150c2048a..c79cf867d 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 @@ -1022,6 +1022,14 @@ public class Functions { } } + public static class ST_ForceRHR 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 o) { + Geometry geometry = (Geometry) o; + return org.apache.sedona.common.Functions.forcePolygonCW(geometry); + } + } + public static class ST_NRings extends ScalarFunction { @DataTypeHint(value = "Integer") public int eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) throws Exception { 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 c8d1a2f2e..e9f746dde 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -1216,6 +1216,14 @@ public class FunctionTest extends TestBase{ assertEquals(expectedDims, actual); } + @Test + public void testForceRHR() { + Table polyTable = tableEnv.sqlQuery("SELECT ST_ForceRHR(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')) AS polyCW"); + String actual = (String) first(polyTable.select(call(Functions.ST_AsText.class.getSimpleName(), $("polyCW")))).getField(0); + String expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))"; + assertEquals(expected, actual); + } + @Test public void testForcePolygonCW() { Table polyTable = tableEnv.sqlQuery("SELECT ST_ForcePolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')) AS polyCW"); diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index dfd9ae13c..7cd9b7df7 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -1458,6 +1458,15 @@ def ST_ForcePolygonCW(geometry: ColumnOrName) -> Column: """ return _call_st_function("ST_ForcePolygonCW", geometry) +@validate_argument_types +def ST_ForceRHR(geometry: ColumnOrName) -> Column: + """ + Returns + :param geometry: Geometry column to change orientation + :return: Clockwise oriented geometry + """ + return _call_st_function("ST_ForceRHR", geometry) + @validate_argument_types def ST_NRings(geometry: ColumnOrName) -> Column: """ diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 56ec8212a..bf81c282a 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -117,6 +117,7 @@ test_configurations = [ (stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"), (stf.ST_ForcePolygonCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"), (stf.ST_ForcePolygonCCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 0, 3 3, 0 0), (1 1, 2 2, 2 1, 1 1))"), + (stf.ST_ForceRHR, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"), (stf.ST_FrechetDistance, ("point", "line",), "point_and_line", "", 5.0990195135927845), (stf.ST_GeometricMedian, ("multipoint",), "multipoint_geom", "", "POINT (22.500002656424286 21.250001168173426)"), (stf.ST_GeometryN, ("geom", 0), "multipoint", "", "POINT (0 0)"), @@ -278,6 +279,7 @@ wrong_type_configurations = [ (stf.ST_Force_2D, (None,)), (stf.ST_ForcePolygonCW, (None,)), (stf.ST_ForcePolygonCCW, (None,)), + (stf.ST_ForceRHR, (None,)), (stf.ST_GeometryN, (None, 0)), (stf.ST_GeometryN, ("", None)), (stf.ST_GeometryN, ("", 0.0)), diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py index ea8497132..8635cfb8b 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -1374,6 +1374,12 @@ class TestPredicateJoin(TestBase): expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))" assert expected == actual + def test_forceRHR(self): + actualDf = self.spark.sql("SELECT ST_ForceRHR(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')) AS polyCW") + actual = actualDf.selectExpr("ST_AsText(polyCW)").take(1)[0][0] + expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))" + assert expected == actual + def test_nRings(self): expected = 1 actualDf = self.spark.sql("SELECT ST_GeomFromText('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))') AS geom") 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 a0b283156..74860ebde 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 @@ -1033,6 +1033,15 @@ public class TestFunctions extends TestBase { ); } + @Test + public void test_ST_ForceRHR() { + registerUDF("ST_ForceRHR", byte[].class); + verifySqlSingleRes( + "SELECT sedona.ST_AsText(sedona.ST_ForceRHR(sedona.ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')))", + "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))" + ); + } + @Test public void test_ST_LengthSpheroid() { registerUDF("ST_LengthSpheroid", byte[].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 3921b4f09..33aebf589 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 @@ -992,6 +992,15 @@ public class TestFunctionsV2 ); } + @Test + public void test_ST_ForceRHR() { + registerUDFV2("ST_ForceRHR", String.class); + verifySqlSingleRes( + "SELECT ST_AsText(sedona.ST_ForceRHR(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')))", + "POLYGON((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20))" + ); + } + @Test public void test_ST_LengthSpheroid() { registerUDFV2("ST_LengthSpheroid", String.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 bad3f0c89..5546f5cc0 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 @@ -1348,6 +1348,15 @@ public class UDFs { ); } + @UDFAnnotations.ParamMeta(argNames = {"geom"}) + public static byte[] ST_ForceRHR(byte[] geom) { + return GeometrySerde.serialize( + Functions.forcePolygonCW( + GeometrySerde.deserialize(geom) + ) + ); + } + @UDFAnnotations.ParamMeta(argNames = {"geom"}) public static double ST_LengthSpheroid(byte[] geom) { return Spheroid.length( 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 2c6ed7907..128ad2b03 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 @@ -1191,6 +1191,15 @@ public class UDFsV2 ); } + @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}, returnTypes = "Geometry") + public static String ST_ForceRHR(String geom) { + return GeometrySerde.serGeoJson( + Functions.forcePolygonCW( + GeometrySerde.deserGeoJson(geom) + ) + ); + } + @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}) public static double ST_LengthSpheroid(String geom) { return Spheroid.length( 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 bd2e801ad..def481157 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 @@ -150,6 +150,7 @@ object Catalog { function[ST_AsEWKT](), function[ST_Force_2D](), function[ST_ForcePolygonCW](), + function[ST_ForceRHR](), function[ST_ZMax](), function[ST_ZMin](), function[ST_YMax](), 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 4b9c09c03..7e3145bdd 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 @@ -1123,6 +1123,13 @@ case class ST_ForcePolygonCW(inputExpressions: Seq[Expression]) } } +case class ST_ForceRHR(inputExpressions: Seq[Expression]) + extends InferredExpression(Functions.forcePolygonCW _) { + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} + case class ST_NRings(inputExpressions: Seq[Expression]) extends InferredExpression(Functions.nRings _) { protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { 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 b78917303..166b46098 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 @@ -397,6 +397,9 @@ object st_functions extends DataFrameAPI { def ST_ForcePolygonCW(geometry: Column): Column = wrapExpression[ST_ForcePolygonCW](geometry) def ST_ForcePolygonCW(geometry: String): Column = wrapExpression[ST_ForcePolygonCW](geometry) + def ST_ForceRHR(geometry: Column): Column = wrapExpression[ST_ForceRHR](geometry) + def ST_ForceRHR(geometry: String): Column = wrapExpression[ST_ForceRHR](geometry) + def ST_NRings(geometry: Column): Column = wrapExpression[ST_NRings](geometry) def ST_NRings(geometry: String): Column = wrapExpression[ST_NRings](geometry) 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 1ff67eeeb..4341a56ad 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 @@ -1204,6 +1204,13 @@ class dataFrameAPITestScala extends TestBaseScala { assertEquals(expectedGeomDefaultValue, wktWriter.write(actualGeomDefaultValue)) } + it("Passed ST_ForceRHR") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))') AS poly") + val actual = baseDf.select(ST_AsText(ST_ForceRHR("poly"))).take(1)(0).get(0).asInstanceOf[String] + val expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))" + assertEquals(expected, actual) + } + it("Passed ST_ForcePolygonCW") { val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))') AS poly") val actual = baseDf.select(ST_AsText(ST_ForcePolygonCW("poly"))).take(1)(0).get(0).asInstanceOf[String] 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 3fee1e53c..f8cb2b24d 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 @@ -948,6 +948,13 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample assert(expected.equals(actual)) } + it("Should pass ST_ForceRHR") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))') as poly") + val actual = baseDf.selectExpr("ST_AsText(ST_ForceRHR(poly))").first().getString(0) + val expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))" + assert(expected.equals(actual)) + } + it("Should pass ST_IsPolygonCW") { var actual = sparkSession.sql("SELECT ST_IsPolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))'))").first().getBoolean(0) assert(actual == false)
