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/sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new 1f24ca690 [SEDONA-625] Add ST_GeneratePoints (#1520)
1f24ca690 is described below

commit 1f24ca69007089710207386aa2a3704e6af559f9
Author: Furqaan Khan <[email protected]>
AuthorDate: Wed Jul 17 17:59:48 2024 -0400

    [SEDONA-625] Add ST_GeneratePoints (#1520)
    
    * feat: add ST_GeneratePoints
    
    * fix: lint errors
    
    * fix: snowflake v2 registeration
    
    * feat: add Nondeterministic trait to spark
    
    * fix: optimize imports
    
    * fix: optimize imports and spark registration
    
    * fix: spark registration 1
    
    * fix: spark registration 2 and remove unnecessary code
    
    * fix: spark registration 3
    
    * fix: remove ExpressionWithRandomSeed
    
    * docs: add new seed parameter to docs
    
    * chore: add the new parameter to all engines and add tests
    
    * fix: snowflake test
    
    * Use per-partition random number generator when seed is not specified.
    
    ---------
    
    Co-authored-by: Kristin Cowalcijk <[email protected]>
---
 .../java/org/apache/sedona/common/Functions.java   | 18 +++++++
 .../common/utils/RandomPointsBuilderSeed.java      | 48 +++++++++++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    | 42 +++++++++++++++++
 docs/api/flink/Function.md                         | 29 ++++++++++++
 docs/api/snowflake/vector-data/Function.md         | 27 +++++++++++
 docs/api/sql/Function.md                           | 29 ++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../apache/sedona/flink/expressions/Functions.java | 19 ++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 43 +++++++++++++++++
 python/sedona/sql/st_functions.py                  | 18 +++++++
 python/tests/sql/test_dataframe_api.py             |  4 ++
 python/tests/sql/test_function.py                  | 20 ++++++++
 .../sedona/snowflake/snowsql/TestFunctions.java    | 14 ++++++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  | 14 ++++++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  | 12 +++++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    | 18 +++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  1 +
 .../sql/sedona_sql/expressions/Functions.scala     | 55 +++++++++++++++++++++-
 .../sql/sedona_sql/expressions/st_functions.scala  | 13 +++++
 .../org/apache/sedona/sql/PreserveSRIDSuite.scala  |  3 +-
 .../apache/sedona/sql/dataFrameAPITestScala.scala  | 22 +++++++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 22 +++++++++
 22 files changed, 470 insertions(+), 2 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 a43519db1..f0ffd0bc5 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1812,6 +1812,24 @@ public class Functions {
     return array;
   }
 
+  public static Geometry generatePoints(Geometry geom, int numPoints, long 
seed) {
+    RandomPointsBuilderSeed pointsBuilder = new 
RandomPointsBuilderSeed(geom.getFactory(), seed);
+    pointsBuilder.setExtent(geom);
+    pointsBuilder.setNumPoints(numPoints);
+    return pointsBuilder.getGeometry();
+  }
+
+  public static Geometry generatePoints(Geometry geom, int numPoints) {
+    return generatePoints(geom, numPoints, 0);
+  }
+
+  public static Geometry generatePoints(Geometry geom, int numPoints, Random 
random) {
+    RandomPointsBuilderSeed pointsBuilder = new 
RandomPointsBuilderSeed(geom.getFactory(), random);
+    pointsBuilder.setExtent(geom);
+    pointsBuilder.setNumPoints(numPoints);
+    return pointsBuilder.getGeometry();
+  }
+
   public static Integer nRings(Geometry geometry) throws Exception {
     String geometryType = geometry.getGeometryType();
     if (!(geometry instanceof Polygon || geometry instanceof MultiPolygon)) {
diff --git 
a/common/src/main/java/org/apache/sedona/common/utils/RandomPointsBuilderSeed.java
 
b/common/src/main/java/org/apache/sedona/common/utils/RandomPointsBuilderSeed.java
new file mode 100644
index 000000000..0d396f392
--- /dev/null
+++ 
b/common/src/main/java/org/apache/sedona/common/utils/RandomPointsBuilderSeed.java
@@ -0,0 +1,48 @@
+/*
+ * 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 java.util.Random;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.shape.random.RandomPointsBuilder;
+
+public class RandomPointsBuilderSeed extends RandomPointsBuilder {
+  Random rand;
+
+  public RandomPointsBuilderSeed(GeometryFactory geometryFactory, long seed) {
+    super(geometryFactory);
+    if (seed > 0) {
+      this.rand = new Random(seed);
+    } else {
+      this.rand = new Random();
+    }
+  }
+
+  public RandomPointsBuilderSeed(GeometryFactory geometryFactory, Random 
random) {
+    super(geometryFactory);
+    this.rand = random;
+  }
+
+  @Override
+  protected Coordinate createRandomCoord(Envelope env) {
+    double x = env.getMinX() + env.getWidth() * rand.nextDouble();
+    double y = env.getMinY() + env.getHeight() * rand.nextDouble();
+    return createCoord(x, y);
+  }
+}
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 3868b4539..a4a3a8c9a 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -2112,6 +2112,48 @@ public class FunctionsTest extends TestBase {
     assertEquals(expected, actual);
   }
 
+  @Test
+  public void generatePoints() throws ParseException {
+    Geometry geom = Constructors.geomFromWKT("LINESTRING(50 50,10 10,10 50)", 
4326);
+
+    Geometry actual =
+        Functions.generatePoints(Functions.buffer(geom, 10, false, 
"endcap=round join=round"), 12);
+    assertEquals(actual.getNumGeometries(), 12);
+
+    actual =
+        Functions.reducePrecision(
+            Functions.generatePoints(
+                Functions.buffer(geom, 10, false, "endcap=round join=round"), 
5, 100),
+            5);
+    String expected =
+        "MULTIPOINT ((40.02957 46.70645), (37.11646 37.38582), (14.2051 
29.23363), (40.82533 31.47273), (28.16839 34.16338))";
+    assertEquals(expected, actual.toString());
+    assertEquals(4326, actual.getSRID());
+
+    geom =
+        Constructors.geomFromEWKT(
+            "MULTIPOLYGON (((10 0, 10 10, 20 10, 20 0, 10 0)), ((50 0, 50 10, 
70 10, 70 0, 50 0)))");
+    actual = Functions.generatePoints(geom, 30);
+    assertEquals(actual.getNumGeometries(), 30);
+
+    // Deterministic when using the same seed
+    Geometry first = Functions.generatePoints(geom, 10, 100);
+    Geometry second = Functions.generatePoints(geom, 10, 100);
+    assertEquals(first, second);
+
+    // Deterministic when using the same random number generator
+    geom = geom.buffer(10, 48);
+    Random rand = new Random(100);
+    Random rand2 = new Random(100);
+    first = Functions.generatePoints(geom, 100, rand);
+    second = Functions.generatePoints(geom, 100, rand);
+    Geometry first2 = Functions.generatePoints(geom, 100, rand2);
+    Geometry second2 = Functions.generatePoints(geom, 100, rand2);
+    assertNotEquals(first, second);
+    assertEquals(first, first2);
+    assertEquals(second, second2);
+  }
+
   @Test
   public void nRingsPolygonOnlyExternal() throws Exception {
     Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 1, 2, 
1, 2, 0, 1, 0));
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 2a77036db..1b2163d19 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -1552,6 +1552,35 @@ Output:
 5.0990195135927845
 ```
 
+## ST_GeneratePoints
+
+Introduction: Generates a specified quantity of pseudo-random points within 
the boundaries of the provided polygonal geometry. When `seed` is either zero 
or not defined then output will be random.
+
+Format:
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer, seed: Long = 0)`
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_GeneratePoints(
+        ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))'), 4
+)
+```
+
+Output:
+
+!!!Note
+    Due to the pseudo-random nature of point generation, the output of this 
function will vary between executions and may not match any provided examples.
+
+```
+MULTIPOINT ((0.2393028905520183 0.9721563442837837), (0.3805848547053376 
0.7546556656982678), (0.0950295778200995 0.2494334895495989), 
(0.4133520939987385 0.3447046312451945))
+```
+
 ## ST_GeoHash
 
 Introduction: Returns GeoHash of the geometry with given precision
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index c067f6a1f..196afa55f 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -1163,6 +1163,33 @@ Output:
 5.0990195135927845
 ```
 
+## ST_GeneratePoints
+
+Introduction: Generates a specified quantity of pseudo-random points within 
the boundaries of the provided polygonal geometry. When `seed` is either zero 
or not defined then output will be random.
+
+Format:
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer, seed: Long = 0)`
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer)`
+
+SQL Example:
+
+```sql
+SELECT ST_GeneratePoints(
+        ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))'), 4
+)
+```
+
+Output:
+
+!!!Note
+    Due to the pseudo-random nature of point generation, the output of this 
function will vary between executions and may not match any provided examples.
+
+```
+MULTIPOINT ((0.2393028905520183 0.9721563442837837), (0.3805848547053376 
0.7546556656982678), (0.0950295778200995 0.2494334895495989), 
(0.4133520939987385 0.3447046312451945))
+```
+
 ## ST_GeoHash
 
 Introduction: Returns GeoHash of the geometry with given precision
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index f2e1e9afb..9d70cee0c 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1557,6 +1557,35 @@ Output:
 5.0990195135927845
 ```
 
+## ST_GeneratePoints
+
+Introduction: Generates a specified quantity of pseudo-random points within 
the boundaries of the provided polygonal geometry. When `seed` is either zero 
or not defined then output will be random.
+
+Format:
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer, seed: Long = 0)`
+
+`ST_GeneratePoints(geom: Geometry, numPoints: Integer)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_GeneratePoints(
+        ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))'), 4
+)
+```
+
+Output:
+
+!!!Note
+    Due to the pseudo-random nature of point generation, the output of this 
function will vary between executions and may not match any provided examples.
+
+```
+MULTIPOINT ((0.2393028905520183 0.9721563442837837), (0.3805848547053376 
0.7546556656982678), (0.0950295778200995 0.2494334895495989), 
(0.4133520939987385 0.3447046312451945))
+```
+
 ## ST_GeoHash
 
 Introduction: Returns GeoHash of the geometry with given precision
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 c043a85d0..b19e99166 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -184,6 +184,7 @@ public class Catalog {
       new Functions.ST_ForceCollection(),
       new Functions.ST_ForcePolygonCW(),
       new Functions.ST_ForceRHR(),
+      new Functions.ST_GeneratePoints(),
       new Functions.ST_NRings(),
       new Functions.ST_IsPolygonCCW(),
       new Functions.ST_ForcePolygonCCW(),
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 87df74658..9b378e0e7 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
@@ -1549,6 +1549,25 @@ public class Functions {
     }
   }
 
+  public static class ST_GeneratePoints 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(value = "Integer") Integer numPoints) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.generatePoints(geom, 
numPoints);
+    }
+
+    @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(value = "Integer") Integer numPoints,
+        @DataTypeHint(value = "BIGINT") Long seed) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.generatePoints(geom, 
numPoints, seed);
+    }
+  }
+
   public static class ST_NRings extends ScalarFunction {
     @DataTypeHint(value = "Integer")
     public int 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 08c8632b1..ea2cb0754 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -2142,6 +2142,49 @@ public class FunctionTest extends TestBase {
     assertTrue(actual);
   }
 
+  @Test
+  public void testGeneratePoints() {
+    Table polyTable =
+        tableEnv.sqlQuery(
+            "SELECT ST_Buffer(ST_GeomFromWKT('LINESTRING(50 50,10 10,10 50)'), 
10, false, 'endcap=round join=round') AS geom");
+    Geometry actual =
+        (Geometry)
+            first(
+                    polyTable.select(
+                        
call(Functions.ST_GeneratePoints.class.getSimpleName(), $("geom"), 15)))
+                .getField(0);
+    assertEquals(actual.getNumGeometries(), 15);
+
+    actual =
+        (Geometry)
+            first(
+                    polyTable
+                        .select(
+                            call(
+                                
Functions.ST_GeneratePoints.class.getSimpleName(),
+                                $("geom"),
+                                5,
+                                100L))
+                        .as("geom")
+                        .select(
+                            
call(Functions.ST_ReducePrecision.class.getSimpleName(), $("geom"), 5)))
+                .getField(0);
+    String expected =
+        "MULTIPOINT ((40.02957 46.70645), (37.11646 37.38582), (14.2051 
29.23363), (40.82533 31.47273), (28.16839 34.16338))";
+    assertEquals(expected, actual.toString());
+
+    polyTable =
+        tableEnv.sqlQuery(
+            "SELECT ST_GeomFromWKT('MULTIPOLYGON (((10 0, 10 10, 20 10, 20 0, 
10 0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))') AS geom");
+    actual =
+        (Geometry)
+            first(
+                    polyTable.select(
+                        
call(Functions.ST_GeneratePoints.class.getSimpleName(), $("geom"), 30)))
+                .getField(0);
+    assertEquals(actual.getNumGeometries(), 30);
+  }
+
   @Test
   public void testNRings() {
     Integer expected = 1;
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index 8b5d1ef75..8100da893 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -548,6 +548,24 @@ def ST_Force_2D(geometry: ColumnOrName) -> Column:
     return _call_st_function("ST_Force_2D", geometry)
 
 
+@validate_argument_types
+def ST_GeneratePoints(geometry: ColumnOrName, numPoints: Union[ColumnOrName, 
int], seed:Optional[Union[ColumnOrName, int]] = None) -> Column:
+    """Generate random points in given geometry.
+
+    :param geometry: Geometry column to hash.
+    :type geometry: ColumnOrName
+    :param numPoints: Precision level to hash geometry at, given as an integer 
or an integer column.
+    :type numPoints: Union[ColumnOrName, int]
+    :return: Generate random points in given geometry
+    :rtype: Column
+    """
+    if seed is None:
+        args = (geometry, numPoints)
+    else:
+        args = (geometry, numPoints, seed)
+
+    return _call_st_function("ST_GeneratePoints", args)
+
 @validate_argument_types
 def ST_GeoHash(geometry: ColumnOrName, precision: Union[ColumnOrName, int]) -> 
Column:
     """Return the geohash of a geometry column at a given precision level.
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 1a97bb6ac..49a2e68f8 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -137,6 +137,8 @@ test_configurations = [
     (stf.ST_ForceRHR, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 
0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
     (stf.ST_FrechetDistance, ("point", "line",), "point_and_line", "", 
5.0990195135927845),
     (stf.ST_GeometricMedian, ("multipoint",), "multipoint_geom", "", "POINT 
(22.500002656424286 21.250001168173426)"),
+    (stf.ST_GeneratePoints, ("geom", 15), "square_geom", 
"ST_NumGeometries(geom)", 15),
+    (stf.ST_GeneratePoints, ("geom", 15, 100), "square_geom", 
"ST_NumGeometries(geom)", 15),
     (stf.ST_GeometryN, ("geom", 0), "multipoint", "", "POINT (0 0)"),
     (stf.ST_GeometryType, ("point",), "point_geom", "", "ST_Point"),
     (stf.ST_HausdorffDistance, ("point", "line",), "point_and_line", "", 
5.0990195135927845),
@@ -351,6 +353,8 @@ wrong_type_configurations = [
     (stf.ST_GeometryN, ("", None)),
     (stf.ST_GeometryN, ("", 0.0)),
     (stf.ST_GeometryType, (None,)),
+    (stf.ST_GeneratePoints, (None, 0.0)),
+    (stf.ST_GeneratePoints, ("", None)),
     (stf.ST_InteriorRingN, (None, 0)),
     (stf.ST_InteriorRingN, ("", None)),
     (stf.ST_InteriorRingN, ("", 0.0)),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index 5fd8dd2b5..5f923b5cb 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1606,6 +1606,26 @@ class TestPredicateJoin(TestBase):
         expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 
20, 20 25, 20 15, 30 20))"
         assert expected == actual
 
+    def test_generate_points(self):
+        actual = self.spark.sql("SELECT 
ST_NumGeometries(ST_GeneratePoints(ST_Buffer(ST_GeomFromWKT('LINESTRING(50 
50,150 150,150 50)'), 10, false, 'endcap=round join=round'), 15))")\
+        .first()[0]
+        assert actual == 15
+
+        actual = self.spark.sql(
+            "SELECT 
ST_AsText(ST_ReducePrecision(ST_GeneratePoints(ST_GeomFromWKT('MULTIPOLYGON 
(((10 0, 10 10, 20 10, 20 0, 10 0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))'), 5, 
10), 5))") \
+            .first()[0]
+        expected = "MULTIPOINT ((53.82582 2.57803), (13.55212 2.44117), 
(59.12854 3.70611), (61.37698 7.14985), (10.49657 4.40622))"
+        assert expected == actual
+
+        actual = self.spark.sql(
+            "SELECT 
ST_NumGeometries(ST_GeneratePoints(ST_Buffer(ST_GeomFromWKT('LINESTRING(50 
50,150 150,150 50)'), 10, false, 'endcap=round join=round'), 15, 100))") \
+            .first()[0]
+        assert actual == 15
+
+        actual = self.spark.sql("SELECT 
ST_NumGeometries(ST_GeneratePoints(ST_GeomFromWKT('MULTIPOLYGON (((10 0, 10 10, 
20 10, 20 0, 10 0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))'), 30))")\
+        .first()[0]
+        assert actual == 30
+
     def test_nRings(self):
         expected = 1
         actualDf = self.spark.sql("SELECT ST_GeomFromText('POLYGON ((1 0, 1 1, 
2 1, 2 0, 1 0))') AS geom")
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 c95ab35a3..f59f63905 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
@@ -394,6 +394,20 @@ public class TestFunctions extends TestBase {
         "select sedona.ST_AsText(sedona.ST_Force2D(sedona.ST_POINTZ(1, 2, 
3)))", "POINT (1 2)");
   }
 
+  @Test
+  public void test_ST_GeneratePoints() {
+    registerUDF("ST_GeneratePoints", byte[].class, int.class);
+    registerUDF("ST_NumGeometries", byte[].class);
+    verifySqlSingleRes(
+        "select 
sedona.ST_NumGeometries(sedona.ST_GeneratePoints(sedona.ST_GeomFromWKT('POLYGON 
((1 0, 1 1, 2 1, 2 0, 1 0))'), 15))",
+        15);
+
+    registerUDF("ST_GeneratePoints", byte[].class, int.class, long.class);
+    verifySqlSingleRes(
+        "select 
sedona.ST_NumGeometries(sedona.ST_GeneratePoints(sedona.ST_GeomFromWKT('POLYGON 
((1 0, 1 1, 2 1, 2 0, 1 0))'), 15, 100))",
+        15);
+  }
+
   @Test
   public void test_ST_GeoHash() {
     registerUDF("ST_GeoHash", byte[].class, int.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 4060e9447..896cd9cb7 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
@@ -376,6 +376,20 @@ public class TestFunctionsV2 extends TestBase {
     verifySqlSingleRes("select ST_AsText(sedona.ST_Force2D(ST_GEOMPOINT(1, 
2)))", "POINT(1 2)");
   }
 
+  @Test
+  public void test_ST_GeneratePoints() {
+    registerUDFV2("ST_GeneratePoints", String.class, int.class);
+    registerUDFV2("ST_NumGeometries", String.class);
+    verifySqlSingleRes(
+        "select 
sedona.ST_NumGeometries(sedona.ST_GeneratePoints(ST_GeomFromWKT('POLYGON ((1 0, 
1 1, 2 1, 2 0, 1 0))'), 15))",
+        15);
+
+    registerUDFV2("ST_GeneratePoints", String.class, int.class, long.class);
+    verifySqlSingleRes(
+        "select 
sedona.ST_NumGeometries(sedona.ST_GeneratePoints(ST_GeomFromWKT('POLYGON ((1 0, 
1 1, 2 1, 2 0, 1 0))'), 15, 100))",
+        15);
+  }
+
   @Test
   public void test_ST_GeoHash() {
     registerUDFV2("ST_GeoHash", String.class, int.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 7985017cb..2a6429dd6 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
@@ -366,6 +366,18 @@ public class UDFs {
     return 
GeometrySerde.serialize(Functions.force2D(GeometrySerde.deserialize(geometry)));
   }
 
+  @UDFAnnotations.ParamMeta(argNames = {"geometry", "numPoints"})
+  public static byte[] ST_GeneratePoints(byte[] geometry, int numPoints) {
+    return GeometrySerde.serialize(
+        Functions.generatePoints(GeometrySerde.deserialize(geometry), 
numPoints));
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geometry", "numPoints", "seed"})
+  public static byte[] ST_GeneratePoints(byte[] geometry, int numPoints, long 
seed) {
+    return GeometrySerde.serialize(
+        Functions.generatePoints(GeometrySerde.deserialize(geometry), 
numPoints, seed));
+  }
+
   @UDFAnnotations.ParamMeta(argNames = {"geometry", "precision"})
   public static String ST_GeoHash(byte[] geometry, int precision) {
     return Functions.geohash(GeometrySerde.deserialize(geometry), precision);
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 fa75a51b0..1b7880785 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
@@ -509,6 +509,24 @@ public class UDFsV2 {
     return 
GeometrySerde.serGeoJson(Functions.force2D(GeometrySerde.deserGeoJson(geometry)));
   }
 
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry", "numPoints"},
+      argTypes = {"Geometry", "int"},
+      returnTypes = "Geometry")
+  public static String ST_GeneratePoints(String geometry, int numPoints) {
+    return GeometrySerde.serGeoJson(
+        Functions.generatePoints(GeometrySerde.deserGeoJson(geometry), 
numPoints));
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry", "numPoints", "seed"},
+      argTypes = {"Geometry", "int", "long"},
+      returnTypes = "Geometry")
+  public static String ST_GeneratePoints(String geometry, int numPoints, long 
seed) {
+    return GeometrySerde.serGeoJson(
+        Functions.generatePoints(GeometrySerde.deserGeoJson(geometry), 
numPoints, seed));
+  }
+
   @UDFAnnotations.ParamMeta(
       argNames = {"geometry", "precision"},
       argTypes = {"Geometry", "int"})
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 2b9aefd84..4fb0aa044 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
@@ -213,6 +213,7 @@ object Catalog {
     function[ST_Force3DZ](0.0),
     function[ST_Force4D](),
     function[ST_ForceCollection](),
+    function[ST_GeneratePoints](),
     function[ST_NRings](),
     function[ST_Translate](0.0),
     function[ST_TriangulatePolygon](),
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 47711716a..70e3582f2 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
@@ -24,7 +24,7 @@ import org.apache.sedona.common.utils.{InscribedCircle, 
ValidDetail}
 import org.apache.sedona.sql.utils.GeometrySerializer
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
-import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes, 
Expression, Generator}
+import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes, 
Expression, Generator, Nondeterministic}
 import org.apache.spark.sql.catalyst.util.ArrayData
 import org.apache.spark.sql.sedona_sql.UDT.GeometryUDT
 import org.apache.spark.sql.sedona_sql.expressions.implicits._
@@ -33,6 +33,7 @@ import org.locationtech.jts.algorithm.MinimumBoundingCircle
 import org.locationtech.jts.geom._
 import 
org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._
 import org.apache.spark.unsafe.types.UTF8String
+import org.apache.spark.util.Utils
 
 /**
  * Return the distance between two geometries.
@@ -1435,6 +1436,58 @@ case class ST_ForceRHR(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_GeneratePoints(inputExpressions: Seq[Expression], randomSeed: 
Long)
+    extends Expression
+    with CodegenFallback
+    with ExpectsInputTypes
+    with Nondeterministic {
+
+  def this(inputExpressions: Seq[Expression]) = this(inputExpressions, 
Utils.random.nextLong())
+
+  @transient private[this] var random: java.util.Random = _
+
+  private val nArgs = children.length
+
+  override protected def initializeInternal(partitionIndex: Int): Unit = 
random =
+    new java.util.Random(randomSeed + partitionIndex)
+
+  override protected def evalInternal(input: InternalRow): Any = {
+    val geom = children.head.toGeometry(input)
+    val numPoints = children(1).eval(input).asInstanceOf[Int]
+    val generatedPoints = if (nArgs == 3) {
+      val seed = children(2).eval(input).asInstanceOf[Int]
+      if (seed > 0) {
+        Functions.generatePoints(geom, numPoints, seed)
+      } else {
+        Functions.generatePoints(geom, numPoints, random)
+      }
+    } else {
+      Functions.generatePoints(geom, numPoints, random)
+    }
+    GeometrySerializer.serialize(generatedPoints)
+  }
+
+  override def nullable: Boolean = true
+
+  override def dataType: DataType = GeometryUDT
+
+  override def inputTypes: Seq[AbstractDataType] = {
+    if (nArgs == 3) {
+      Seq(GeometryUDT, IntegerType, IntegerType)
+    } else if (nArgs == 2) {
+      Seq(GeometryUDT, IntegerType)
+    } else {
+      throw new IllegalArgumentException(s"Invalid number of arguments: 
$nArgs")
+    }
+  }
+
+  override def children: Seq[Expression] = inputExpressions
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class ST_NRings(inputExpressions: Seq[Expression])
     extends InferredExpression(Functions.nRings _) {
   protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
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 cc7a756ee..e28cd120a 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
@@ -676,6 +676,19 @@ object st_functions extends DataFrameAPI {
   def ST_ForceRHR(geometry: Column): Column = 
wrapExpression[ST_ForceRHR](geometry)
   def ST_ForceRHR(geometry: String): Column = 
wrapExpression[ST_ForceRHR](geometry)
 
+  def ST_GeneratePoints(geometry: Column, numPoints: Column): Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints)
+  def ST_GeneratePoints(geometry: String, numPoints: String): Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints)
+  def ST_GeneratePoints(geometry: String, numPoints: Integer): Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints)
+  def ST_GeneratePoints(geometry: String, numPoints: Integer, seed: Integer): 
Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints, seed)
+  def ST_GeneratePoints(geometry: String, numPoints: String, seed: String): 
Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints, seed)
+  def ST_GeneratePoints(geometry: Column, numPoints: Column, seed: Column): 
Column =
+    wrapExpression[ST_GeneratePoints](geometry, numPoints, seed)
+
   def ST_NRings(geometry: Column): Column = wrapExpression[ST_NRings](geometry)
 
   def ST_NRings(geometry: String): Column = wrapExpression[ST_NRings](geometry)
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
index 1d4cc9e8b..414a9aeed 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
@@ -108,7 +108,8 @@ class PreserveSRIDSuite extends TestBaseScala with 
TableDrivenPropertyChecks {
       ("ST_BoundingDiagonal(geom1)", 1000),
       ("ST_DelaunayTriangles(geom4)", 1000),
       ("ST_Rotate(geom1, 10)", 1000),
-      ("ST_Collect(geom1, geom2, geom3)", 1000))
+      ("ST_Collect(geom1, geom2, geom3)", 1000),
+      ("ST_GeneratePoints(geom1, 3)", 1000))
 
     forAll(testCases) { case (expression: String, srid: Int) =>
       it(s"$expression") {
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 81b849592..d01cb6799 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
@@ -1833,6 +1833,28 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertTrue(actual)
     }
 
+    it("Should pass ST_GeneratePoints") {
+      var poly = sparkSession
+        .sql(
+          "SELECT ST_Buffer(ST_GeomFromWKT('LINESTRING(50 50,150 150,150 
50)'), 10, false, 'endcap=round join=round') AS geom")
+      var actual = poly
+        .select(ST_NumGeometries(ST_GeneratePoints("geom", 15)))
+        .first()
+        .get(0)
+        .asInstanceOf[Int]
+      assert(actual == 15)
+
+      poly = sparkSession
+        .sql(
+          "SELECT ST_GeomFromWKT('MULTIPOLYGON (((10 0, 10 10, 20 10, 20 0, 10 
0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))') AS geom")
+      actual = poly
+        .select(ST_NumGeometries(ST_GeneratePoints("geom", 30)))
+        .first()
+        .get(0)
+        .asInstanceOf[Int]
+      assert(actual == 30)
+    }
+
     it("Passed ST_NRings") {
       val polyDf =
         sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((1 0, 1 1, 2 1, 2 0, 
1 0))') AS geom")
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 ea399f4f1..176ab296a 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
@@ -2916,6 +2916,28 @@ class functionTestScala
     assert(actual == 1)
   }
 
+  it("Should pass ST_GeneratePoints") {
+    var actual = sparkSession
+      .sql("SELECT 
ST_NumGeometries(ST_GeneratePoints(ST_Buffer(ST_GeomFromWKT('LINESTRING(50 
50,150 150,150 50)'), 10, false, 'endcap=round join=round'), 15))")
+      .first()
+      .get(0)
+    assert(actual == 15)
+
+    actual = sparkSession
+      .sql("SELECT 
ST_NumGeometries(ST_GeneratePoints(ST_GeomFromWKT('MULTIPOLYGON (((10 0, 10 10, 
20 10, 20 0, 10 0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))'), 30))")
+      .first()
+      .get(0)
+    assert(actual == 30)
+
+    actual = sparkSession
+      .sql("SELECT 
ST_AsText(ST_ReducePrecision(ST_GeneratePoints(ST_GeomFromWKT('MULTIPOLYGON 
(((10 0, 10 10, 20 10, 20 0, 10 0)), ((50 0, 50 10, 70 10, 70 0, 50 0)))'), 5, 
10), 5))")
+      .first()
+      .get(0)
+    val expected =
+      "MULTIPOINT ((53.82582 2.57803), (13.55212 2.44117), (59.12854 3.70611), 
(61.37698 7.14985), (10.49657 4.40622))"
+    assertEquals(expected, actual)
+  }
+
   it("should pass ST_NRings") {
     val geomTestCases = Map(
       ("'POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'") -> 1,


Reply via email to