This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch SEDONA-567 in repository https://gitbox.apache.org/repos/asf/sedona.git
commit f9058542b360cdb0babc5fa1bc22b8c318a08caa Author: Furqaan Khan <[email protected]> AuthorDate: Fri Mar 29 23:37:31 2024 -0400 [TASK-70] Add ST_M (#144) * feat: add ST_M * fix: snowflake registration * fix: remove function from snowflake * docs: remove function from snowflake docs --- .../java/org/apache/sedona/common/Functions.java | 7 +++++++ .../org/apache/sedona/common/FunctionsTest.java | 21 +++++++++++++++++++++ docs/api/flink/Function.md | 20 ++++++++++++++++++++ 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 | 7 +++++++ python/sedona/sql/st_functions.py | 12 ++++++++++++ python/tests/sql/test_dataframe_api.py | 4 ++++ python/tests/sql/test_function.py | 5 +++++ .../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../sql/sedona_sql/expressions/Functions.scala | 8 ++++++++ .../sql/sedona_sql/expressions/st_functions.scala | 3 +++ .../apache/sedona/sql/dataFrameAPITestScala.scala | 6 ++++++ .../org/apache/sedona/sql/functionTestScala.scala | 6 ++++++ 15 files changed, 129 insertions(+) diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java b/common/src/main/java/org/apache/sedona/common/Functions.java index 5cec2a42d..6093ad94a 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -390,6 +390,13 @@ public class Functions { return null; } + public static Double m(Geometry geom) { + if (geom instanceof Point) { + return geom.getCoordinate().getM(); + } + return null; + } + public static double xMin(Geometry geometry) { Coordinate[] points = geometry.getCoordinates(); double min = Double.MAX_VALUE; 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 701a24594..ad9741849 100644 --- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -369,6 +369,27 @@ public class FunctionsTest extends TestBase { assertEquals(actualResult, expectedResult); } + @Test + public void testM() throws ParseException { + Geometry geom = Constructors.geomFromWKT("POINT ZM(1 2 3 4)", 0); + double actual = Functions.m(geom); + double expected = 4; + assertEquals(expected, actual, FP_TOLERANCE2); + + geom = Constructors.geomFromWKT("POINT M(1 2 3)", 0); + actual = Functions.m(geom); + expected = 3; + assertEquals(expected, actual, FP_TOLERANCE2); + + geom = Constructors.geomFromWKT("POINT Z(1 2 3)", 0); + actual = Functions.m(geom); + assertTrue(Double.isNaN(actual)); + + geom = Constructors.geomFromWKT("LINESTRING ZM(1 2 3 4, 2 3 4 5)", 0); + Double actualRes = Functions.m(geom); + assertNull(actualRes); + } + @Test public void dimensionGeometry3D() { Point point3D = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1, 1)); diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index 5c6b2f6f3..c6cef4343 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -2006,6 +2006,26 @@ Output: LINESTRING (69.28469348539744 94.28469348539744, 100 125, 111.70035626068274 140.21046313888758) ``` +## ST_M + +Introduction: Returns M Coordinate of given Point, null otherwise. + +Format: `ST_M(geom: Geometry)` + +Since: `vTBD` + +SQL Example + +```sql +SELECT ST_M(ST_MakePoint(1, 2, 3, 4)) +``` + +Output: + +``` +4.0 +``` + ## ST_MakeLine Introduction: Creates a LineString containing the points of Point, MultiPoint, or LineString geometries. Other geometry types cause an error. diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index 814c9c376..317b8b4a0 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -2013,6 +2013,26 @@ Output: LINESTRING (69.28469348539744 94.28469348539744, 100 125, 111.70035626068274 140.21046313888758) ``` +## ST_M + +Introduction: Returns M Coordinate of given Point, null otherwise. + +Format: `ST_M(geom: Geometry)` + +Since: `vTBD` + +SQL Example + +```sql +SELECT ST_M(ST_MakePoint(1, 2, 3, 4)) +``` + +Output: + +``` +4.0 +``` + ## ST_MakeLine Introduction: Creates a LineString containing the points of Point, MultiPoint, or LineString geometries. Other geometry types cause an error. 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..2452ea03a 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -121,6 +121,7 @@ public class Catalog { new Functions.ST_LineFromMultiPoint(), new Functions.ST_LineMerge(), new Functions.ST_LineSubstring(), + new Functions.ST_M(), new Functions.ST_MakeLine(), new Functions.ST_Polygon(), new Functions.ST_Polygonize(), 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..b06b97d3a 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 @@ -741,6 +741,14 @@ public class Functions { } } + public static class ST_M extends ScalarFunction { + @DataTypeHint("Double") + public Double eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) { + Geometry geom = (Geometry) o; + return org.apache.sedona.common.Functions.m(geom); + } + } + public static class ST_MakeLine 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, 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..e9f4af0f9 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -692,6 +692,13 @@ public class FunctionTest extends TestBase{ assertEquals(7.89, first(pointTable).getField(0)); } + @Test + public void testM() { + Table pointTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POINT ZM(1 2 3 4)') AS point"); + double actual = (double) first(pointTable.select(call(Functions.ST_M.class.getSimpleName(), $("point")))).getField(0); + assertEquals(4, actual, FP_TOLERANCE); + } + @Test public void testZMax() { Table polygonTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(1 3 4, 5 6 7)') AS " + polygonColNames[0]); diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index dfd9ae13c..4afd63c46 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -858,6 +858,18 @@ def ST_LineSubstring(line_string: ColumnOrName, start_fraction: ColumnOrNameOrNu return _call_st_function("ST_LineSubstring", (line_string, start_fraction, end_fraction)) +@validate_argument_types +def ST_M(geom: ColumnOrName) -> Column: + """Return the M coordinate of a point geometry. + + :param point: Point geometry column to get the coordinate for. + :type point: ColumnOrName + :return: M coordinate of the point geometry as a double column. + :rtype: Column + """ + return _call_st_function("ST_M", geom) + + @validate_argument_types def ST_MakeLine(geom1: ColumnOrName, geom2: Optional[ColumnOrName] = None) -> Column: """Creates a LineString containing the points of Point, MultiPoint, or LineString geometries. Other geometry types cause an error. diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 56ec8212a..f9a8aab53 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -141,6 +141,7 @@ test_configurations = [ (stf.ST_LineLocatePoint, ("line", "point"), "line_and_point", "", 0.5), (stf.ST_LineMerge, ("geom",), "multiline_geom", "", "LINESTRING (0 0, 1 0, 1 1, 0 0)"), (stf.ST_LineSubstring, ("line", 0.5, 1.0), "linestring_geom", "", "LINESTRING (2.5 0, 3 0, 4 0, 5 0)"), + (stf.ST_M, ("point",), "4D_point", "", 4.0), (stf.ST_MakeValid, ("geom",), "invalid_geom", "", "MULTIPOLYGON (((1 5, 3 3, 1 1, 1 5)), ((5 3, 7 5, 7 1, 5 3)))"), (stf.ST_MakeLine, ("line1", "line2"), "two_lines", "", "LINESTRING (0 0, 1 1, 0 0, 3 2)"), (stf.ST_Polygon, ("geom", 4236), "closed_linestring_geom", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"), @@ -305,6 +306,7 @@ wrong_type_configurations = [ (stf.ST_LineSubstring, (None, 0.5, 1.0)), (stf.ST_LineSubstring, ("", None, 1.0)), (stf.ST_LineSubstring, ("", 0.5, None)), + (stf.ST_M, (None,)), (stf.ST_MakeValid, (None,)), (stf.ST_MakePolygon, (None,)), (stf.ST_MinimumBoundingCircle, (None,)), @@ -429,6 +431,8 @@ class TestDataFrameAPI(TestBase): return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 0))') AS geom") elif request.param == "two_points": return TestDataFrameAPI.spark.sql("SELECT ST_PointZ(0.0, 0.0, 0.0) AS a, ST_PointZ(3.0, 0.0, 4.0) AS b") + elif request.param == "4D_point": + return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('POINT ZM(1 2 3 4)') AS point") elif request.param == "invalid_geom": return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('POLYGON ((1 5, 1 1, 3 3, 5 3, 7 1, 7 5, 5 3, 3 3, 1 5))') AS geom") elif request.param == "overlapping_polys": diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py index ea8497132..2e6d3414b 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -910,6 +910,11 @@ class TestPredicateJoin(TestBase): # Then assert subdivided.count() == 16 + def test_st_m(self): + baseDf = self.spark.sql("SELECT ST_GeomFromWKT('POINT ZM (1 2 3 4)') AS point") + actual = baseDf.selectExpr("ST_M(point)").take(1)[0][0] + assert actual == 4.0 + def test_st_subdivide_explode_lateral(self): # Given geometry_df = self.__wkt_list_to_data_frame( 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..0a8cb29b2 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 @@ -108,6 +108,7 @@ object Catalog { function[ST_Snap](), function[ST_ClosestPoint](), function[ST_Boundary](), + function[ST_M](), function[ST_MinimumBoundingRadius](), function[ST_MinimumBoundingCircle](BufferParameters.DEFAULT_QUADRANT_SEGMENTS * 6), function[ST_EndPoint](), 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..599159d2b 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 @@ -511,6 +511,14 @@ case class ST_MinimumBoundingCircle(inputExpressions: Seq[Expression]) } } +case class ST_M(inputExpressions: Seq[Expression]) + extends InferredExpression(Functions.m _) { + + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} + /** * Return a linestring being a substring of the input one starting and ending at the given fractions of total 2d length. 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..0a432fe55 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 @@ -212,6 +212,9 @@ object st_functions extends DataFrameAPI { def ST_LineSubstring(lineString: Column, startFraction: Column, endFraction: Column): Column = wrapExpression[ST_LineSubstring](lineString, startFraction, endFraction) def ST_LineSubstring(lineString: String, startFraction: Double, endFraction: Double): Column = wrapExpression[ST_LineSubstring](lineString, startFraction, endFraction) + def ST_M(geoms: Column): Column = wrapExpression[ST_M](geoms) + def ST_M(geoms: String): Column = wrapExpression[ST_M](geoms) + def ST_MakeLine(geoms: Column): Column = wrapExpression[ST_MakeLine](geoms) def ST_MakeLine(geoms: String): Column = wrapExpression[ST_MakeLine](geoms) def ST_MakeLine(geom1: Column, geom2: Column): Column = wrapExpression[ST_MakeLine](geom1, geom2) 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..ae2c3bb40 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 @@ -52,6 +52,12 @@ class dataFrameAPITestScala extends TestBaseScala { assert(actualResult == expectedResult) } + it("Passed ST_M") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POINT ZM (10 20 30 40)') AS point") + val actual = baseDf.select(ST_M("point")).first().getDouble(0) + assert(actual == 40.0) + } + it("passed st_makepoint") { val df = sparkSession.sql("SELECT 0.0 AS x, 1.0 AS y, 2.0 AS z").select(ST_AsText(ST_MakePoint("x", "y", "z"))) val actualResult = df.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..8626409b4 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 @@ -378,6 +378,12 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample assert(!testtable.take(1)(0).get(1).asInstanceOf[Boolean]) } + it("Passed ST_M") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POINT ZM (1 2 3 4)') AS point") + val actual = baseDf.selectExpr("ST_M(point)").first().getDouble(0) + assert(actual == 4.0) + } + it("Passed ST_MakeLine") { val testtable = sparkSession.sql( """SELECT
