From f8800d6b18bb1b4f15aabe1949153e2061dc86e3 Mon Sep 17 00:00:00 2001 From: Nilesh Gajwani Date: Thu, 22 Jun 2023 21:33:32 -0700 Subject: [PATCH] [SEDONA-213] Add ST_BoundingDiagonal (#863) --- .../org/apache/sedona/common/Functions.java | 34 ++++++++++ .../apache/sedona/common/FunctionsTest.java | 62 ++++++++++++++++++- docs/api/flink/Function.md | 27 ++++++++ docs/api/sql/Function.md | 27 ++++++++ .../java/org/apache/sedona/flink/Catalog.java | 1 + .../sedona/flink/expressions/Functions.java | 9 +++ .../org/apache/sedona/flink/FunctionTest.java | 9 +++ python/sedona/sql/st_functions.py | 13 +++- python/tests/sql/test_dataframe_api.py | 1 + python/tests/sql/test_function.py | 6 ++ .../org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../sedona_sql/expressions/Functions.scala | 6 ++ .../sedona_sql/expressions/st_functions.scala | 6 ++ .../sedona/sql/dataFrameAPITestScala.scala | 9 +++ .../apache/sedona/sql/functionTestScala.scala | 14 +++++ 15 files changed, 222 insertions(+), 3 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 1efd0f474b..000180ba5c 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -931,4 +931,38 @@ public static Geometry geometricMedian(Geometry geometry) throws Exception { return geometricMedian(geometry, DEFAULT_TOLERANCE, DEFAULT_MAX_ITER, false); } + public static Geometry boundingDiagonal(Geometry geometry) { + if (geometry.isEmpty()) { + return GEOMETRY_FACTORY.createLineString(); + }else { + //Envelope envelope = geometry.getEnvelopeInternal(); + // if (envelope.isNull()) return GEOMETRY_FACTORY.createLineString(); + Double startX = null, startY = null, startZ = null, + endX = null, endY = null, endZ = null; + boolean is3d = !Double.isNaN(geometry.getCoordinate().z); + for (Coordinate currCoordinate : geometry.getCoordinates()) { + startX = startX == null ? currCoordinate.getX() : Math.min(startX, currCoordinate.getX()); + startY = startY == null ? currCoordinate.getY() : Math.min(startY, currCoordinate.getY()); + + endX = endX == null ? currCoordinate.getX() : Math.max(endX, currCoordinate.getX()); + endY = endY == null ? currCoordinate.getY() : Math.max(endY, currCoordinate.getY()); + if (is3d) { + Double geomZ = currCoordinate.getZ(); + startZ = startZ == null ? currCoordinate.getZ() : Math.min(startZ, currCoordinate.getZ()); + endZ = endZ == null ? currCoordinate.getZ() : Math.max(endZ, currCoordinate.getZ()); + } + } + Coordinate startCoordinate; + Coordinate endCoordinate; + if (is3d) { + startCoordinate = new Coordinate(startX, startY, startZ); + endCoordinate = new Coordinate(endX, endY, endZ); + }else { + startCoordinate = new Coordinate(startX, startY); + endCoordinate = new Coordinate(endX, endY); + } + return GEOMETRY_FACTORY.createLineString(new Coordinate[] {startCoordinate, endCoordinate}); + } + } + } 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 3a1745b6b4..f14cf260a4 100644 --- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -23,8 +23,6 @@ import org.locationtech.jts.geom.*; import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.io.WKTWriter; - -import javax.sound.sampled.Line; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -857,4 +855,64 @@ public void translateHybridGeomCollectionDeltaZ() { assertEquals(wktWriter3D.write(expectedPoint3D), wktWriter3D.write(actualGeometry.getGeometryN(0).getGeometryN(1))); assertEquals(emptyLineString.toText(), actualGeometry.getGeometryN(0).getGeometryN(2).toText()); } + @Test + public void boundingDiagonalGeom2D() { + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 1, 2, 1, 2, 2, 2, 0, 1, 0)); + String expected = "LINESTRING (1 0, 2 2)"; + String actual = Functions.boundingDiagonal(polygon).toText(); + assertEquals(expected, actual); + } + + @Test + public void boundingDiagonalGeom3D() { + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 1, 3, 2, 2, 2, 4, 5, 1, 1, 1, 1, 0, 1)); + WKTWriter wktWriter = new WKTWriter(3); + String expected = "LINESTRING Z(1 0 1, 3 4 5)"; + String actual = wktWriter.write(Functions.boundingDiagonal(polygon)); + assertEquals(expected, actual); + } + + @Test + public void boundingDiagonalGeomEmpty() { + LineString emptyLineString = GEOMETRY_FACTORY.createLineString(); + String expected = "LINESTRING EMPTY"; + String actual = Functions.boundingDiagonal(emptyLineString).toText(); + assertEquals(expected, actual); + } + + @Test + public void boundingDiagonalGeomCollection2D() { + // ("'GEOMETRYCOLLECTION (MULTIPOLYGON (((1 1, 1 -1, 2 2, 2 9, 9 1, 1 1)), ((5 5, 4 4, 2 2 , 5 5))), POINT (-1 0))'") -> "'LINESTRING (-1 -1, 9 9)'" + Polygon polygon1 = GEOMETRY_FACTORY.createPolygon(coordArray(1, 1, 1, -1, 2, 2, 2, 9, 9, 1, 1, 1)); + Polygon polygon2 = GEOMETRY_FACTORY.createPolygon(coordArray(5, 5, 4, 4, 2, 2, 5, 5)); + MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {polygon1, polygon2}); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(2, 2, 3, 3)); + Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(-1, 0)); + Geometry geometryCollection = GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, lineString, point}); + String expected = "LINESTRING (-1 -1, 9 9)"; + String actual = Functions.boundingDiagonal(geometryCollection).toText(); + assertEquals(expected, actual); + } + + @Test + public void boundingDiagonalGeomCollection3D() { + Polygon polygon1 = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 1, 4, 1, -1, 6, 2, 2, 4, 2, 9, 4, 9, 1, 0, 1, 1, 4)); + Polygon polygon2 = GEOMETRY_FACTORY.createPolygon(coordArray3d(5, 5, 1, 4, 4, 1, 2, 2, 2, 5, 5, 1)); + MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {polygon1, polygon2}); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray3d(2, 2, 9, 3, 3, -5)); + Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(-1, 9, 1)); + Geometry geometryCollection = GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, lineString, point}); + String expected = "LINESTRING Z(-1 -1 -5, 9 9 9)"; + WKTWriter wktWriter = new WKTWriter(3); + String actual = wktWriter.write(Functions.boundingDiagonal(geometryCollection)); + assertEquals(expected, actual); + } + + @Test + public void boundingDiagonalSingleVertex() { + Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(10, 5)); + String expected = "LINESTRING (10 5, 10 5)"; + String actual = Functions.boundingDiagonal(point).toText(); + assertEquals(expected, actual); + } } diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index fb68b55c93..345f8c5453 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -208,6 +208,33 @@ SELECT ST_Boundary(ST_GeomFromText('POLYGON ((1 1, 0 0, -1 1, 1 1))')) Output: `LINEARRING (1 1, 0 0, -1 1, 1 1)` +## ST_BoundingDiagonal + +Introduction: Returns a linestring spanning minimum and maximum values of each dimension of the given geometry's coordinates as its start and end point respectively. +If an empty geometry is provided, the returned LineString is also empty. +If a single vertex (POINT) is provided, the returned LineString has both the start and end points same as the points coordinates + +Format: `ST_BoundingDiagonal(geom: geometry)` + +Since: `v1.5.0` + +Example: +```sql +SELECT ST_BoundingDiagonal(ST_GeomFromWKT(geom)) +``` + +Input: `POLYGON ((1 1 1, 3 3 3, 0 1 4, 4 4 0, 1 1 1))` + +Output: `LINESTRING Z(0 1 1, 4 4 4)` + +Input: `POINT (10 10)` + +Output: `LINESTRING (10 10, 10 10)` + +Input: `GEOMETRYCOLLECTION(POLYGON ((5 5 5, -1 2 3, -1 -1 0, 5 5 5)), POINT (10 3 3))` + +Output: `LINESTRING Z(-1 -1 0, 10 5 5)` + ## ST_Buffer Introduction: Returns a geometry/geography that represents all points whose distance from this Geometry/geography is less than or equal to distance. diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index 4ef78b148e..2e3d71ad1a 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -203,6 +203,33 @@ SELECT ST_Boundary(ST_GeomFromText('POLYGON((1 1,0 0, -1 1, 1 1))')) Output: `LINESTRING (1 1, 0 0, -1 1, 1 1)` +## ST_BoundingDiagonal + +Introduction: Returns a linestring spanning minimum and maximum values of each dimension of the given geometry's coordinates as its start and end point respectively. +If an empty geometry is provided, the returned LineString is also empty. +If a single vertex (POINT) is provided, the returned LineString has both the start and end points same as the points coordinates + +Format: `ST_BoundingDiagonal(geom: geometry)` + +Since: `v1.5.0` + +Spark SQL Example: +```sql +SELECT ST_BoundingDiagonal(ST_GeomFromWKT(geom)) +``` + +Input: `POLYGON ((1 1 1, 3 3 3, 0 1 4, 4 4 0, 1 1 1))` + +Output: `LINESTRING Z(0 1 1, 4 4 4)` + +Input: `POINT (10 10)` + +Output: `LINESTRING (10 10, 10 10)` + +Input: `GEOMETRYCOLLECTION(POLYGON ((5 5 5, -1 2 3, -1 -1 0, 5 5 5)), POINT (10 3 3))` + +Output: `LINESTRING Z(-1 -1 0, 10 5 5)` + ## ST_Buffer Introduction: Returns a geometry/geography that represents all points whose distance from this Geometry/geography is less than or equal to distance. 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 8d3559599e..51b208ea1d 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -99,6 +99,7 @@ public static UserDefinedFunction[] getFuncs() { new Functions.ST_Force3D(), new Functions.ST_NRings(), new Functions.ST_Translate(), + new Functions.ST_BoundingDiagonal(), }; } 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 79adcc8d23..b3e92290eb 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 @@ -623,4 +623,13 @@ public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.j } } + public static class ST_BoundingDiagonal 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.boundingDiagonal(geometry); + } + + } } 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 0bf2536d7f..e49172a3a3 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -736,4 +736,13 @@ public void testTranslate() { assertEquals(expected, actual); } + @Test + public void testBoundingDiagonal() { + Table polyTable = tableEnv.sqlQuery("SELECT ST_BoundingDiagonal(ST_GeomFromWKT('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'))" +" AS " + polygonColNames[0]); + polyTable = polyTable.select(call(Functions.ST_AsText.class.getSimpleName(), $(polygonColNames[0]))); + String expected = "LINESTRING (1 0, 2 1)"; + String actual = (String) first(polyTable).getField(0); + assertEquals(expected, actual); + } + } diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index 32a2f2728c..be60c6e677 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -111,7 +111,8 @@ "ST_NumPoints", "ST_Force3D", "ST_NRings", - "ST_Translate" + "ST_Translate", + "ST_BoundingDiagonal" ] @@ -1277,3 +1278,13 @@ def ST_Translate(geometry: ColumnOrName, deltaX: Union[ColumnOrName, float], del args = (geometry, deltaX, deltaY, deltaZ) return _call_st_function("ST_Translate", args) +@validate_argument_types +def ST_BoundingDiagonal(geometry: ColumnOrName) -> Column: + """ + Returns a LineString with the min/max values of each dimension of the bounding box of the given geometry as its + start/end coordinates. + :param geometry: Geometry to return bounding diagonal of. + :return: LineString spanning min and max values of each dimension of the given geometry + """ + + return _call_st_function("ST_BoundingDiagonal", geometry) diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 56229b0e02..6aae5aef29 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -65,6 +65,7 @@ (stf.ST_Boundary, ("geom",), "triangle_geom", "", "LINESTRING (0 0, 1 0, 1 1, 0 0)"), (stf.ST_Buffer, ("point", 1.0), "point_geom", "ST_PrecisionReduce(geom, 2)", "POLYGON ((0.98 0.8, 0.92 0.62, 0.83 0.44, 0.71 0.29, 0.56 0.17, 0.38 0.08, 0.2 0.02, 0 0, -0.2 0.02, -0.38 0.08, -0.56 0.17, -0.71 0.29, -0.83 0.44, -0.92 0.62, -0.98 0.8, -1 1, -0.98 1.2, -0.92 1.38, -0.83 1.56, -0.71 1.71, -0.56 1.83, -0.38 1.92, -0.2 1.98, 0 2, 0.2 1.98, 0.38 1.92, 0.56 1.83, 0.71 1.71, 0.83 1.56, 0.92 1.38, 0.98 1.2, 1 1, 0.98 0.8))"), (stf.ST_BuildArea, ("geom",), "multiline_geom", "ST_Normalize(geom)", "POLYGON ((0 0, 1 1, 1 0, 0 0))"), + (stf.ST_BoundingDiagonal, ("geom",), "square_geom", "ST_BoundingDiagonal(geom)", "LINESTRING (1 0, 2 1)"), (stf.ST_Centroid, ("geom",), "triangle_geom", "ST_PrecisionReduce(geom, 2)", "POINT (0.67 0.33)"), (stf.ST_Collect, (lambda: f.expr("array(a, b)"),), "two_points", "", "MULTIPOINT Z (0 0 0, 3 0 4)"), (stf.ST_Collect, ("a", "b"), "two_points", "", "MULTIPOINT Z (0 0 0, 3 0 4)"), diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py index 6897fb8ee5..18b28984ef 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -1098,3 +1098,9 @@ def test_translate(self): actual = actualDf.selectExpr("ST_AsText(geom)").take(1)[0][0] assert expected == actual + def test_boundingDiagonal(self): + expected = "LINESTRING (1 0, 2 1)" + actual_df = self.spark.sql("SELECT ST_BoundingDiagonal(ST_GeomFromText('POLYGON ((1 0, 1 1, 2 1, 2 0, " + "1 0))')) AS geom") + actual = actual_df.selectExpr("ST_AsText(geom)").take(1)[0][0] + assert expected == actual diff --git a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala index 9df4bb31a3..f9929c56ad 100644 --- a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala +++ b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala @@ -151,6 +151,7 @@ object Catalog { function[ST_Force3D](0.0), function[ST_NRings](), function[ST_Translate](0.0), + function[ST_BoundingDiagonal](), // Expression for rasters function[RS_NormalizedDifference](), function[RS_Mean](), diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala index 41052806dd..cc90ca01df 100644 --- a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala @@ -1010,3 +1010,9 @@ case class ST_Translate(inputExpressions: Seq[Expression]) } } +case class ST_BoundingDiagonal(inputExpressions: Seq[Expression]) + extends InferredUnaryExpression(Functions.boundingDiagonal) with FoldableExpression { + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala index 6e110e6ad0..f603cf278e 100644 --- a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala @@ -327,4 +327,10 @@ object st_functions extends DataFrameAPI { def ST_Translate(geometry: String, deltaX: Double, deltaY: Double): Column = wrapExpression[ST_Translate](geometry, deltaX, deltaY, 0.0) + def ST_BoundingDiagonal(geometry: Column) = + wrapExpression[ST_BoundingDiagonal](geometry) + + def ST_BoundingDiagonal(geometry: String) = + wrapExpression[ST_BoundingDiagonal](geometry) + } diff --git a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala index a9aab8444e..8e6ed18bd8 100644 --- a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala +++ b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala @@ -994,5 +994,14 @@ class dataFrameAPITestScala extends TestBaseScala { val expectedDefaultValue = "POLYGON Z((3 3 1, 3 4 1, 4 4 1, 4 3 1, 3 3 1))" assert(expectedDefaultValue == actualDefaultValue) } + + it("Passed ST_BoundingDiagonal") { + val polyDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((1 0 1, 2 3 2, 5 0 1, 5 2 9, 1 0 1))') AS geom") + val df = polyDf.select(ST_BoundingDiagonal("geom")) + val wKTWriter = new WKTWriter(3); + val expected = "LINESTRING Z(1 0 1, 5 3 9)" + val actual = wKTWriter.write(df.take(1)(0).get(0).asInstanceOf[Geometry]) + assertEquals(expected, actual) + } } } diff --git a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala index 2bab5e5413..fa3cfc26c8 100644 --- a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala +++ b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala @@ -1973,4 +1973,18 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample assertEquals(expectedDefaultValue, actualDefaultValue) } } + + it ("should pass ST_BoundingDiagonal") { + val geomTestCases = Map ( + ("'POINT (10 10)'")-> "'LINESTRING (10 10, 10 10)'", + ("'POLYGON ((1 1 1, 4 4 4, 0 9 3, 0 9 9, 1 1 1))'") -> "'LINESTRING Z(0 1 1, 4 9 9)'", + ("'GEOMETRYCOLLECTION (MULTIPOLYGON (((1 1, 1 -1, 2 2, 2 9, 9 1, 1 1)), ((5 5, 4 4, 2 2 , 5 5))), POINT (-1 0))'") -> "'LINESTRING (-1 -1, 9 9)'" + ) + for (((geom), expectedResult) <- geomTestCases) { + val df = sparkSession.sql(s"SELECT ST_AsText(ST_BoundingDiagonal(ST_GeomFromWKT($geom))) AS geom, " + s"$expectedResult") + val actual = df.take(1)(0).get(0).asInstanceOf[String] + val expected = df.take(1)(0).get(1).asInstanceOf[String] + assertEquals(expected, actual) + } + } }