This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch prepare-1.7.2 in repository https://gitbox.apache.org/repos/asf/sedona.git
commit 637fd5f74475d3b75062fb47d3d7a5f0b0a80142 Author: Furqaan Khan <[email protected]> AuthorDate: Tue Mar 25 15:04:46 2025 -0400 [SEDONA-726] Fix ST_Force_2D and add ST_Force2D (#1876) --- .../java/org/apache/sedona/common/Functions.java | 2 +- .../common/utils/GeometryForce2DTransformer.java | 44 +++++++++++++ .../org/apache/sedona/common/FunctionsTest.java | 75 ++++++++++++++++++++++ docs/api/flink/Function.md | 22 ++++++- docs/api/snowflake/vector-data/Function.md | 26 +++++++- docs/api/sql/Function.md | 22 ++++++- .../main/java/org/apache/sedona/flink/Catalog.java | 1 + .../apache/sedona/flink/expressions/Functions.java | 10 +++ .../java/org/apache/sedona/flink/FunctionTest.java | 13 +++- python/sedona/sql/st_functions.py | 12 ++++ python/tests/sql/test_dataframe_api.py | 2 + python/tests/sql/test_function.py | 15 ++++- .../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../sql/sedona_sql/expressions/Functions.scala | 13 ++++ .../sql/sedona_sql/expressions/st_functions.scala | 3 + 15 files changed, 255 insertions(+), 6 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 c4bcb67086..97e08b7b0f 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -885,7 +885,7 @@ public class Functions { } public static Geometry force2D(Geometry geometry) { - return GeomUtils.get2dGeom(geometry); + return GeometryForce2DTransformer.transform2D(geometry); } public static boolean isEmpty(Geometry geometry) { diff --git a/common/src/main/java/org/apache/sedona/common/utils/GeometryForce2DTransformer.java b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce2DTransformer.java new file mode 100644 index 0000000000..78bc16fa51 --- /dev/null +++ b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce2DTransformer.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sedona.common.utils; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.util.GeometryTransformer; + +public class GeometryForce2DTransformer extends GeometryTransformer { + + @Override + protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { + Coordinate[] newCoords = new Coordinate[coords.size()]; + for (int i = 0; i < coords.size(); i++) { + Coordinate coordinate = coords.getCoordinate(i); + newCoords[i] = new Coordinate(coordinate.getX(), coordinate.getY()); + } + + return createCoordinateSequence(newCoords); + } + + public static Geometry transform2D(Geometry geometry) { + if (geometry.getCoordinates().length == 0) return geometry; + GeometryForce2DTransformer transformer = new GeometryForce2DTransformer(); + return transformer.transform(geometry); + } +} 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 642efe0216..4819384e70 100644 --- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -2133,6 +2133,81 @@ public class FunctionsTest extends TestBase { assertEquals(expectedWKT, actualWKT); } + @Test + public void force2D() throws ParseException { + Geometry geom = + Constructors.geomFromWKT( + "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (3 5, 5 7, 5 3, 3 5), (5 7, 11 5, 5 3, 5 7))", + 0); + byte[] polyBinary = Functions.asEWKB(geom); + Geometry polyFromBinary = Constructors.geomFromWKB(polyBinary); + Geometry forcedPoly = Functions.force2D(Functions.makeValid(polyFromBinary, false)); + String actual = Functions.asWKT(forcedPoly); + String expected = + "POLYGON ((0 10, 10 10, 10 5.333333333333333, 5 7, 3 5, 5 3, 10 4.666666666666667, 10 0, 0 0, 0 10))"; + assertEquals(expected, actual); + + geom = Constructors.geomFromWKT("POINT ZM(1 2 2 5)", 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "POINT (1 2)"; + assertEquals(expected, actual); + + geom = Constructors.geomFromWKT("MULTIPOINT ZM((1 2 2 5), (2 3 2 5))", 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "MULTIPOINT ((1 2), (2 3))"; + assertEquals(expected, actual); + + geom = Constructors.geomFromWKT("LINESTRING (1 2 5, 2 3 5, 3 4 5)", 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "LINESTRING (1 2, 2 3, 3 4)"; + assertEquals(expected, actual); + + // return 2D as is + geom = + Constructors.geomFromWKT( + "MULTILINESTRING ((10 10, 20 20, 30 30), (15 15, 25 25, 35 35))", 0); + Geometry actualGeom = Functions.force2D(geom); + assertTrue(Predicates.equals(actualGeom, geom)); + + geom = Constructors.geomFromWKT("LINEARRING M(30 10 5, 40 40 5, 20 40 5, 10 20 5, 30 10 5)", 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "LINEARRING (30 10, 40 40, 20 40, 10 20, 30 10)"; + assertEquals(expected, actual); + + geom = + Constructors.geomFromWKT( + "POLYGON ZM((0 0 0 0, 10 0 0 0, 10 10 0 0, 0 10 0 0, 0 0 0 0), (4 4 0 0, 4 6 0 0, 6 6 0 0, 6 4 0 0, 4 4 0 0))", + 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (4 4, 4 6, 6 6, 6 4, 4 4))"; + assertEquals(expected, actual); + + geom = + Constructors.geomFromWKT( + "POLYGON ZM((0 0 0 0, 10 0 0 0, 10 10 0 0, 0 0 0 0), (4 4 0 0, 4 6 0 0, 6 6 0 0, 4 4 0 0))", + 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "POLYGON ((0 0, 10 0, 10 10, 0 0), (4 4, 4 6, 6 6, 4 4))"; + assertEquals(expected, actual); + + geom = + Constructors.geomFromWKT( + "MULTIPOLYGON M(((30 10 5, 40 40 5, 20 40 5, 10 20 5, 30 10 5)), ((15 5 5, 10 20 5, 20 30 5, 15 5 5)))", + 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = "MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)), ((15 5, 10 20, 20 30, 15 5)))"; + assertEquals(expected, actual); + + geom = + Constructors.geomFromWKT( + "GEOMETRYCOLLECTION ZM(POINT ZM(10 10 2 5), LINESTRING ZM(15 15 2 5, 25 25 2 5, 35 35 2 5), POLYGON ZM((30 10 2 5, 40 40 2 5, 20 40 2 5, 10 20 2 5, 30 10 2 5)))", + 0); + actual = Functions.asWKT(Functions.force2D(geom)); + expected = + "GEOMETRYCOLLECTION (POINT (10 10), LINESTRING (15 15, 25 25, 35 35), POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)))"; + assertEquals(expected, actual); + } + @Test public void testForce3DM() throws ParseException { Geometry geom = Constructors.geomFromWKT("POINT (1 2)", 0); diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index 8446bc5a69..d5960a2284 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -1439,7 +1439,7 @@ POINT (2 1) ## ST_Force_2D -Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force2D](#st_force2d). Format: `ST_Force_2D (A: Geometry)` @@ -1457,6 +1457,26 @@ Output: POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) ``` +## ST_Force2D + +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force_2D](#st_force_2d). + +Format: `ST_Force2D (A: Geometry)` + +Since: `v1.8.0` + +Example: + +```sql +SELECT ST_Force2D(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 2),(1 1 2,3 1 2,1 3 2,1 1 2))')) +``` + +Output: + +``` +POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) +``` + ## ST_Force3D Introduction: Forces the geometry into a 3-dimensional model so that all output representations will have X, Y and Z coordinates. diff --git a/docs/api/snowflake/vector-data/Function.md b/docs/api/snowflake/vector-data/Function.md index f6b8c447f2..08bfb3b30a 100644 --- a/docs/api/snowflake/vector-data/Function.md +++ b/docs/api/snowflake/vector-data/Function.md @@ -1162,7 +1162,7 @@ Output: `POINT (2 1)` ## ST_Force_2D -Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force2D](#st_force2d). Format: `ST_Force_2D (A:geometry)` @@ -1184,6 +1184,30 @@ Result: +---------------------------------------------------------------+ ``` +## ST_Force2D + +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force_2D](#st_force_2d). + +Format: `ST_Force2D (A:geometry)` + +Example: + +```sql +SELECT ST_AsText( + ST_Force2D(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 2),(1 1 2,3 1 2,1 3 2,1 1 2))')) +) AS geom +``` + +Result: + +``` ++---------------------------------------------------------------+ +|geom | ++---------------------------------------------------------------+ +|POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) | ++---------------------------------------------------------------+ +``` + ## ST_Force3D Introduction: Forces the geometry into a 3-dimensional model so that all output representations will have X, Y and Z coordinates. diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index d4bda5c44a..0cf40f4f2a 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -1495,7 +1495,7 @@ POINT (2 1) ## ST_Force_2D -Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force2D](#st_force2d). Format: `ST_Force_2D (A: Geometry)` @@ -1513,6 +1513,26 @@ Output: POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) ``` +## ST_Force2D + +Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force_2D](#st_force_2d). + +Format: `ST_Force2D (A: Geometry)` + +Since: `v1.8.0` + +SQL Example + +```sql +SELECT ST_Force2D(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 2),(1 1 2,3 1 2,1 3 2,1 1 2))')) +``` + +Output: + +``` +POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) +``` + ## ST_Force3D Introduction: Forces the geometry into a 3-dimensional model so that all output representations will have X, Y and Z coordinates. 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 497aaa5fe6..8654e3bf2a 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -128,6 +128,7 @@ public class Catalog { new Functions.ST_AsGML(), new Functions.ST_AsKML(), new Functions.ST_Force_2D(), + new Functions.ST_Force2D(), new Functions.ST_IsEmpty(), new Functions.ST_X(), new Functions.ST_Y(), 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 86562860fe..af90fabbe6 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 @@ -865,6 +865,16 @@ public class Functions { } } + public static class ST_Force2D 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 geom = (Geometry) o; + return org.apache.sedona.common.Functions.force2D(geom); + } + } + public static class ST_IsEmpty extends ScalarFunction { @DataTypeHint("Boolean") public boolean 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 838f39db26..b85a9f0ae5 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -1126,7 +1126,7 @@ public class FunctionTest extends TestBase { } @Test - public void testForce2D() { + public void testForce_2D() { Table polygonTable = createPolygonTable(1); Table Forced2DTable = polygonTable.select( @@ -1136,6 +1136,17 @@ public class FunctionTest extends TestBase { "POLYGON ((-0.5 -0.5, -0.5 0.5, 0.5 0.5, 0.5 -0.5, -0.5 -0.5))", result.toString()); } + @Test + public void testForce2D() { + Table polygonTable = createPolygonTable(1); + Table Forced2DTable = + polygonTable.select( + call(Functions.ST_Force2D.class.getSimpleName(), $(polygonColNames[0]))); + Geometry result = (Geometry) first(Forced2DTable).getField(0); + assertEquals( + "POLYGON ((-0.5 -0.5, -0.5 0.5, 0.5 0.5, 0.5 -0.5, -0.5 -0.5))", result.toString()); + } + @Test public void testIsEmpty() { Table polygonTable = createPolygonTable(testDataSize); diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index 684ffa8e93..82bead1db2 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -654,6 +654,18 @@ def ST_Force_2D(geometry: ColumnOrName) -> Column: return _call_st_function("ST_Force_2D", geometry) +@validate_argument_types +def ST_Force2D(geometry: ColumnOrName) -> Column: + """Force the geometry column to only output two dimensional representations. + + :param geometry: Geometry column to force to be 2D. + :type geometry: ColumnOrName + :return: Geometry column identical to geometry except with only X and Y coordinates. + :rtype: Column + """ + return _call_st_function("ST_Force2D", geometry) + + @validate_argument_types def ST_GeneratePoints( geometry: ColumnOrName, diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 99d1f17754..22ee40c799 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -543,6 +543,7 @@ test_configurations = [ ), (stf.ST_FlipCoordinates, ("point",), "point_geom", "", "POINT (1 0)"), (stf.ST_Force_2D, ("point",), "point_geom", "", "POINT (0 1)"), + (stf.ST_Force2D, ("point",), "point_geom", "", "POINT (0 1)"), (stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"), ( stf.ST_Force3DM, @@ -1304,6 +1305,7 @@ wrong_type_configurations = [ (stf.ST_ExteriorRing, (None,)), (stf.ST_FlipCoordinates, (None,)), (stf.ST_Force_2D, (None,)), + (stf.ST_Force2D, (None,)), (stf.ST_Force3DM, (None,)), (stf.ST_Force3DZ, (None,)), (stf.ST_Force4D, (None,)), diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py index 96f31e4d94..da9f2bc253 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -2125,7 +2125,7 @@ class TestPredicateJoin(TestBase): ) assert point.take(1)[0][0] == test[2] - def test_st_force2d(self): + def test_st_force_2d(self): tests1 = { "'POINT(0 5)'": "POINT (0 5)", "'POLYGON((0 0 2, 0 5 2, 5 0 2, 0 0 2), (1 1 2, 3 1 2, 1 3 2, 1 1 2))'": "POLYGON ((0 0, 0 5, 5 0, 0 0), (1 1, 3 1, 1 3, 1 1))", @@ -2138,6 +2138,19 @@ class TestPredicateJoin(TestBase): ) assert geom_2d.take(1)[0][0] == expected_geom + def test_st_force2d(self): + tests1 = { + "'POINT(0 5)'": "POINT (0 5)", + "'POLYGON((0 0 2, 0 5 2, 5 0 2, 0 0 2), (1 1 2, 3 1 2, 1 3 2, 1 1 2))'": "POLYGON ((0 0, 0 5, 5 0, 0 0), (1 1, 3 1, 1 3, 1 1))", + "'LINESTRING(0 5 1, 0 0 1, 0 10 2)'": "LINESTRING (0 5, 0 0, 0 10)", + } + + for input_geom, expected_geom in tests1.items(): + geom_2d = self.spark.sql( + "select ST_AsText(ST_Force2D(ST_GeomFromText({})))".format(input_geom) + ) + assert geom_2d.take(1)[0][0] == expected_geom + def test_st_buildarea(self): tests = { "'MULTILINESTRING((0 0, 10 0, 10 10, 0 10, 0 0),(10 10, 20 10, 20 20, 10 20, 10 10))'": "MULTIPOLYGON (((0 0, 0 10, 10 10, 10 0, 0 0)), ((10 10, 10 20, 20 20, 20 10, 10 10)))", 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 57fe3893e0..e0e3503012 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 @@ -187,6 +187,7 @@ object Catalog extends AbstractCatalog { function[ST_PointN](), function[ST_AsEWKT](), function[ST_Force_2D](), + function[ST_Force2D](), function[ST_ForcePolygonCW](), function[ST_ForceRHR](), function[ST_ZMax](), 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 8fb84b3b61..b594d8a43d 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 @@ -1241,6 +1241,19 @@ case class ST_Force_2D(inputExpressions: Seq[Expression]) } } +/* + * Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. + * + * @param inputExpressions + */ +case class ST_Force2D(inputExpressions: Seq[Expression]) + extends InferredExpression(Functions.force2D _) { + + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} + /** * Returns the geometry in EWKT format * 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 84d555ff64..637579e8c2 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 @@ -218,6 +218,9 @@ object st_functions extends DataFrameAPI { def ST_Force_2D(geometry: Column): Column = wrapExpression[ST_Force_2D](geometry) def ST_Force_2D(geometry: String): Column = wrapExpression[ST_Force_2D](geometry) + def ST_Force2D(geometry: Column): Column = wrapExpression[ST_Force2D](geometry) + def ST_Force2D(geometry: String): Column = wrapExpression[ST_Force2D](geometry) + def ST_GeoHash(geometry: Column, precision: Column): Column = wrapExpression[ST_GeoHash](geometry, precision) def ST_GeoHash(geometry: String, precision: Int): Column =
