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)
+ }
+ }
}