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 34c4ad1495 [SEDONA-676] Add ST_Perimeter (#1686)
34c4ad1495 is described below

commit 34c4ad1495f4aca9f94f9853223a8bfeff36bc12
Author: Furqaan Khan <[email protected]>
AuthorDate: Thu Nov 21 00:39:30 2024 -0500

    [SEDONA-676] Add ST_Perimeter (#1686)
    
    * feat: add one variant of ST_Perimeter and temp commit
    
    * feat: add ST_Perimeter with 2 and 3 arg versions
    
    * fix: test by having low decimal tolerance
    
    * fix: snowflake tests
    
    * feat: lenient assumes that input geom is 4326. fix: snowflake tests
    
    * fix: snowflake tests and remove V2 test as GeoJSON doesn't contain SRID
    
    * fix: minor snowflake test bug
    
    * fix: minor snowflake test bug 2/2
---
 .../java/org/apache/sedona/common/Functions.java   | 40 ++++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    | 73 ++++++++++++++++++++++
 docs/api/flink/Function.md                         | 45 +++++++++++++
 docs/api/snowflake/vector-data/Function.md         | 43 +++++++++++++
 docs/api/sql/Function.md                           | 45 +++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  1 +
 .../apache/sedona/flink/expressions/Functions.java | 27 ++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 26 ++++++++
 python/sedona/sql/st_functions.py                  | 24 +++++++
 python/tests/sql/test_dataframe_api.py             |  9 +++
 python/tests/sql/test_function.py                  | 19 ++++++
 .../sedona/snowflake/snowsql/TestFunctions.java    | 19 ++++++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  | 12 ++++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  | 15 +++++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    | 24 +++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  1 +
 .../sql/sedona_sql/expressions/Functions.scala     | 11 ++++
 .../sql/sedona_sql/expressions/st_functions.scala  | 11 ++++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  | 19 ++++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 18 ++++++
 20 files changed, 482 insertions(+)

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 c93bae3e2f..8502c241bd 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1094,6 +1094,46 @@ public class Functions {
     return geom.getFactory().createPoint(interPoint);
   }
 
+  public static double perimeter(Geometry geometry, boolean use_spheroid, 
boolean lenient) {
+    if (use_spheroid && geometry.getSRID() != 4326) {
+      if (!lenient) {
+        throw new IllegalArgumentException(
+            "For spheroidal perimeter calculations, the input geometry must be 
in the WGS84 CRS (SRID 4326).");
+      }
+    }
+
+    String geomType = geometry.getGeometryType();
+    if (geomType.equalsIgnoreCase(Geometry.TYPENAME_POLYGON)) {
+      return calculateLength(geometry, use_spheroid);
+    } else if (geomType.equalsIgnoreCase(Geometry.TYPENAME_MULTIPOLYGON)) {
+      return calculateLength(geometry, use_spheroid);
+    } else if 
(geomType.equalsIgnoreCase(Geometry.TYPENAME_GEOMETRYCOLLECTION)) {
+      double perimeter = 0;
+      for (int i = 0; i < geometry.getNumGeometries(); i++) {
+        perimeter += perimeter(geometry.getGeometryN(i), use_spheroid, 
lenient);
+      }
+      return perimeter;
+    } else {
+      return 0;
+    }
+  }
+
+  private static double calculateLength(Geometry geometry, boolean 
use_spheroid) {
+    if (use_spheroid) {
+      return Spheroid.length(geometry);
+    } else {
+      return length(geometry);
+    }
+  }
+
+  public static double perimeter(Geometry geometry, boolean use_spheroid) {
+    return perimeter(geometry, use_spheroid, true);
+  }
+
+  public static double perimeter(Geometry geometry) {
+    return perimeter(geometry, false);
+  }
+
   /**
    * Forces a Polygon/MultiPolygon to use clockwise orientation for the 
exterior ring and a
    * counter-clockwise for the interior ring(s).
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 74ab3483b0..72b850b900 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -1100,6 +1100,79 @@ public class FunctionsTest extends TestBase {
     assertEquals(0, expected.compareTo(actual, 
COORDINATE_SEQUENCE_COMPARATOR));
   }
 
+  @Test
+  public void testPerimeter() throws ParseException {
+    Geometry geom =
+        Constructors.geomFromWKT(
+            "MULTIPOLYGON(((763104.471273676 2949418.44119003,763104.477769673 
2949418.42538203,763104.189609677 2949418.22343004,763104.471273676 
2949418.44119003)),((763104.471273676 2949418.44119003,763095.804579742 
2949436.33850239,763086.132105649 2949451.46730207,763078.452329651 
2949462.11549407,763075.354136904 2949466.17407812,763064.362142565 
2949477.64291974,763059.953961626 2949481.28983009,762994.637609571 
2949532.04103014,762990.568508415 2949535.06640477,762986.710889563 2 [...]
+            2249);
+    double actual = Functions.perimeter(geom);
+    double expected = 845.227713366824;
+    assertEquals(expected, actual, FP_TOLERANCE);
+
+    geom =
+        Constructors.geomFromWKT(
+            "GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0.5 1.5, 0 1, 0 0)), 
MULTIPOLYGON (((2 2, 2 3, 3 3, 3 2, 2 2)), ((4 4, 4 5, 5 5, 5 4, 4 4), (4.2 
4.2, 4.8 4.2, 4.8 4.8, 4.2 4.8, 4.2 4.2))))",
+            0);
+    actual = Functions.perimeter(geom);
+    expected = 14.814213562373094;
+    assertEquals(expected, actual, FP_TOLERANCE);
+
+    geom = Constructors.geomFromWKT("LINESTRING (0 0, 100 100)", 0);
+    actual = Functions.perimeter(geom);
+    expected = 0;
+    assertEquals(expected, actual, FP_TOLERANCE);
+
+    geom =
+        Constructors.geomFromWKT(
+            "GEOMETRYCOLLECTION(LINESTRING (6 6, 7 7, 8 6), POINT (9 9), POINT 
(10 10))", 0);
+    actual = Functions.perimeter(geom);
+    expected = 0;
+    assertEquals(expected, actual, FP_TOLERANCE);
+  }
+
+  @Test
+  public void testPerimeterSpherical() throws ParseException {
+    Geometry geom =
+        Constructors.geomFromWKT(
+            "MULTIPOLYGON (((-122.33 47.61, -122.32 47.62, -122.31 47.61, 
-122.30 47.62, -122.29 47.61, -122.30 47.60, -122.31 47.59, -122.32 47.60, 
-122.33 47.61), (-122.315 47.605, -122.305 47.615, -122.295 47.605, -122.305 
47.595, -122.315 47.605)), ((-122.35 47.65, -122.34 47.66, -122.33 47.65, 
-122.32 47.66, -122.31 47.65, -122.32 47.64, -122.33 47.63, -122.34 47.64, 
-122.35 47.65)))",
+            4326);
+    double actual = Functions.perimeter(geom, true, false);
+    double expected = 26841.6072;
+    assertEquals(expected, actual, FP_TOLERANCE2);
+
+    geom =
+        Constructors.geomFromWKT(
+            "POLYGON((-122.33 47.61, -122.32 47.62, -122.31 47.61, -122.30 
47.62, -122.29 47.61, -122.30 47.60, -122.31 47.59, -122.32 47.60, -122.33 
47.61), (-122.315 47.605, -122.305 47.615, -122.295 47.605, -122.305 47.595, 
-122.315 47.605))",
+            4326);
+    actual = Functions.perimeter(geom, true, false);
+    expected = 16106.5064;
+    assertEquals(expected, actual, FP_TOLERANCE2);
+
+    geom =
+        Constructors.geomFromWKT(
+            "POLYGON((-122.33 47.61, -122.32 47.62, -122.31 47.61, -122.30 
47.62, -122.29 47.61, -122.30 47.60, -122.31 47.59, -122.32 47.60, -122.33 
47.61))",
+            4326);
+    actual = Functions.perimeter(geom, true, false);
+    expected = 10737.6184;
+    assertEquals(expected, actual, FP_TOLERANCE2);
+
+    geom.setSRID(0);
+    actual = Functions.perimeter(geom, true);
+    expected = 10737.6184;
+    assertEquals(expected, actual, FP_TOLERANCE2);
+
+    // ignores the LineString and just calculates the perimeter of the Polygon 
in this Geometry
+    // Collection
+    geom =
+        Constructors.geomFromWKT(
+            "GEOMETRYCOLLECTION(LINESTRING(10 10, 20 20, 30 10),POLYGON((40 
40, 50 40, 50 50, 40 50, 40 40)))",
+            4326);
+    actual = Functions.perimeter(geom, true, false);
+    expected = 3792549.0135;
+    assertEquals(expected, actual, FP_TOLERANCE2);
+  }
+
   @Test
   public void testForcePolygonCW() throws ParseException {
     Geometry polyCCW =
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 56c358a3bf..8bcaa9e41d 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -3044,6 +3044,51 @@ Output:
 2
 ```
 
+## ST_Perimeter
+
+Introduction: This function calculates the 2D perimeter of a given geometry. 
It supports Polygon, MultiPolygon, and GeometryCollection geometries (as long 
as the GeometryCollection contains polygonal geometries). For other types, it 
returns 0. To measure lines, use [ST_Length](#st_length).
+
+To get the perimeter in meters, set `use_spheroid` to `true`. This calculates 
the geodesic perimeter using the WGS84 spheroid. When using `use_spheroid`, the 
`lenient` parameter defaults to true, assuming the geometry uses EPSG:4326. To 
throw an exception instead, set `lenient` to `false`.
+
+Format:
+
+`ST_Perimeter(geom: Geometry)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean, lenient: Boolean = True)`
+
+Since: `v1.7.0`
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))')
+)
+```
+
+Output:
+
+```
+20.0
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))', 4326),
+        true, false
+)
+```
+
+Output:
+
+```
+2216860.5497177234
+```
+
 ## ST_PointN
 
 Introduction: Return the Nth point in a single linestring or circular 
linestring in the geometry. Negative values are counted backwards from the end 
of the LineString, so that -1 is the last point. Returns NULL if there is no 
linestring in the geometry.
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index d3dc43d772..7d0e6e408d 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -2278,6 +2278,49 @@ SELECT ST_NumPoints(ST_GeomFromText('LINESTRING(0 1, 1 
0, 2 0)'))
 
 Output: `3`
 
+## ST_Perimeter
+
+Introduction: This function calculates the 2D perimeter of a given geometry. 
It supports Polygon, MultiPolygon, and GeometryCollection geometries (as long 
as the GeometryCollection contains polygonal geometries). For other types, it 
returns 0. To measure lines, use [ST_Length](#st_length).
+
+To get the perimeter in meters, set `use_spheroid` to `true`. This calculates 
the geodesic perimeter using the WGS84 spheroid. When using `use_spheroid`, the 
`lenient` parameter defaults to true, assuming the geometry uses EPSG:4326. To 
throw an exception instead, set `lenient` to `false`.
+
+Format:
+
+`ST_Perimeter(geom: Geometry)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean, lenient: Boolean = True)`
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))')
+)
+```
+
+Output:
+
+```
+20.0
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))', 4326),
+        true, false
+)
+```
+
+Output:
+
+```
+2216860.5497177234
+```
+
 ## ST_PointN
 
 Introduction: Return the Nth point in a single linestring or circular 
linestring in the geometry. Negative values are counted backwards from the end 
of the LineString, so that -1 is the last point. Returns NULL if there is no 
linestring in the geometry.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index d909bd3cab..ac63ca7cf9 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -3129,6 +3129,51 @@ Output:
 3
 ```
 
+## ST_Perimeter
+
+Introduction: This function calculates the 2D perimeter of a given geometry. 
It supports Polygon, MultiPolygon, and GeometryCollection geometries (as long 
as the GeometryCollection contains polygonal geometries). For other types, it 
returns 0. To measure lines, use [ST_Length](#st_length).
+
+To get the perimeter in meters, set `use_spheroid` to `true`. This calculates 
the geodesic perimeter using the WGS84 spheroid. When using `use_spheroid`, the 
`lenient` parameter defaults to true, assuming the geometry uses EPSG:4326. To 
throw an exception instead, set `lenient` to `false`.
+
+Format:
+
+`ST_Perimeter(geom: Geometry)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean)`
+
+`ST_Perimeter(geom: Geometry, use_spheroid: Boolean, lenient: Boolean = True)`
+
+Since: `v1.7.0`
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))')
+)
+```
+
+Output:
+
+```
+20.0
+```
+
+SQL Example:
+
+```sql
+SELECT ST_Perimeter(
+        ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))', 4326),
+        true, false
+)
+```
+
+Output:
+
+```
+2216860.5497177234
+```
+
 ## ST_PointN
 
 Introduction: Return the Nth point in a single linestring or circular 
linestring in the geometry. Negative values are counted backwards from the end 
of the LineString, so that -1 is the last point. Returns NULL if there is no 
linestring in the geometry.
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 5844fc30f0..396ad16cf0 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -100,6 +100,7 @@ public class Catalog {
       new FunctionsGeoTools.ST_Transform(),
       new Functions.ST_FlipCoordinates(),
       new Functions.ST_GeoHash(),
+      new Functions.ST_Perimeter(),
       new Functions.ST_PointOnSurface(),
       new Functions.ST_Scale(),
       new Functions.ST_ScaleGeom(),
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 83d53e999b..15789ad25a 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
@@ -568,6 +568,33 @@ public class Functions {
     }
   }
 
+  public static class ST_Perimeter extends ScalarFunction {
+    @DataTypeHint(value = "Double")
+    public Double eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+            Object o) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.perimeter(geom);
+    }
+
+    @DataTypeHint(value = "Double")
+    public Double eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+        Boolean use_spheroid) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.perimeter(geom, use_spheroid);
+    }
+
+    @DataTypeHint(value = "Double")
+    public Double eval(
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+        Boolean use_spheroid,
+        boolean lenient) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.perimeter(geom, use_spheroid, 
lenient);
+    }
+  }
+
   public static class ST_PointOnSurface extends ScalarFunction {
     @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
     public Geometry 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 d3d569d940..cbb9fec605 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -764,6 +764,32 @@ public class FunctionTest extends TestBase {
     assertEquals("POINTM", first(pointTable).getField(0));
   }
 
+  @Test
+  public void testPerimeter() {
+    Table polygonTable = createPolygonTable(testDataSize);
+    Table perimeterTable =
+        polygonTable.select(
+            call(Functions.ST_Perimeter.class.getSimpleName(), 
$(polygonColNames[0])));
+    Double perimeter = (Double) first(perimeterTable).getField(0);
+    assertEquals(4.0, perimeter, FP_TOLERANCE);
+
+    polygonTable =
+        tableEnv.sqlQuery(
+            "SELECT ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))', 
4326) AS geom");
+    perimeterTable =
+        polygonTable.select(
+            call(Functions.ST_Perimeter.class.getSimpleName(), $("geom"), 
true, false));
+    perimeter = (Double) first(perimeterTable).getField(0);
+    assertEquals(443770.91724830196, perimeter, FP_TOLERANCE);
+
+    polygonTable =
+        tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 1, 1 
0, 0 0))') AS geom");
+    perimeterTable =
+        polygonTable.select(call(Functions.ST_Perimeter.class.getSimpleName(), 
$("geom"), true));
+    perimeter = (Double) first(perimeterTable).getField(0);
+    assertEquals(443770.91724830196, perimeter, FP_TOLERANCE);
+  }
+
   @Test
   public void testPointOnSurface() {
     Table pointTable = createPointTable_real(testDataSize);
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index 3bec08d7bf..9e77909c15 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1170,6 +1170,30 @@ def ST_MakeLine(geom1: ColumnOrName, geom2: 
Optional[ColumnOrName] = None) -> Co
     return _call_st_function("ST_MakeLine", args)
 
 
+@validate_argument_types
+def ST_Perimeter(
+    geom: ColumnOrName,
+    use_spheroid: Optional[Union[ColumnOrName, bool]] = None,
+    lenient: Optional[Union[ColumnOrName, bool]] = None,
+) -> Column:
+    """Returns the perimeter of a Polygon/MultiPolygon geometries. Otherwise, 
returns 0
+
+    @param geom: Polygonal geometry
+    @param use_spheroid: Use Spheroid
+    @param lenient: suppresses the exception
+    @return: Perimeter of a Polygon/MultiPolygon geometries
+    """
+
+    args = (geom, use_spheroid, lenient)
+
+    if lenient is None:
+        if use_spheroid is None:
+            args = (geom,)
+        else:
+            args = (geom, use_spheroid)
+    return _call_st_function("ST_Perimeter", args)
+
+
 @validate_argument_types
 def ST_Points(geometry: ColumnOrName) -> Column:
     """Creates a MultiPoint geometry consisting of all the coordinates of the 
input geometry
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 9c5c65e093..5683d6d29c 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -731,6 +731,15 @@ test_configurations = [
         0.2927864015850548,
     ),
     (stf.ST_MaxDistance, ("a", "b"), "overlapping_polys", "", 
3.1622776601683795),
+    (stf.ST_Perimeter, ("geom",), "triangle_geom", "", 3.414213562373095),
+    (stf.ST_Perimeter, ("geom", True), "triangle_geom", "ceil(geom)", 378794),
+    (
+        stf.ST_Perimeter,
+        (lambda: stf.ST_SetSRID("geom", 4326), True),
+        "triangle_geom",
+        "ceil(geom)",
+        378794,
+    ),
     (
         stf.ST_Points,
         ("line",),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index ec21d853b1..e92dbe4c7f 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1649,6 +1649,25 @@ class TestPredicateJoin(TestBase):
         for actual, expected in result:
             assert actual == expected
 
+    def test_st_perimeter(self):
+        baseDf = self.spark.sql(
+            "SELECT ST_GeomFromWKT('POLYGON((743238 2967416,743238 
2967450,743265 2967450,743265.625 2967416,743238 2967416))') AS geom"
+        )
+        actual = baseDf.selectExpr("ST_Perimeter(geom)").take(1)[0][0]
+        expected = 122.63074400009504
+        assert actual == expected
+
+        baseDf = self.spark.sql(
+            "SELECT ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))', 
4326) AS geom"
+        )
+        actual = baseDf.selectExpr("ST_Perimeter(geom, true)").first()[0]
+        expected = 443770.91724830196
+        assert expected == actual
+
+        actual = baseDf.selectExpr("ST_Perimeter(geom, true, 
false)").first()[0]
+        expected = 443770.91724830196
+        assert expected == actual
+
     def test_st_points(self):
         # Given
         geometry_df = self.spark.createDataFrame(
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 157994b329..c3fdd3160a 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
@@ -769,6 +769,25 @@ public class TestFunctions extends TestBase {
         "POINT (5 6)");
   }
 
+  @Test
+  public void test_ST_Perimeter() {
+    registerUDF("ST_Perimeter", byte[].class);
+    verifySqlSingleRes(
+        "SELECT sedona.ST_Perimeter(sedona.ST_GeomFromText('POLYGON((0 0, 0 5, 
5 5, 5 0, 0 0))'))",
+        20.0);
+
+    registerUDF("ST_Perimeter", byte[].class, boolean.class);
+    verifySqlSingleRes(
+        "SELECT CEIL(sedona.ST_Perimeter(sedona.ST_GeomFromText('POLYGON((0 0, 
0 5, 5 5, 5 0, 0 0))'), true))",
+        2216861.0);
+
+    registerUDF("ST_Perimeter", byte[].class, boolean.class, boolean.class);
+    registerUDF("ST_GeomFromText", String.class, int.class);
+    verifySqlSingleRes(
+        "SELECT CEIL(sedona.ST_Perimeter(sedona.ST_GeomFromText('POLYGON((0 0, 
0 5, 5 5, 5 0, 0 0))', 4326), true, false))",
+        2216861.0);
+  }
+
   @Test
   public void test_ST_PointOnSurface() {
     registerUDF("ST_PointOnSurface", 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 a387f5d9e6..e1b9ccc7de 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
@@ -719,6 +719,18 @@ public class TestFunctionsV2 extends TestBase {
         "POINT(5 6)");
   }
 
+  @Test
+  public void test_ST_Perimeter() {
+    registerUDFV2("ST_Perimeter", String.class);
+    verifySqlSingleRes(
+        "SELECT sedona.ST_Perimeter(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 
0, 0 0))'))", 20.0);
+
+    registerUDFV2("ST_Perimeter", String.class, boolean.class);
+    verifySqlSingleRes(
+        "SELECT CEIL(sedona.ST_Perimeter(ST_GeomFromText('POLYGON((0 0, 0 5, 5 
5, 5 0, 0 0))'), true))",
+        2216861.0);
+  }
+
   @Test
   public void test_ST_PointOnSurface() {
     registerUDFV2("ST_PointOnSurface", 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 83c075cd6b..368cdb3e4b 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
@@ -807,6 +807,21 @@ public class UDFs {
     return 
GeometrySerde.serialize(Functions.pointN(GeometrySerde.deserialize(geometry), 
n));
   }
 
+  @UDFAnnotations.ParamMeta(argNames = {"geometry"})
+  public static double ST_Perimeter(byte[] geometry) {
+    return Functions.perimeter(GeometrySerde.deserialize(geometry));
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geometry", "use_spheroid"})
+  public static double ST_Perimeter(byte[] geometry, boolean use_spheroid) {
+    return Functions.perimeter(GeometrySerde.deserialize(geometry), 
use_spheroid);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geometry", "use_spheroid", "lenient"})
+  public static double ST_Perimeter(byte[] geometry, boolean use_spheroid, 
boolean lenient) {
+    return Functions.perimeter(GeometrySerde.deserialize(geometry), 
use_spheroid, lenient);
+  }
+
   @UDFAnnotations.ParamMeta(argNames = {"geometry"})
   public static byte[] ST_PointOnSurface(byte[] geometry) {
     return 
GeometrySerde.serialize(Functions.pointOnSurface(GeometrySerde.deserialize(geometry)));
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 a645f87836..a8e6e2efd6 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
@@ -939,6 +939,30 @@ public class UDFsV2 {
     return 
GeometrySerde.serGeoJson(Functions.pointN(GeometrySerde.deserGeoJson(geometry), 
n));
   }
 
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry"},
+      argTypes = {"Geometry"},
+      returnTypes = "double")
+  public static double ST_Perimeter(String geometry) {
+    return Functions.perimeter(GeometrySerde.deserGeoJson(geometry));
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry", "use_spheroid"},
+      argTypes = {"Geometry", "boolean"},
+      returnTypes = "double")
+  public static double ST_Perimeter(String geometry, boolean use_spheroid) {
+    return Functions.perimeter(GeometrySerde.deserGeoJson(geometry), 
use_spheroid);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry", "use_spheroid", "lenient"},
+      argTypes = {"Geometry", "boolean", "boolean"},
+      returnTypes = "double")
+  public static double ST_Perimeter(String geometry, boolean use_spheroid, 
boolean lenient) {
+    return Functions.perimeter(GeometrySerde.deserGeoJson(geometry), 
use_spheroid, lenient);
+  }
+
   @UDFAnnotations.ParamMeta(
       argNames = {"geometry"},
       argTypes = {"Geometry"},
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 442d0203c7..b491375379 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
@@ -55,6 +55,7 @@ object Catalog {
     function[ST_GeomFromGML](),
     function[ST_GeomFromKML](),
     function[ST_CoordDim](),
+    function[ST_Perimeter](),
     function[ST_Point](),
     function[ST_Points](),
     function[ST_MakeEnvelope](),
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 91f833e936..dc8a290b8a 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
@@ -993,6 +993,17 @@ case class ST_MakeLine(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_Perimeter(inputExpressions: Seq[Expression])
+    extends InferredExpression(
+      inferrableFunction3(Functions.perimeter),
+      inferrableFunction2(Functions.perimeter),
+      inferrableFunction1(Functions.perimeter)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class ST_Points(inputExpressions: Seq[Expression])
     extends InferredExpression(Functions.points _) {
 
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 e7b11c3c17..7bb753cc28 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
@@ -358,6 +358,17 @@ object st_functions extends DataFrameAPI {
   def ST_MakeLine(geom1: String, geom2: String): Column =
     wrapExpression[ST_MakeLine](geom1, geom2)
 
+  def ST_Perimeter(geom: Column): Column = wrapExpression[ST_Perimeter](geom)
+  def ST_Perimeter(geom: String): Column = wrapExpression[ST_Perimeter](geom)
+  def ST_Perimeter(geom: Column, use_spheroid: Column): Column =
+    wrapExpression[ST_Perimeter](geom, use_spheroid)
+  def ST_Perimeter(geom: String, use_spheroid: Boolean): Column =
+    wrapExpression[ST_Perimeter](geom, use_spheroid)
+  def ST_Perimeter(geom: Column, use_spheroid: Column, lenient: Column): 
Column =
+    wrapExpression[ST_Perimeter](geom, use_spheroid, lenient)
+  def ST_Perimeter(geom: String, use_spheroid: Boolean, lenient: Boolean): 
Column =
+    wrapExpression[ST_Perimeter](geom, use_spheroid, lenient)
+
   def ST_Points(geom: Column): Column = wrapExpression[ST_Points](geom)
   def ST_Points(geom: String): Column = wrapExpression[ST_Points](geom)
 
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 e0dbe5a60a..cf9b8a0f7a 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
@@ -760,6 +760,25 @@ class dataFrameAPITestScala extends TestBaseScala {
       assert(actualResult.toText() == expectedResult)
     }
 
+    it("Passed ST_Perimeter") {
+      var baseDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT('POLYGON((743238 2967416,743238 2967450,743265 
2967450,743265.625 2967416,743238 2967416))') AS geom")
+      var actual = baseDf.select(ST_Perimeter("geom")).first().get(0)
+      var expected = 122.63074400009504
+      assertEquals(expected, actual)
+
+      baseDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))', 4326) AS 
geom")
+      actual = baseDf.select(ST_Perimeter("geom", use_spheroid = 
true)).first().get(0)
+      expected = 443770.91724830196
+      assertEquals(expected, actual)
+
+      actual =
+        baseDf.select(ST_Perimeter("geom", use_spheroid = true, lenient = 
false)).first().get(0)
+      expected = 443770.91724830196
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_Project") {
       val baseDf = sparkSession.sql(
         "SELECT ST_GeomFromWKT('POINT(0 0)') as point, ST_MakeEnvelope(0, 1, 
2, 0) as poly")
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 e727cfdd10..9bda2da441 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
@@ -595,6 +595,24 @@ class functionTestScala
       assert(row.get(1).asInstanceOf[Geometry].toText.equals("LINESTRING (5 6, 
7 8, 9 10)"))
     }
 
+    it("Passed ST_Perimeter") {
+      var baseDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT('POLYGON((743238 2967416,743238 2967450,743265 
2967450,743265.625 2967416,743238 2967416))') AS geom")
+      var actual = baseDf.selectExpr("ST_Perimeter(geom)").first().get(0)
+      var expected = 122.63074400009504
+      assertEquals(expected, actual)
+
+      baseDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))', 4326) AS 
geom")
+      actual = baseDf.selectExpr("ST_Perimeter(geom, true)").first().get(0)
+      expected = 443770.91724830196
+      assertEquals(expected, actual)
+
+      actual = baseDf.selectExpr("ST_Perimeter(geom, true, 
false)").first().get(0)
+      expected = 443770.91724830196
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_Points") {
 
       val testtable = sparkSession.sql(

Reply via email to