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 8253419e [SEDONA-302] Add ST_Translate | [SEDONA-196] Update tests for 
ST_Force3D (#862)
8253419e is described below

commit 8253419e096e354774b60297dacdf8648be35ab0
Author: Nilesh Gajwani <[email protected]>
AuthorDate: Wed Jun 14 22:54:59 2023 -0700

    [SEDONA-302] Add ST_Translate | [SEDONA-196] Update tests for ST_Force3D 
(#862)
---
 .../java/org/apache/sedona/common/Functions.java   |  17 ++-
 .../org/apache/sedona/common/utils/GeomUtils.java  |  21 +++-
 .../org/apache/sedona/common/FunctionsTest.java    | 126 +++++++++++++++++++++
 docs/api/flink/Function.md                         |  25 ++++
 docs/api/sql/Function.md                           |  23 ++++
 .../main/java/org/apache/sedona/flink/Catalog.java |   3 +-
 .../apache/sedona/flink/expressions/Functions.java |  16 +++
 .../java/org/apache/sedona/flink/FunctionTest.java |   9 ++
 python/sedona/sql/st_functions.py                  |  21 +++-
 python/tests/sql/test_dataframe_api.py             |   3 +-
 python/tests/sql/test_function.py                  |   6 +
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |   1 +
 .../sql/sedona_sql/expressions/Functions.scala     |   7 ++
 .../sql/sedona_sql/expressions/st_functions.scala  |  10 ++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |  16 +++
 .../org/apache/sedona/sql/functionTestScala.scala  |  19 ++++
 16 files changed, 316 insertions(+), 7 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 fdd6cc9a..1efd0f47 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -835,9 +835,10 @@ public class Functions {
         Coordinate[] points = geometry.getCoordinates();
         if(points.length == 0)
             return points;
-        boolean is3d = !Double.isNaN(points[0].z);
+
         Coordinate[] coordinates = new Coordinate[points.length];
         for(int i = 0; i < points.length; i++) {
+            boolean is3d = !Double.isNaN(points[i].z);
             coordinates[i] = points[i].copy();
             if(!is3d)
                 coordinates[i].z = 0.0;
@@ -881,6 +882,20 @@ public class Functions {
         return numRings;
     }
 
+    public static Geometry translate(Geometry geometry, double deltaX, double 
deltaY, double deltaZ) {
+        if (!geometry.isEmpty()) {
+            GeomUtils.translateGeom(geometry, deltaX, deltaY, deltaZ);
+        }
+        return geometry;
+    }
+
+    public static Geometry translate(Geometry geometry, double deltaX, double 
deltaY) {
+        if (!geometry.isEmpty()) {
+            GeomUtils.translateGeom(geometry, deltaX, deltaY, 0.0);
+        }
+        return geometry;
+    }
+
     public static Geometry geometricMedian(Geometry geometry, double 
tolerance, int maxIter, boolean failIfNotConverged) throws Exception {
         String geometryType = geometry.getGeometryType();
         if(!(Geometry.TYPENAME_POINT.equals(geometryType) || 
Geometry.TYPENAME_MULTIPOINT.equals(geometryType))) {
diff --git a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java 
b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java
index 13585df8..8795f830 100644
--- a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java
+++ b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java
@@ -14,6 +14,8 @@
 package org.apache.sedona.common.utils;
 
 import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
 import org.locationtech.jts.geom.impl.CoordinateArraySequence;
 
 import org.locationtech.jts.geom.CoordinateSequence;
@@ -25,8 +27,10 @@ import org.locationtech.jts.io.WKTWriter;
 import org.locationtech.jts.operation.polygonize.Polygonizer;
 import org.locationtech.jts.operation.union.UnaryUnionOp;
 
+import java.awt.*;
 import java.nio.ByteOrder;
 import java.util.*;
+import java.util.List;
 
 import static org.locationtech.jts.geom.Coordinate.NULL_ORDINATE;
 
@@ -424,8 +428,8 @@ public class GeomUtils {
     public static Geometry get3DGeom(Geometry geometry, double zValue) {
         Coordinate[] coordinates = geometry.getCoordinates();
         if (coordinates.length == 0) return geometry;
-        boolean is3d = !Double.isNaN(coordinates[0].z);
         for(int i = 0; i < coordinates.length; i++) {
+            boolean is3d = !Double.isNaN(coordinates[i].z);
             if(!is3d) {
                 coordinates[i].setZ(zValue);
             }
@@ -442,4 +446,19 @@ public class GeomUtils {
             return 1 + polygon.getNumInteriorRing();
         }
     }
+
+    public static void translateGeom(Geometry geometry, double deltaX, double 
deltaY, double deltaZ) {
+        Coordinate[] coordinates = geometry.getCoordinates();
+        for (int i = 0; i < coordinates.length; i++) {
+            Coordinate currCoordinate = coordinates[i];
+            currCoordinate.setX(currCoordinate.getX() + deltaX);
+            currCoordinate.setY(currCoordinate.getY() + deltaY);
+            if (!Double.isNaN(currCoordinate.z)) {
+                currCoordinate.setZ(currCoordinate.getZ() + deltaZ);
+            }
+        }
+        if (deltaX != 0 || deltaY != 0 || deltaZ != 0) {
+            geometry.geometryChanged();
+        }
+    }
 }
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 71525ec1..3a1745b6 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -649,6 +649,46 @@ public class FunctionsTest {
         assertEquals(emptyLine.isEmpty(), forcedEmptyLine.isEmpty());
     }
 
+    @Test
+    public void force3DHybridGeomCollection() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        Polygon polygon3D = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 1, 
1, 2, 2, 2, 3, 3, 3, 1, 1, 1));
+        MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new 
Polygon[] {polygon3D, polygon});
+        Point point3D = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1, 1));
+        LineString lineString = 
GEOMETRY_FACTORY.createLineString(coordArray(1, 0, 1, 1, 1, 2));
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        Geometry geomCollection = 
GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] 
{GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, 
point3D, emptyLineString, lineString})});
+        Polygon expectedPolygon3D = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 2, 1, 1, 2, 2, 1, 2, 2, 0, 2, 
1, 0, 2));
+        LineString expectedLineString3D = 
GEOMETRY_FACTORY.createLineString(coordArray3d(1, 0, 2, 1, 1, 2, 1, 2, 2));
+        Geometry actualGeometryCollection = Functions.force3D(geomCollection, 
2);
+        WKTWriter wktWriter3D = new WKTWriter(3);
+        assertEquals(wktWriter3D.write(polygon3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(0).getGeometryN(0)));
+        assertEquals(wktWriter3D.write(expectedPolygon3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(0).getGeometryN(1)));
+        assertEquals(wktWriter3D.write(point3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(1)));
+        assertEquals(emptyLineString.toText(), 
actualGeometryCollection.getGeometryN(0).getGeometryN(2).toText());
+        assertEquals(wktWriter3D.write(expectedLineString3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(3)));
+    }
+
+    @Test
+    public void force3DHybridGeomCollectionDefaultValue() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        Polygon polygon3D = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 1, 
1, 2, 2, 2, 3, 3, 3, 1, 1, 1));
+        MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new 
Polygon[] {polygon3D, polygon});
+        Point point3D = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1, 1));
+        LineString lineString = 
GEOMETRY_FACTORY.createLineString(coordArray(1, 0, 1, 1, 1, 2));
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        Geometry geomCollection = 
GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] 
{GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, 
point3D, emptyLineString, lineString})});
+        Polygon expectedPolygon3D = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 0, 1, 1, 0, 2, 1, 0, 2, 0, 0, 
1, 0, 0));
+        LineString expectedLineString3D = 
GEOMETRY_FACTORY.createLineString(coordArray3d(1, 0, 0, 1, 1, 0, 1, 2, 0));
+        Geometry actualGeometryCollection = Functions.force3D(geomCollection);
+        WKTWriter wktWriter3D = new WKTWriter(3);
+        assertEquals(wktWriter3D.write(polygon3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(0).getGeometryN(0)));
+        assertEquals(wktWriter3D.write(expectedPolygon3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(0).getGeometryN(1)));
+        assertEquals(wktWriter3D.write(point3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(1)));
+        assertEquals(emptyLineString.toText(), 
actualGeometryCollection.getGeometryN(0).getGeometryN(2).toText());
+        assertEquals(wktWriter3D.write(expectedLineString3D), 
wktWriter3D.write(actualGeometryCollection.getGeometryN(0).getGeometryN(3)));
+    }
+
     @Test
     public void nRingsPolygonOnlyExternal() throws Exception {
         Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
@@ -731,4 +771,90 @@ public class FunctionsTest {
         assertEquals(expected, e.getMessage());
     }
 
+    @Test
+    public void translateEmptyObjectNoDeltaZ() {
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        String expected = emptyLineString.toText();
+        String actual = Functions.translate(emptyLineString, 1, 1).toText();
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void translateEmptyObjectDeltaZ() {
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        String expected = emptyLineString.toText();
+        String actual = Functions.translate(emptyLineString, 1, 3, 2).toText();
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void translate2DGeomNoDeltaZ() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        String expected = GEOMETRY_FACTORY.createPolygon(coordArray(2, 4, 2, 
5, 3, 5, 3, 4, 2, 4)).toText();
+        String actual = Functions.translate(polygon, 1, 4).toText();
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void translate2DGeomDeltaZ() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        String expected = GEOMETRY_FACTORY.createPolygon(coordArray(2, 3, 2, 
4, 3, 4, 3, 3, 2, 3)).toText();
+        String actual = Functions.translate(polygon, 1, 3, 2).toText();
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void translate3DGeomNoDeltaZ() {
+        WKTWriter wktWriter = new WKTWriter(3);
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 1, 
1, 1, 1, 2, 1, 1, 2, 0, 1, 1, 0, 1));
+        Polygon expectedPolygon = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(2, 5, 1, 2, 6, 1, 3, 6, 1, 3, 5, 1, 
2, 5, 1));
+        assertEquals(wktWriter.write(expectedPolygon), 
wktWriter.write(Functions.translate(polygon, 1, 5)));
+    }
+
+    @Test
+    public void translate3DGeomDeltaZ() {
+        WKTWriter wktWriter = new WKTWriter(3);
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 1, 
1, 1, 1, 2, 1, 1, 2, 0, 1, 1, 0, 1));
+        Polygon expectedPolygon = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(2, 2, 4, 2, 3, 4, 3, 3, 4, 3, 2, 4, 
2, 2, 4));
+        assertEquals(wktWriter.write(expectedPolygon), 
wktWriter.write(Functions.translate(polygon, 1, 2, 3)));
+    }
+
+    @Test
+    public void translateHybridGeomCollectionNoDeltaZ() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        Polygon polygon3D = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 
1, 2, 0, 2, 2, 1, 2, 1, 0, 1));
+        MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new 
Polygon[] {polygon3D, polygon});
+        Point point3D = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1, 1));
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        Geometry geomCollection = 
GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] 
{GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, 
point3D, emptyLineString})});
+        Polygon expectedPolygon = GEOMETRY_FACTORY.createPolygon(coordArray(2, 
2, 2, 3, 3, 3, 3, 2, 2, 2));
+        Polygon expectedPolygon3D = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(2, 2, 1, 3, 2, 2, 3, 3, 2, 2, 2, 
1));
+        Point expectedPoint3D = GEOMETRY_FACTORY.createPoint(new Coordinate(2, 
3, 1));
+        WKTWriter wktWriter3D = new WKTWriter(3);
+        GeometryCollection actualGeometry = (GeometryCollection) 
Functions.translate(geomCollection, 1, 2);
+        assertEquals(wktWriter3D.write(expectedPolygon3D), 
wktWriter3D.write(actualGeometry.getGeometryN(0).getGeometryN(0).getGeometryN(0)));
+        assertEquals(expectedPolygon.toText(), 
actualGeometry.getGeometryN(0).getGeometryN(0).getGeometryN(1).toText());
+        assertEquals(wktWriter3D.write(expectedPoint3D), 
wktWriter3D.write(actualGeometry.getGeometryN(0).getGeometryN(1)));
+        assertEquals(emptyLineString.toText(), 
actualGeometry.getGeometryN(0).getGeometryN(2).toText());
+    }
+
+    @Test
+    public void translateHybridGeomCollectionDeltaZ() {
+        Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 
1, 2, 1, 2, 0, 1, 0));
+        Polygon polygon3D = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 
1, 2, 0, 2, 2, 1, 2, 1, 0, 1));
+        MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new 
Polygon[] {polygon3D, polygon});
+        Point point3D = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1, 1));
+        LineString emptyLineString = GEOMETRY_FACTORY.createLineString();
+        Geometry geomCollection = 
GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] 
{GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {multiPolygon, 
point3D, emptyLineString})});
+        Polygon expectedPolygon = GEOMETRY_FACTORY.createPolygon(coordArray(2, 
3, 2, 4, 3, 4, 3, 3, 2, 3));
+        Polygon expectedPolygon3D = 
GEOMETRY_FACTORY.createPolygon(coordArray3d(2, 3, 6, 3, 3, 7, 3, 4, 7, 2, 3, 
6));
+        Point expectedPoint3D = GEOMETRY_FACTORY.createPoint(new Coordinate(2, 
4, 6));
+        WKTWriter wktWriter3D = new WKTWriter(3);
+        GeometryCollection actualGeometry = (GeometryCollection) 
Functions.translate(geomCollection, 1, 3, 5);
+
+        assertEquals(wktWriter3D.write(expectedPolygon3D), 
wktWriter3D.write(actualGeometry.getGeometryN(0).getGeometryN(0).getGeometryN(0)));
+        assertEquals(expectedPolygon.toText(), 
actualGeometry.getGeometryN(0).getGeometryN(0).getGeometryN(1).toText());
+        assertEquals(wktWriter3D.write(expectedPoint3D), 
wktWriter3D.write(actualGeometry.getGeometryN(0).getGeometryN(1)));
+        assertEquals(emptyLineString.toText(), 
actualGeometry.getGeometryN(0).getGeometryN(2).toText());
+    }
 }
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 24793c45..fb68b55c 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -993,6 +993,31 @@ FROM polygondf
 !!!note
     The detailed EPSG information can be searched on 
[EPSG.io](https://epsg.io/).
 
+## ST_Translate
+Introduction: Returns the input geometry with its X, Y and Z coordinates (if 
present in the geometry) translated by deltaX, deltaY and deltaZ (if specified)
+
+If the geometry is 2D, and a deltaZ parameter is specified, no change is done 
to the Z coordinate of the geometry and the resultant geometry is also 2D.
+
+If the geometry is empty, no change is done to it.
+
+If the given geometry contains sub-geometries (GEOMETRY COLLECTION, MULTI 
POLYGON/LINE/POINT), all underlying geometries are individually translated.
+
+Format: `ST_Translate(geometry: geometry, deltaX: deltaX, deltaY: deltaY, 
deltaZ: deltaZ)`
+
+Since: `1.4.1`
+
+Example: 
+
+Input: `ST_Translate(GEOMETRYCOLLECTION(MULTIPOLYGON (((1 0, 1 1, 2 1, 2 0, 1 
0)), ((1 2, 3 4, 3 5, 1 2))), POINT(1, 1, 1), LINESTRING EMPTY), 2, 2, 3)`
+
+Output: `GEOMETRYCOLLECTION(MULTIPOLYGON (((3 2, 3 3, 4 3, 4 2, 3 2)), ((3 4, 
5 6, 5 7, 3 4))), POINT(3, 3, 4), LINESTRING EMPTY)`
+
+Input: `ST_Translate(POINT(1, 3, 2), 1, 2)`
+
+Output: `POINT(2, 5, 2)`
+
+
+
 ## ST_X
 
 Introduction: Returns X Coordinate of given Point, null otherwise.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index c3665ecd..4ef78b14 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1598,6 +1598,29 @@ FROM polygondf
 !!!note
        The detailed EPSG information can be searched on 
[EPSG.io](https://epsg.io/).
 
+
+## ST_Translate
+Introduction: Returns the input geometry with its X, Y and Z coordinates (if 
present in the geometry) translated by deltaX, deltaY and deltaZ (if specified)
+
+If the geometry is 2D, and a deltaZ parameter is specified, no change is done 
to the Z coordinate of the geometry and the resultant geometry is also 2D.
+
+If the geometry is empty, no change is done to it. 
+If the given geometry contains sub-geometries (GEOMETRY COLLECTION, MULTI 
POLYGON/LINE/POINT), all underlying geometries are individually translated.
+
+Format: `ST_Translate(geometry: geometry, deltaX: deltaX, deltaY: deltaY, 
deltaZ: deltaZ)`
+
+Since: `1.4.1`
+
+Example:
+
+Input: `ST_Translate(GEOMETRYCOLLECTION(MULTIPOLYGON (((1 0, 1 1, 2 1, 2 0, 1 
0)), ((1 2, 3 4, 3 5, 1 2))), POINT(1, 1, 1), LINESTRING EMPTY), 2, 2, 3)`
+
+Output: `GEOMETRYCOLLECTION(MULTIPOLYGON (((3 2, 3 3, 4 3, 4 2, 3 2)), ((3 4, 
5 6, 5 7, 3 4))), POINT(3, 3, 4), LINESTRING EMPTY)`
+
+Input: `ST_Translate(POINT(1, 3, 2), 1, 2)`
+
+Output: `POINT(2, 5, 2)`
+
 ## ST_Union
 
 Introduction: Return the union of geometry A and B
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 1000e370..8d355959 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -97,7 +97,8 @@ public class Catalog {
                 new Functions.ST_GeometricMedian(),
                 new Functions.ST_NumPoints(),
                 new Functions.ST_Force3D(),
-                new Functions.ST_NRings()
+                new Functions.ST_NRings(),
+                new Functions.ST_Translate(),
         };
     }
 
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 dce65f6e..79adcc8d 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
@@ -607,4 +607,20 @@ public class Functions {
         }
     }
 
+    public static class ST_Translate extends ScalarFunction {
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint("Double") Double deltaX, 
@DataTypeHint("Double") Double deltaY) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.translate(geometry, 
deltaX, deltaY);
+        }
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint("Double") Double deltaX, 
@DataTypeHint("Double") Double deltaY, @DataTypeHint("Double") Double deltaZ) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.translate(geometry, 
deltaX, deltaY, deltaZ);
+        }
+    }
+
 }
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 2fd80acd..0bf2536d 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -727,4 +727,13 @@ public class FunctionTest extends TestBase{
         assertEquals(expected, actual);
     }
 
+    @Test
+    public void testTranslate() {
+        Table polyTable = tableEnv.sqlQuery("SELECT 
ST_Translate(ST_GeomFromWKT('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'), 2, 5)" + 
"AS " + polygonColNames[0]);
+        polyTable = 
polyTable.select(call(Functions.ST_AsText.class.getSimpleName(), 
$(polygonColNames[0])));
+        String expected = "POLYGON ((3 5, 3 6, 4 6, 4 5, 3 5))";
+        String actual = (String) first(polyTable).getField(0);
+        assertEquals(expected, actual);
+    }
+
 }
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index fbfc73f4..32a2f272 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -110,7 +110,8 @@ __all__ = [
     "ST_ZMin",
     "ST_NumPoints",
     "ST_Force3D",
-    "ST_NRings"
+    "ST_NRings",
+    "ST_Translate"
 ]
 
 
@@ -1234,7 +1235,7 @@ def ST_ZMin(geometry: ColumnOrName) -> Column:
     :rtype: Column
     """
     return _call_st_function("ST_ZMin", geometry)
-
+@validate_argument_types
 def ST_NumPoints(geometry: ColumnOrName) -> Column:
     """Return the number of points in a LineString
     :param geometry: Geometry column to get number of points from.
@@ -1244,7 +1245,7 @@ def ST_NumPoints(geometry: ColumnOrName) -> Column:
     """
     return _call_st_function("ST_NumPoints", geometry)
 
-
+@validate_argument_types
 def ST_Force3D(geometry: ColumnOrName, zValue: Optional[Union[ColumnOrName, 
float]] = 0.0) -> Column:
     """
     Return a geometry with a 3D coordinate of value 'zValue' forced upon it. 
No change happens if the geometry is already 3D
@@ -1255,6 +1256,7 @@ def ST_Force3D(geometry: ColumnOrName, zValue: 
Optional[Union[ColumnOrName, floa
     args = (geometry, zValue)
     return _call_st_function("ST_Force3D", args)
 
+@validate_argument_types
 def ST_NRings(geometry: ColumnOrName) -> Column:
     """
     Returns the total number of rings in a Polygon or MultiPolygon. Compared 
to ST_NumInteriorRings, ST_NRings takes exterior rings into account as well.
@@ -1262,3 +1264,16 @@ def ST_NRings(geometry: ColumnOrName) -> Column:
     :return: Number of exterior rings + interior rings (if any) for the given 
Polygon or MultiPolygon
     """
     return _call_st_function("ST_NRings", geometry)
+@validate_argument_types
+def ST_Translate(geometry: ColumnOrName, deltaX: Union[ColumnOrName, float], 
deltaY: Union[ColumnOrName, float], deltaZ: Optional[Union[ColumnOrName, 
float]] = 0.0) -> Column:
+    """
+    Returns the geometry with x, y and z (if present) coordinates offset by 
given deltaX, deltaY, and deltaZ values.
+    :param geometry: Geometry column whose coordinates are to be translated.
+    :param deltaX: value by which to offset X coordinate.
+    :param deltaY: value by which to offset Y coordinate.
+    :param deltaZ: value by which to offset Z coordinate (if present).
+    :return: The input geometry with its coordinates translated.
+    """
+    args = (geometry, deltaX, deltaY, deltaZ)
+    return _call_st_function("ST_Translate", args)
+
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index c4cf1847..56229b0e 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -85,7 +85,7 @@ test_configurations = [
     (stf.ST_ExteriorRing, ("geom",), "triangle_geom", "", "LINESTRING (0 0, 1 
0, 1 1, 0 0)"),
     (stf.ST_FlipCoordinates, ("point",), "point_geom", "", "POINT (1 0)"),
     (stf.ST_Force_2D, ("point",), "point_geom", "", "POINT (0 1)"),
-    (stf.ST_Force3D, ("point", 1), "point_geom", "", "POINT Z (0 1 1)"),
+    (stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
     (stf.ST_GeometricMedian, ("multipoint",), "multipoint_geom", "", "POINT 
(22.500002656424286 21.250001168173426)"),
     (stf.ST_GeometryN, ("geom", 0), "multipoint", "", "POINT (0 0)"),
     (stf.ST_GeometryType, ("point",), "point_geom", "", "ST_Point"),
@@ -130,6 +130,7 @@ test_configurations = [
     (stf.ST_SubDivideExplode, ("line", 5), "linestring_geom", 
"collect_list(geom)", ["LINESTRING (0 0, 2.5 0)", "LINESTRING (2.5 0, 5 0)"]),
     (stf.ST_SymDifference, ("a", "b"), "overlapping_polys", "", "MULTIPOLYGON 
(((1 0, 0 0, 0 1, 1 1, 1 0)), ((2 0, 2 1, 3 1, 3 0, 2 0)))"),
     (stf.ST_Transform, ("point", lambda: f.lit("EPSG:4326"), lambda: 
f.lit("EPSG:32649")), "point_geom", "ST_PrecisionReduce(geom, 2)", "POINT 
(-33788209.77 0)"),
+    (stf.ST_Translate, ("geom", 1.0, 1.0,), "square_geom", "", "POLYGON ((2 1, 
2 2, 3 2, 3 1, 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_X, ("b",), "two_points", "", 3.0),
     (stf.ST_XMax, ("line",), "linestring_geom", "", 5.0),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index 4f8e670d..6897fb8e 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1092,3 +1092,9 @@ class TestPredicateJoin(TestBase):
         actual = actualDf.selectExpr("ST_NRings(geom)").take(1)[0][0]
         assert expected == actual
 
+    def test_translate(self):
+        expected = "POLYGON ((3 5, 3 6, 4 6, 4 5, 3 5))"
+        actualDf = self.spark.sql("SELECT 
ST_Translate(ST_GeomFromText('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'), 2, 5) AS 
geom")
+        actual = actualDf.selectExpr("ST_AsText(geom)").take(1)[0][0]
+        assert expected == actual
+
diff --git a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala 
b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 1b0e1dbc..9df4bb31 100644
--- a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -150,6 +150,7 @@ object Catalog {
     function[ST_NumPoints](),
     function[ST_Force3D](0.0),
     function[ST_NRings](),
+    function[ST_Translate](0.0),
     // Expression for rasters
     function[RS_NormalizedDifference](),
     function[RS_Mean](),
diff --git 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index c3b6e6c6..41052806 100644
--- 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1003,3 +1003,10 @@ case class ST_NRings(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_Translate(inputExpressions: Seq[Expression])
+  extends InferredQuarternaryExpression(Functions.translate) with 
FoldableExpression {
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
diff --git 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 42662421..6e110e6a 100644
--- 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -317,4 +317,14 @@ object st_functions extends DataFrameAPI {
   def ST_NRings(geometry: Column): Column = wrapExpression[ST_NRings](geometry)
 
   def ST_NRings(geometry: String): Column = wrapExpression[ST_NRings](geometry)
+
+  def ST_Translate(geometry: Column, deltaX: Column, deltaY: Column, deltaZ: 
Column): Column = wrapExpression[ST_Translate](geometry, deltaX, deltaY, deltaZ)
+
+  def ST_Translate(geometry: String, deltaX: Double, deltaY: Double, deltaZ: 
Double): Column = wrapExpression[ST_Translate](geometry, deltaX, deltaY, deltaZ)
+
+  def ST_Translate(geometry: Column, deltaX: Column, deltaY: Column): Column = 
wrapExpression[ST_Translate](geometry, deltaX, deltaY, 0.0)
+
+  def ST_Translate(geometry: String, deltaX: Double, deltaY: Double): Column = 
wrapExpression[ST_Translate](geometry, deltaX, deltaY, 0.0)
+
+
 }
diff --git 
a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala 
b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 5e183e7a..a9aab844 100644
--- 
a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ 
b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -978,5 +978,21 @@ class dataFrameAPITestScala extends TestBaseScala {
       val actual = df.take(1)(0).getInt(0)
       assert(expected == actual)
     }
+
+    it("Passed ST_Translate") {
+      val polyDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((1 0 1, 1 
1 1, 2 1 1, 2 0 1, 1 0 1))') AS geom")
+      val df = polyDf.select(ST_Translate("geom", 2, 3, 1))
+      val wktWriter3D = new WKTWriter(3);
+      val actualGeom = df.take(1)(0).get(0).asInstanceOf[Geometry]
+      val actual = wktWriter3D.write(actualGeom)
+      val expected = "POLYGON Z((3 3 2, 3 4 2, 4 4 2, 4 3 2, 3 3 2))"
+      assert(expected == actual)
+
+      val dfDefaultValue = polyDf.select(ST_Translate("geom", 2, 3))
+      val actualGeomDefaultValue = 
dfDefaultValue.take(1)(0).get(0).asInstanceOf[Geometry]
+      val actualDefaultValue = wktWriter3D.write(actualGeomDefaultValue)
+      val expectedDefaultValue = "POLYGON Z((3 3 1, 3 4 1, 4 4 1, 4 3 1, 3 3 
1))"
+      assert(expectedDefaultValue == actualDefaultValue)
+    }
   }
 }
diff --git 
a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala 
b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 5dd12782..2bab5e54 100644
--- a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -1954,4 +1954,23 @@ class functionTestScala extends TestBaseScala with 
Matchers with GeometrySample
       assertEquals(expected, actual)
     }
   }
+
+  it ("should pass ST_Translate") {
+    val geomTestCases = Map(
+      ("'POINT (1 1 1)'") -> ("'POINT Z(2 2 2)'", "'POINT Z(2 2 1)'"),
+      ("'POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'") -> ("'POLYGON ((2 1, 2 2, 3 2, 
3 1, 2 1))'", "'POLYGON ((2 1, 2 2, 3 2, 3 1, 2 1))'"),
+      ("'LINESTRING EMPTY'") -> ("'LINESTRING EMPTY'", "'LINESTRING EMPTY'"),
+      ("'GEOMETRYCOLLECTION (MULTIPOLYGON (((1 0, 1 1, 2 1, 2 0, 1 0)), ((1 2, 
3 4, 3 5, 1 2))))'") -> ("'GEOMETRYCOLLECTION (MULTIPOLYGON (((2 1, 2 2, 3 2, 3 
1, 2 1)), ((2 3, 4 5, 4 6, 2 3))))'", "'GEOMETRYCOLLECTION (MULTIPOLYGON (((2 
1, 2 2, 3 2, 3 1, 2 1)), ((2 3, 4 5, 4 6, 2 3))))'")
+    )
+    for (((geom), expectedResult) <- geomTestCases) {
+      val df = sparkSession.sql(s"SELECT 
ST_AsText(ST_Translate(ST_GeomFromWKT($geom), 1, 1, 1)) AS geom, " + 
s"$expectedResult")
+      val dfDefaultValue = sparkSession.sql(s"SELECT 
ST_AsText(ST_Translate(ST_GeomFromWKT($geom), 1, 1)) AS geom, " + 
s"$expectedResult")
+      val actual = df.take(1)(0).get(0).asInstanceOf[String]
+      val actualDefaultValue = 
dfDefaultValue.take(1)(0).get(0).asInstanceOf[String]
+      val expected = 
df.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(0).asInstanceOf[String]
+      val expectedDefaultValue = 
dfDefaultValue.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(1).asInstanceOf[String]
+      assertEquals(expected, actual)
+      assertEquals(expectedDefaultValue, actualDefaultValue)
+    }
+  }
 }

Reply via email to