You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2022/12/22 07:16:16 UTC

[incubator-sedona] branch master updated: [SEDONA-215] Add ST_ConcaveHull (#737)

This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new f6cd274f [SEDONA-215] Add ST_ConcaveHull (#737)
f6cd274f is described below

commit f6cd274f91fa4b870c1e346c521aa8bde04b472d
Author: awadhesh singh <da...@gmail.com>
AuthorDate: Thu Dec 22 12:46:10 2022 +0530

    [SEDONA-215] Add ST_ConcaveHull (#737)
---
 .../java/org/apache/sedona/common/Functions.java    |  9 +++++++++
 docs/api/flink/Function.md                          | 21 +++++++++++++++++++++
 docs/api/sql/Function.md                            | 16 ++++++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java  |  1 +
 .../apache/sedona/flink/expressions/Functions.java  | 16 ++++++++++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java  | 13 +++++++++++++
 python/sedona/sql/st_functions.py                   | 19 ++++++++++++++++++-
 python/tests/sql/test_dataframe_api.py              |  3 +++
 python/tests/sql/test_function.py                   | 16 ++++++++++++++++
 .../streaming/spark/test_constructor_functions.py   |  5 +++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala   |  1 +
 .../sql/sedona_sql/expressions/Functions.scala      | 13 +++++++++++++
 .../sql/sedona_sql/expressions/st_functions.scala   |  5 +++++
 .../apache/sedona/sql/dataFrameAPITestScala.scala   | 10 +++++++---
 .../org/apache/sedona/sql/functionTestScala.scala   | 13 +++++++++++++
 15 files changed, 157 insertions(+), 4 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 af83d434..f6e24e55 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -19,6 +19,7 @@ import org.apache.sedona.common.utils.GeometryGeoHashEncoder;
 import org.geotools.geometry.jts.JTS;
 import org.geotools.referencing.CRS;
 import org.locationtech.jts.algorithm.MinimumBoundingCircle;
+import org.locationtech.jts.algorithm.hull.ConcaveHull;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryCollection;
@@ -435,6 +436,14 @@ public class Functions {
         return GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[0]));
     }
 
+    public static Geometry concaveHull(Geometry geometry, double pctConvex, boolean allowHoles){
+        ConcaveHull concave_hull = new ConcaveHull(geometry);
+        concave_hull.setMaximumEdgeLengthRatio(pctConvex);
+        concave_hull.setHolesAllowed(allowHoles);
+
+        return concave_hull.getHull();
+    }
+
     public static Geometry convexHull(Geometry geometry) {
         return geometry.convexHull();
     }
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 5acc7c14..55cc9b6e 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -223,6 +223,27 @@ Input: `MULTILINESTRING((0 0, 10 0, 10 10, 0 10, 0 0),(10 10, 20 10, 20 20, 10 2
 
 Output: `MULTIPOLYGON(((0 0,0 10,10 10,10 0,0 0)),((10 10,10 20,20 20,20 10,10 10)))`
 
+## ST_ConcaveHull
+
+Introduction: Return the Concave Hull of polgyon A, with alpha set to pctConvex[0, 1] in the Delaunay Triangulation method, the concave hull will not contain a hole unless allowHoles is set to true
+
+Format: `ST_ConcaveHull (A:geometry, pctConvex:float)`
+
+Format: `ST_ConcaveHull (A:geometry, pctConvex:float, allowHoles:Boolean)`
+
+Since: `v1.3.2`
+
+Example:
+
+```SQL
+SELECT ST_ConcaveHull(polygondf.countyshape, pctConvex)`
+FROM polygondf
+```
+
+Input: `Polygon ((0 0, 1 2, 2 2, 3 2, 5 0, 4 0, 3 1, 2 1, 1 0, 0 0))`
+
+Output: `POLYGON ((1 2, 2 2, 3 2, 5 0, 4 0, 1 0, 0 0, 1 2))`
+
 ## ST_Distance
 
 Introduction: Return the Euclidean distance between A and B
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 37e41681..82a104b9 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -332,6 +332,22 @@ Result:
 +----------------------------------------------------------------------------+
 ```
 
+## ST_ConcaveHull
+
+Introduction: Return the Concave Hull of polgyon A, with alpha set to pctConvex[0, 1] in the Delaunay Triangulation method, the concave hull will not contain a hole unless allowHoles is set to true
+
+Format: `ST_ConcaveHull (A:geometry, pctConvex:float)`
+
+Format: `ST_ConcaveHull (A:geometry, pctConvex:float, allowHoles:Boolean)`
+
+Since: `v1.3.2`
+
+Spark SQL example:
+```SQL
+SELECT ST_ConcaveHull(polygondf.countyshape, pctConvex)`
+FROM polygondf
+```
+
 ## ST_ConvexHull
 
 Introduction: Return the Convex Hull of polgyon A
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 40c9e450..96f6b4ee 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -40,6 +40,7 @@ public class Catalog {
                 new Functions.ST_Azimuth(),
                 new Functions.ST_Boundary(),
                 new Functions.ST_Buffer(),
+                new Functions.ST_ConcaveHull(),
                 new Functions.ST_Envelope(),
                 new Functions.ST_Distance(),
                 new Functions.ST_3DDistance(),
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 6d046f4c..c104e9e0 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
@@ -55,6 +55,22 @@ public class Functions {
         }
     }
 
+    public static class ST_ConcaveHull 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,
+                             @DataTypeHint("Double") Double pctConvex) {
+            Geometry geom = (Geometry) o;
+            return org.apache.sedona.common.Functions.concaveHull(geom, pctConvex, false);
+        }
+
+        @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,
+                             @DataTypeHint("Double") Double pctConvex, @DataTypeHint("Boolean") Boolean allowHoles) {
+            Geometry geom = (Geometry) o;
+            return org.apache.sedona.common.Functions.concaveHull(geom, pctConvex, allowHoles);
+        }
+    }
+
     public static class ST_Envelope 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) {
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 2c212231..cf04865d 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -72,6 +72,19 @@ public class FunctionTest extends TestBase{
         assert(result instanceof Polygon);
     }
 
+    @Test
+    public void testConcaveHull() {
+        Table polygonTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('Polygon ((0 0, 1 2, 2 2, 3 2, 5 0, 4 0, 3 1, 2 1, 1 0, 0 0))') as geom");
+        Table concaveHullPolygonTable = polygonTable.select(call(Functions.ST_ConcaveHull.class.getSimpleName(), $("geom"), 1.0, true));
+        Geometry result = (Geometry) first(concaveHullPolygonTable).getField(0);
+        assertEquals("POLYGON ((1 2, 2 2, 3 2, 5 0, 4 0, 1 0, 0 0, 1 2))", result.toString());
+
+        Table polygonTable2 = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 0))') as geom");
+        Table concaveHullPolygonTable2 = polygonTable2.select(call(Functions.ST_ConcaveHull.class.getSimpleName(), $("geom"), 1.0));
+        Geometry result2 = (Geometry) first(concaveHullPolygonTable2).getField(0);
+        assertEquals("POLYGON ((0 0, 1 1, 1 0, 0 0))", result2.toString());
+    }
+
     @Test
     public void testEnvelope() {
         Table linestringTable = createLineStringTable(1);
diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py
index 4de6e050..a33b76bd 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -41,6 +41,7 @@ __all__ = [
     "ST_Centroid",
     "ST_Collect",
     "ST_CollectionExtract",
+    "ST_ConcaveHull",
     "ST_ConvexHull",
     "ST_Difference",
     "ST_Distance",
@@ -334,6 +335,22 @@ def ST_CollectionExtract(collection: ColumnOrName, geom_type: Optional[Union[Col
     return _call_st_function("ST_CollectionExtract", args)
 
 
+@validate_argument_types
+def ST_ConcaveHull(geometry: ColumnOrName, pctConvex: Union[ColumnOrName, float], allowHoles: Optional[Union[ColumnOrName, bool]] = None) -> Column:
+    """Generate the cancave hull of a geometry column.
+
+    :param geometry: Geometry column to generate a cancave hull for.
+    :type geometry: ColumnOrName
+    :param pctConvex: value between 0 and 1, controls the concaveness of the computed hull.
+    :type pctConvex: Union[ColumnOrName, float]
+    :param allowHoles: The computed hull will not contain holes unless allowHoles is specified as true
+    :type allowHoles: Optional[Union[ColumnOrName, bool]], optional
+    :return: Concave hull of geometry as a geometry column.
+    :rtype: Column
+    """
+    args = (geometry, pctConvex) if allowHoles is None else (geometry, pctConvex, allowHoles)
+    return _call_st_function("ST_ConcaveHull", args)
+
 @validate_argument_types
 def ST_ConvexHull(geometry: ColumnOrName) -> Column:
     """Generate the convex hull of a geometry column.
@@ -1106,4 +1123,4 @@ def ST_ZMin(geometry: ColumnOrName) -> Column:
     :return: Minimum Z coordinate for the geometry as a double column.
     :rtype: Column
     """
-    return _call_st_function("ST_ZMin", geometry)
\ No newline at end of file
+    return _call_st_function("ST_ZMin", geometry)
diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py
index 341ac8ce..bef6634b 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -69,6 +69,8 @@ test_configurations = [
     (stf.ST_Collect, ("a", "b"), "two_points", "", "MULTIPOINT Z (0 0 0, 3 0 4)"),
     (stf.ST_CollectionExtract, ("geom",), "geom_collection", "", "MULTILINESTRING ((0 0, 1 0))"),
     (stf.ST_CollectionExtract, ("geom", 1), "geom_collection", "", "MULTIPOINT (0 0)"),
+    (stf.ST_ConcaveHull, ("geom", 1.0), "triangle_geom", "", "POLYGON ((0 0, 1 1, 1 0, 0 0))"),
+    (stf.ST_ConcaveHull, ("geom", 1.0, True), "triangle_geom", "", "POLYGON ((1 1, 1 0, 0 0, 1 1))"),
     (stf.ST_ConvexHull, ("geom",), "triangle_geom", "", "POLYGON ((0 0, 1 1, 1 0, 0 0))"),
     (stf.ST_Difference, ("a", "b"), "overlapping_polys", "", "POLYGON ((1 0, 0 0, 0 1, 1 1, 1 0))"),
     (stf.ST_Distance, ("a", "b"), "two_points", "", 3.0),
@@ -196,6 +198,7 @@ wrong_type_configurations = [
     (stf.ST_Centroid, (None,)),
     (stf.ST_Collect, (None,)),
     (stf.ST_CollectionExtract, (None,)),
+    (stf.ST_ConcaveHull, (None, 1.0)),
     (stf.ST_ConvexHull, (None,)),
     (stf.ST_Difference, (None, "b")),
     (stf.ST_Difference, ("", None)),
diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py
index 5fba932d..8db02262 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -50,6 +50,22 @@ class TestPredicateJoin(TestBase):
         ]
     )
 
+    def test_st_concave_hull(self):
+        polygon_wkt_df = self.spark.read.format("csv"). \
+            option("delimiter", "\t"). \
+            option("header", "false"). \
+            load(mixed_wkt_geometry_input_location)
+
+        polygon_wkt_df.createOrReplaceTempView("polygontable")
+        polygon_wkt_df.show()
+
+        polygon_df = self.spark.sql("select ST_GeomFromWKT(polygontable._c0) as countyshape from polygontable")
+        polygon_df.createOrReplaceTempView("polygondf")
+        polygon_df.show()
+
+        function_df = self.spark.sql("select ST_ConcaveHull(polygondf.countyshape, 1.0) from polygondf")
+        function_df.show()
+
     def test_st_convex_hull(self):
         polygon_wkt_df = self.spark.read.format("csv"). \
             option("delimiter", "\t"). \
diff --git a/python/tests/streaming/spark/test_constructor_functions.py b/python/tests/streaming/spark/test_constructor_functions.py
index c6f7830a..6fc55b99 100644
--- a/python/tests/streaming/spark/test_constructor_functions.py
+++ b/python/tests/streaming/spark/test_constructor_functions.py
@@ -49,6 +49,11 @@ SEDONA_LISTED_SQL_FUNCTIONS = [
      .with_function_name("ST_Distance")
      .with_arguments(["ST_GeomFromText('POINT (21 52)')", "ST_GeomFromText('POINT (21 53)')"])
      .with_expected_result(1.0)),
+    (SuiteContainer.empty()
+     .with_function_name("ST_ConcaveHull")
+     .with_arguments(["ST_GeomFromText('POLYGON ((21 52, 21 53, 22 53, 22 52, 21 52))')", "1.0"])
+     .with_expected_result(1.0)
+     .with_transform("ST_AREA")),
     (SuiteContainer.empty()
      .with_function_name("ST_ConvexHull")
      .with_arguments(["ST_GeomFromText('POLYGON ((21 52, 21 53, 22 53, 22 52, 21 52))')"])
diff --git a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 09e1d256..3f467aec 100644
--- a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -56,6 +56,7 @@ object Catalog {
     function[ST_Disjoint](),
     function[ST_Distance](),
     function[ST_3DDistance](),
+    function[ST_ConcaveHull](false),
     function[ST_ConvexHull](),
     function[ST_NPoints](),
     function[ST_NDims](),
diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index c7435f93..5e5ad11b 100644
--- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -98,6 +98,19 @@ case class ST_3DDistance(inputExpressions: Seq[Expression])
   }
 }
 
+/**
+ * Return the concave hull of a Geometry.
+ *
+ * @param inputExpressions
+ */
+case class ST_ConcaveHull(inputExpressions: Seq[Expression])
+  extends InferredTernaryExpression(Functions.concaveHull) with FoldableExpression {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]): Expression = {
+    copy(inputExpressions = newChildren)
+  }
+}
+
 /**
   * Return the convex hull of a Geometry.
   *
diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 90de477c..16073e93 100644
--- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -79,6 +79,11 @@ object st_functions extends DataFrameAPI {
   def ST_CollectionExtract(collection: Column, geomType: Column): Column = wrapExpression[ST_CollectionExtract](collection, geomType)
   def ST_CollectionExtract(collection: String, geomType: Int): Column = wrapExpression[ST_CollectionExtract](collection, geomType)
 
+  def ST_ConcaveHull(geometry: Column, pctConvex:Column): Column = wrapExpression[ST_ConcaveHull](geometry, pctConvex, false)
+  def ST_ConcaveHull(geometry: String, pctConvex:Double): Column = wrapExpression[ST_ConcaveHull](geometry, pctConvex, false)
+  def ST_ConcaveHull(geometry: Column, pctConvex:Column, allowHoles: Column): Column = wrapExpression[ST_ConcaveHull](geometry, pctConvex, allowHoles)
+  def ST_ConcaveHull(geometry: String, pctConvex:Double, allowHoles: Boolean): Column = wrapExpression[ST_ConcaveHull](geometry, pctConvex, allowHoles)
+
   def ST_ConvexHull(geometry: Column): Column = wrapExpression[ST_ConvexHull](geometry)
   def ST_ConvexHull(geometry: String): Column = wrapExpression[ST_ConvexHull](geometry)
   
diff --git a/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 47790e55..7d2deca7 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -19,12 +19,9 @@
 package org.apache.sedona.sql
 
 import scala.collection.mutable.WrappedArray
-
 import org.apache.commons.codec.binary.Hex
 import org.apache.spark.sql.functions.lit
-
 import org.locationtech.jts.geom.Geometry
-
 import org.apache.spark.sql.sedona_sql.expressions.st_constructors._
 import org.apache.spark.sql.sedona_sql.expressions.st_functions._
 import org.apache.spark.sql.sedona_sql.expressions.st_predicates._
@@ -153,6 +150,13 @@ class dataFrameAPITestScala extends TestBaseScala {
     }
 
     // functions
+      it("Passed ST_ConcaveHull"){
+        val baseDF = sparkSession.sql("SELECT ST_GeomFromWKT('Polygon ((0 0, 1 2, 2 2, 3 2, 5 0, 4 0, 3 1, 2 1, 1 0, 0 0))') as mline")
+        val df = baseDF.select(ST_ConcaveHull("mline", 1, true))
+        val actualResult = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
+        assert(actualResult == "POLYGON ((1 2, 2 2, 3 2, 5 0, 4 0, 1 0, 0 0, 1 2))")
+      }
+
     it("Passed ST_ConvexHull") {
       val polygonDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 0))') AS geom")
       val df = polygonDf.select(ST_ConvexHull("geom"))
diff --git a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 9562b1e2..784a2200 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -43,6 +43,15 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
 
   describe("Sedona-SQL Function Test") {
 
+    it("Passed ST_ConcaveHull") {
+      var polygonWktDf = sparkSession.read.format("csv").option("delimiter", "\t").option("header", "false").load(mixedWktGeometryInputLocation)
+      polygonWktDf.createOrReplaceTempView("polygontable")
+      var polygonDf = sparkSession.sql("select ST_GeomFromWKT(polygontable._c0) as countyshape from polygontable")
+      polygonDf.createOrReplaceTempView("polygondf")
+      var functionDf = sparkSession.sql("select ST_ConcaveHull(polygondf.countyshape, 1, true) from polygondf")
+      assert(functionDf.count() > 0);
+    }
+
     it("Passed ST_ConvexHull") {
       var polygonWktDf = sparkSession.read.format("csv").option("delimiter", "\t").option("header", "false").load(mixedWktGeometryInputLocation)
       polygonWktDf.createOrReplaceTempView("polygontable")
@@ -1625,6 +1634,10 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
     assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_3DDistance(null, null)")
     assert(functionDf.first().get(0) == null)
+    functionDf = sparkSession.sql("select ST_ConcaveHull(null, 1, true)")
+    assert(functionDf.first().get(0) == null)
+    functionDf = sparkSession.sql("select ST_ConcaveHull(null, 1)")
+    assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_ConvexHull(null)")
     assert(functionDf.first().get(0) == null)
     functionDf = sparkSession.sql("select ST_NPoints(null)")