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

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

commit 6da51e9925c492b7cff8dd942601e88111c97018
Author: Furqaan Khan <[email protected]>
AuthorDate: Thu May 9 22:11:54 2024 -0400

    [TASK-134] Add ST_UnaryUnion (#189)
    
    * feat: Add ST_UnaryUnion
    
    * fix: sedona tests
    
    * fix: snowflake tests
---
 .../java/org/apache/sedona/common/Functions.java    |  5 +++++
 .../org/apache/sedona/common/FunctionsTest.java     | 21 ++++++++++++++++++---
 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                   | 10 ++++++++++
 python/tests/sql/test_dataframe_api.py              |  4 ++++
 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 +
 .../sql/sedona_sql/expressions/Functions.scala      |  8 ++++++++
 .../sql/sedona_sql/expressions/st_functions.scala   |  3 +++
 .../apache/sedona/sql/dataFrameAPITestScala.scala   |  7 +++++++
 .../org/apache/sedona/sql/functionTestScala.scala   |  7 +++++++
 20 files changed, 180 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 161d862db..e2bf5c6ea 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -39,6 +39,7 @@ import org.locationtech.jts.operation.distance3d.Distance3DOp;
 import org.locationtech.jts.operation.linemerge.LineMerger;
 import org.locationtech.jts.operation.overlay.snap.GeometrySnapper;
 import org.locationtech.jts.operation.polygonize.Polygonizer;
+import org.locationtech.jts.operation.union.UnaryUnionOp;
 import org.locationtech.jts.operation.valid.IsSimpleOp;
 import org.locationtech.jts.operation.valid.IsValidOp;
 import org.locationtech.jts.operation.valid.TopologyValidationError;
@@ -1317,6 +1318,10 @@ public class Functions {
         return GEOMETRY_FACTORY.createGeometryCollection(geoms).union();
     }
 
+    public static Geometry unaryUnion(Geometry geom) {
+        return UnaryUnionOp.union(geom);
+    }
+
     public static Geometry createMultiGeometryFromOneElement(Geometry 
geometry) {
         if (geometry instanceof Circle) {
             return GEOMETRY_FACTORY.createGeometryCollection(new Circle[] 
{(Circle) 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 8915c2cab..b7a79d1af 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -725,7 +725,6 @@ public class FunctionsTest extends TestBase {
         long[] cellIds = new long[]{1152991873351024640L, 
1153132610839379968L, 1153273348327735296L, 1153414085816090624L};
         Geometry[] polygons = Functions.s2ToGeom(cellIds);
         String actual = Functions.asWKT(Functions.union(polygons));
-        System.out.println(Arrays.toString(polygons));
         String expected = "POLYGON ((0.6014716838554667 -0.0000000000000254, 
0.6014716838554158 -0.0000000000000254, -0.0000000000000254 
-0.0000000000000254, -0.0000000000000254 0.6014385452363985, 
-0.0000000000000254 0.6014716838554667, -0.0000000000000254 1.2121321753162642, 
0.6014716838554667 1.2121321753162642, 0.6014716838554667 1.2120654068310366, 
1.2121321753162642 1.2120654068310366, 1.2121321753162642 0.6014385452364494, 
1.2121321753162642 0.6013371003640015, 1.2121321753162642  [...]
         assertEquals(expected, actual);
 
@@ -743,7 +742,24 @@ public class FunctionsTest extends TestBase {
         actual = Functions.asWKT(Functions.union(mPoly));
         expected = "POLYGON ((0.6014716838554667 -0.0000000000000254, 
0.6014716838554158 -0.0000000000000254, -0.0000000000000254 
-0.0000000000000254, -0.0000000000000254 0.6014385452363985, 
-0.0000000000000254 0.6014716838554667, -0.0000000000000254 1.2121321753162642, 
0.6014716838554667 1.2121321753162642, 0.6014716838554667 1.2120654068310366, 
1.2121321753162642 1.2120654068310366, 1.2121321753162642 0.6014385452364494, 
1.2121321753162642 0.6013371003640015, 1.2121321753162642 -0.0000 [...]
         assertEquals(expected, actual);
-        System.out.println(actual);
+    }
+
+    @Test
+    public void testUnaryUnion() throws ParseException {
+        Geometry geometry = Constructors.geomFromEWKT("MULTIPOLYGON(((0 10,0 
30,20 30,20 10,0 10)),((10 0,10 20,30 20,30 0,10 0)))");
+        String actual = Functions.unaryUnion(geometry).toString();
+        String expected = "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 
20, 30 0, 10 0))";
+        assertEquals(expected, actual);
+
+        geometry = Constructors.geomFromEWKT("MULTILINESTRING ((10 10, 20 20, 
30 30),(25 25, 35 35, 45 45),(40 40, 50 50, 60 60),(55 55, 65 65, 75 75))");
+        actual = Functions.unaryUnion(geometry).toString();
+        expected = "MULTILINESTRING ((10 10, 20 20, 25 25), (25 25, 30 30), 
(30 30, 35 35, 40 40), (40 40, 45 45), (45 45, 50 50, 55 55), (55 55, 60 60), 
(60 60, 65 65, 75 75))";
+        assertEquals(expected, actual);
+
+        geometry = Constructors.geomFromEWKT("GEOMETRYCOLLECTION (POINT (10 
10),LINESTRING (20 20, 30 30),POLYGON ((25 25, 35 35, 35 35, 25 25)),MULTIPOINT 
(30 30, 40 40),MULTILINESTRING ((40 40, 50 50), (45 45, 55 55)),MULTIPOLYGON 
(((50 50, 60 60, 60 60, 50 50)), ((55 55, 65 65, 65 65, 55 55))))");
+        actual = Functions.unaryUnion(geometry).toString();
+        expected = "GEOMETRYCOLLECTION (POINT (10 10), LINESTRING (20 20, 30 
30), LINESTRING (40 40, 45 45), LINESTRING (45 45, 50 50), LINESTRING (50 50, 
55 55))";
+        assertEquals(expected, actual);
     }
 
     @Test
@@ -1940,7 +1956,6 @@ public class FunctionsTest extends TestBase {
         for (int[] testCase : testCases_special) {
             Geometry geom = GEOMETRY_FACTORY.createPoint(new 
Coordinate(testCase[0], testCase[1]));
             int actualEPSG = Functions.bestSRID(geom);
-            System.out.println("actualEPSG: "+actualEPSG);
             int expectedEPSG = testCase[2];
             assertEquals("Failed at coordinates (" + testCase[0] + ", " + 
testCase[1] + ")", expectedEPSG, actualEPSG);
         }
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 660658491..9a0047ff6 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -3404,6 +3404,26 @@ Output:
 GEOMETRYCOLLECTION (POLYGON ((0 0, 0 10, 5 5, 0 0)), POLYGON ((5 8, 5 5, 0 10, 
5 8)), POLYGON ((10 0, 0 0, 5 5, 10 0)), POLYGON ((10 10, 5 8, 0 10, 10 10)), 
POLYGON ((10 0, 5 5, 8 5, 10 0)), POLYGON ((5 8, 10 10, 8 8, 5 8)), POLYGON 
((10 10, 10 0, 8 5, 10 10)), POLYGON ((8 5, 8 8, 10 10, 8 5)))
 ```
 
+## ST_UnaryUnion
+
+Introduction: This variant of [ST_Union](#st_union) operates on a single 
geometry input. The input geometry can be a simple Geometry type, a 
MultiGeometry, or a GeometryCollection. The function calculates the geometric 
union across all components and elements within the provided geometry object.
+
+Format: `ST_UnaryUnion(geometry: Geometry)`
+
+Since: `vTBD`
+
+SQL Example
+
+```sql
+SELECT ST_UnaryUnion(ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 30,20 30,20 10,0 
10)),((10 0,10 20,30 20,30 0,10 0)))'))
+```
+
+Output:
+
+```
+POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 0, 10 0))
+```
+
 ## ST_Union
 
 Introduction:
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index 5f862e767..43e208727 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -2557,6 +2557,24 @@ Output:
 GEOMETRYCOLLECTION (POLYGON ((0 0, 0 10, 5 5, 0 0)), POLYGON ((5 8, 5 5, 0 10, 
5 8)), POLYGON ((10 0, 0 0, 5 5, 10 0)), POLYGON ((10 10, 5 8, 0 10, 10 10)), 
POLYGON ((10 0, 5 5, 8 5, 10 0)), POLYGON ((5 8, 10 10, 8 8, 5 8)), POLYGON 
((10 10, 10 0, 8 5, 10 10)), POLYGON ((8 5, 8 8, 10 10, 8 5)))
 ```
 
+## ST_UnaryUnion
+
+Introduction: This variant of [ST_Union](#st_union) operates on a single 
geometry input. The input geometry can be a simple Geometry type, a 
MultiGeometry, or a GeometryCollection. The function calculates the geometric 
union across all components and elements within the provided geometry object.
+
+Format: `ST_UnaryUnion(geometry: Geometry)`
+
+SQL Example
+
+```sql
+SELECT ST_UnaryUnion(ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 30,20 30,20 10,0 
10)),((10 0,10 20,30 20,30 0,10 0)))'))
+```
+
+Output:
+
+```
+POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 0, 10 0))
+```
+
 ## ST_Union
 
 Introduction: Return the union of geometry A and B
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index ff0a58fbe..af394655d 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -3514,6 +3514,26 @@ Output:
 GEOMETRYCOLLECTION (POLYGON ((0 0, 0 10, 5 5, 0 0)), POLYGON ((5 8, 5 5, 0 10, 
5 8)), POLYGON ((10 0, 0 0, 5 5, 10 0)), POLYGON ((10 10, 5 8, 0 10, 10 10)), 
POLYGON ((10 0, 5 5, 8 5, 10 0)), POLYGON ((5 8, 10 10, 8 8, 5 8)), POLYGON 
((10 10, 10 0, 8 5, 10 10)), POLYGON ((8 5, 8 8, 10 10, 8 5)))
 ```
 
+## ST_UnaryUnion
+
+Introduction: This variant of [ST_Union](#st_union) operates on a single 
geometry input. The input geometry can be a simple Geometry type, a 
MultiGeometry, or a GeometryCollection. The function calculates the geometric 
union across all components and elements within the provided geometry object.
+
+Format: `ST_UnaryUnion(geometry: Geometry)`
+
+Since: `vTBD`
+
+SQL Example
+
+```sql
+SELECT ST_UnaryUnion(ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 30,20 30,20 10,0 
10)),((10 0,10 20,30 20,30 0,10 0)))'))
+```
+
+Output:
+
+```
+POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 0, 10 0))
+```
+
 ## ST_Union
 
 Introduction:
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 772aa27cf..8d723fa80 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -175,6 +175,7 @@ public class Catalog {
                 new Functions.ST_ForcePolygonCCW(),
                 new Functions.ST_Translate(),
                 new Functions.ST_TriangulatePolygon(),
+                new Functions.ST_UnaryUnion(),
                 new Functions.ST_Union(),
                 new Functions.ST_VoronoiPolygons(),
                 new Functions.ST_FrechetDistance(),
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 79d1a8b26..72d7d2c95 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
@@ -1267,6 +1267,14 @@ public class Functions {
         }
     }
 
+    public static class ST_UnaryUnion 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.unaryUnion(geometry);
+        }
+    }
+
     public static class ST_Union 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 f128b31d2..9e53a7495 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -316,6 +316,14 @@ public class FunctionTest extends TestBase{
         assertEquals(expected, actual);
     }
 
+    @Test
+    public void testUnaryUnion() {
+        Table table = tableEnv.sqlQuery("SELECT 
ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 30,20 30,20 10,0 10)),((10 0,10 20,30 
20,30 0,10 0)))') AS geom");
+        String actual = 
first(table.select(call(Functions.ST_UnaryUnion.class.getSimpleName(), 
$("geom")))).getField(0).toString();
+        String expected = "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 
20, 30 0, 10 0))";
+        assertEquals(expected, actual);
+    }
+
     @Test
     public void testUnionArrayVariant() {
         Table polyTable = tableEnv.sqlQuery("SELECT 
ARRAY[ST_GeomFromWKT('POLYGON ((-3 -3, 3 -3, 3 3, -3 3, -3 -3))'), 
ST_GeomFromWKT('POLYGON ((-2 1, 2 1, 2 4, -2 4, -2 1))')] as polys");
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index 1a3dd9809..75b95c332 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1470,6 +1470,16 @@ def ST_TriangulatePolygon(geom: ColumnOrName) -> Column:
     """
     return _call_st_function("ST_TriangulatePolygon", geom)
 
+@validate_argument_types
+def ST_UnaryUnion(geom: ColumnOrName) -> Column:
+    """Calculate the unary union of a geometry
+
+        :param geom: Geometry to do union
+        :type geom: ColumnOrName
+        :return: Geometry representing the unary union of geom as a geometry 
column.
+        :rtype: Column
+        """
+    return _call_st_function("ST_UnaryUnion", geom)
 
 @validate_argument_types
 def ST_Union(a: ColumnOrName, b: Optional[ColumnOrName] = None) -> Column:
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index b6fd404d4..9ba7bfb1d 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -206,6 +206,7 @@ test_configurations = [
     (stf.ST_TriangulatePolygon, ("geom",), "square_geom", "", 
"GEOMETRYCOLLECTION (POLYGON ((1 0, 1 1, 2 1, 1 0)), POLYGON ((2 1, 2 0, 1 0, 2 
1)))"),
     (stf.ST_Union, ("a", "b"), "overlapping_polys", "", "POLYGON ((1 0, 0 0, 0 
1, 1 1, 2 1, 3 1, 3 0, 2 0, 1 0))"),
     (stf.ST_Union, ("polys",), "array_polygons", "", "POLYGON ((2 3, 3 3, 3 
-3, -3 -3, -3 3, -2 3, -2 4, 2 4, 2 3))"),
+    (stf.ST_UnaryUnion, ("geom",), "overlapping_mPolys", "", "POLYGON ((10 0, 
10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 0, 10 0))"),
     (stf.ST_VoronoiPolygons, ("geom",), "multipoint", "", "GEOMETRYCOLLECTION 
(POLYGON ((-1 -1, -1 2, 2 -1, -1 -1)), POLYGON ((-1 2, 2 2, 2 -1, -1 2)))"),
     (stf.ST_X, ("b",), "two_points", "", 3.0),
     (stf.ST_XMax, ("line",), "linestring_geom", "", 5.0),
@@ -415,6 +416,7 @@ wrong_type_configurations = [
     (stf.ST_TriangulatePolygon, (None,)),
     (stf.ST_Union, (None, "")),
     (stf.ST_Union, (None,)),
+    (stf.ST_UnaryUnion, (None,)),
     (stf.ST_X, (None,)),
     (stf.ST_XMax, (None,)),
     (stf.ST_XMin, (None,)),
@@ -516,6 +518,8 @@ class TestDataFrameAPI(TestBase):
             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":
             return TestDataFrameAPI.spark.sql("SELECT 
ST_GeomFromWKT('POLYGON((0 0, 2 0, 2 1, 0 1, 0 0))') AS a, 
ST_GeomFromWKT('POLYGON((1 0, 3 0, 3 1, 1 1, 1 0))') AS b")
+        elif request.param == "overlapping_mPolys":
+            return TestDataFrameAPI.spark.sql("SELECT 
ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 30,20 30,20 10,0 10)),((10 0,10 20,30 
20,30 0,10 0)))') AS geom")
         elif request.param == "multipoint":
             return TestDataFrameAPI.spark.sql("SELECT 
ST_GeomFromWKT('MULTIPOINT ((0 0), (1 1))') AS geom")
         elif request.param == "geom_with_hole":
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index fcedfb794..37da2fbfa 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -464,6 +464,12 @@ class TestPredicateJoin(TestBase):
         expected = "MULTIPOLYGON (((5 -3, 5 -1, 7 -1, 7 -3, 5 -3)), ((-3 -3, 
-3 3, 3 3, 3 -3, -3 -3)), ((4 4, 4 6, 6 6, 6 4, 4 4)))"
         assert expected == actual
 
+    def test_st_unary_union(self):
+        baseDf = self.spark.sql("SELECT ST_GeomFromWKT('MULTIPOLYGON(((0 10,0 
30,20 30,20 10,0 10)),((10 0,10 20,30 20,30 0,10 0)))') AS geom")
+        actual = baseDf.selectExpr("ST_UnaryUnion(geom)").take(1)[0][0].wkt
+        expected = "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 
0, 10 0))"
+        assert expected == actual
+
     def test_st_azimuth(self):
         sample_points = create_sample_points(20)
         sample_pair_points = [[el, sample_points[1]] for el in sample_points]
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 8a39b3f01..edfe2b9b4 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
@@ -954,6 +954,15 @@ public class TestFunctions extends TestBase {
         );
     }
 
+    @Test
+    public void test_ST_UnaryUnion() {
+        registerUDF("ST_UnaryUnion", byte[].class);
+        verifySqlSingleRes(
+                "select 
sedona.ST_AsText(sedona.ST_UnaryUnion(sedona.ST_GeomFromText('MULTIPOLYGON(((0 
10,0 30,20 30,20 10,0 10)),((10 0,10 20,30 20,30 0,10 0)))')))",
+                "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 30 0, 
10 0))"
+        );
+    }
+
     @Test
     public void test_ST_VoronoiPolygons() {
         registerUDF("ST_VoronoiPolygons", 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 6f9448783..1927789a6 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
@@ -916,6 +916,15 @@ public class TestFunctionsV2
         );
     }
 
+    @Test
+    public void test_ST_UnaryUnion() {
+        registerUDFV2("ST_UnaryUnion", String.class);
+        verifySqlSingleRes(
+                "select 
ST_AsText(sedona.ST_UnaryUnion(ST_GeometryFromWKT('MULTILINESTRING ((10 10, 20 
20, 30 30),(25 25, 35 35, 45 45),(40 40, 50 50, 60 60),(55 55, 65 65, 75 
75))')))",
+                "MULTILINESTRING((10 10,20 20,25 25),(25 25,30 30),(30 30,35 
35,40 40),(40 40,45 45),(45 45,50 50,55 55),(55 55,60 60),(60 60,65 65,75 75))"
+        );
+    }
+
     @Test
     public void test_ST_VoronoiPolygons() {
         registerUDFV2("ST_VoronoiPolygons", 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 66b07e703..32402968c 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
@@ -1370,6 +1370,15 @@ public class UDFs {
         );
     }
 
+    @UDFAnnotations.ParamMeta(argNames = {"geometry"})
+    public static byte[] ST_UnaryUnion(byte[] geometry) {
+        return GeometrySerde.serialize(
+                Functions.unaryUnion(
+                        GeometrySerde.deserialize(geometry)
+                )
+        );
+    }
+
     @UDFAnnotations.ParamMeta(argNames = {"geometry"})
     public static byte[] ST_VoronoiPolygons(byte[] geometry) {
         return GeometrySerde.serialize(
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 e2e0acdc5..a30f775f5 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
@@ -1128,6 +1128,15 @@ public class UDFsV2
         );
     }
 
+    @UDFAnnotations.ParamMeta(argNames = {"geometry"}, argTypes = 
{"Geometry"}, returnTypes = "Geometry")
+    public static String ST_UnaryUnion(String geometry) {
+        return GeometrySerde.serGeoJson(
+                Functions.unaryUnion(
+                        GeometrySerde.deserGeoJson(geometry)
+                )
+        );
+    }
+
     @UDFAnnotations.ParamMeta(argNames = {"geometry"}, argTypes = 
{"Geometry"}, returnTypes = "Geometry")
     public static String ST_VoronoiPolygons(String geometry) {
         return GeometrySerde.serGeoJson(
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 c6a0d5d41..ff522f9f6 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
@@ -85,6 +85,7 @@ object Catalog {
     function[ST_Intersection](),
     function[ST_Difference](),
     function[ST_SymDifference](),
+    function[ST_UnaryUnion](),
     function[ST_Union](),
     function[ST_IsValid](),
     function[ST_IsEmpty](),
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 ded1e31f7..2e1711e70 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
@@ -913,6 +913,14 @@ case class ST_SymDifference(inputExpressions: 
Seq[Expression])
   }
 }
 
+case class ST_UnaryUnion(inputExpressions: Seq[Expression])
+  extends InferredExpression(inferrableFunction1(Functions.unaryUnion)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 /**
   * Return the union of geometry A and B
   *
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 9bb23391d..e7b441486 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
@@ -368,6 +368,9 @@ object st_functions extends DataFrameAPI {
   def ST_SimplifyPolygonHull(geometry: Column, vertexFactor: Column, isOuter: 
Column): Column = wrapExpression[ST_SimplifyPolygonHull](geometry, 
vertexFactor, isOuter)
   def ST_SimplifyPolygonHull(geometry: String, vertexFactor: Double, isOuter: 
Boolean): Column = wrapExpression[ST_SimplifyPolygonHull](geometry, 
vertexFactor, isOuter)
 
+  def ST_UnaryUnion(geometry: Column): Column = 
wrapExpression[ST_UnaryUnion](geometry)
+  def ST_UnaryUnion(geometry: String): Column = 
wrapExpression[ST_UnaryUnion](geometry)
+
   def ST_Union(a: Column, b: Column): Column = wrapExpression[ST_Union](a, b)
   def ST_Union(a: String, b: String): Column = wrapExpression[ST_Union](a, b)
   def ST_Union(geoms: Column): Column = wrapExpression[ST_Union](geoms)
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 b1ac7ee26..434a938ab 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
@@ -754,6 +754,13 @@ class dataFrameAPITestScala extends TestBaseScala {
       assert(expected.equals(actual))
     }
 
+    it("Passed ST_UnaryUnion") {
+      val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTIPOLYGON(((0 
10,0 30,20 30,20 10,0 10)),((10 0,10 20,30 20,30 0,10 0)))') AS geom")
+      val actual = 
baseDf.select(ST_UnaryUnion("geom")).first().get(0).asInstanceOf[Geometry].toText
+      val expected = "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 
30 0, 10 0))"
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_Azimuth") {
       val baseDf = sparkSession.sql("SELECT ST_Point(0.0, 0.0) AS a, 
ST_Point(1.0, 1.0) AS b")
       val df = baseDf.select(ST_Azimuth("a", "b"))
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 1b81fb909..38af32109 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
@@ -781,6 +781,13 @@ class functionTestScala extends TestBaseScala with 
Matchers with GeometrySample
       assert(symDiff.first().get(0) == null)
     }
 
+    it("Passed ST_UnaryUnion") {
+      val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTIPOLYGON(((0 
10,0 30,20 30,20 10,0 10)),((10 0,10 20,30 20,30 0,10 0)))') AS geom")
+      val actual = 
baseDf.selectExpr("ST_UnaryUnion(geom)").first().get(0).asInstanceOf[Geometry].toText
+      val expected = "POLYGON ((10 0, 10 10, 0 10, 0 30, 20 30, 20 20, 30 20, 
30 0, 10 0))"
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_Union - part of right overlaps left") {
 
       val testtable = sparkSession.sql("select ST_GeomFromWKT('POLYGON ((-3 
-3, 3 -3, 3 3, -3 3, -3 -3))') as a, ST_GeomFromWKT('POLYGON ((-2 1, 2 1, 2 4, 
-2 4, -2 1))') as b")

Reply via email to