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 3d9913b9e9 [GH-2799] Add ST_OffsetCurve function (#2811)
3d9913b9e9 is described below
commit 3d9913b9e9c724b9609a796f7341641ab0a890c7
Author: Jia Yu <[email protected]>
AuthorDate: Wed Apr 1 09:03:44 2026 -0700
[GH-2799] Add ST_OffsetCurve function (#2811)
---
.../java/org/apache/sedona/common/Functions.java | 15 ++++
.../org/apache/sedona/common/FunctionsTest.java | 46 ++++++++++++
docs/api/flink/Geometry-Functions.md | 1 +
.../flink/Geometry-Processing/ST_OffsetCurve.md | 62 ++++++++++++++++
.../snowflake/vector-data/Geometry-Functions.md | 1 +
.../Geometry-Processing/ST_OffsetCurve.md | 60 ++++++++++++++++
docs/api/sql/Geometry-Functions.md | 1 +
docs/api/sql/Geometry-Processing/ST_OffsetCurve.md | 62 ++++++++++++++++
.../ST_OffsetCurve/ST_OffsetCurve_negative.svg | 55 ++++++++++++++
.../ST_OffsetCurve/ST_OffsetCurve_positive.svg | 44 ++++++++++++
.../ST_OffsetCurve/ST_OffsetCurve_quadrant.svg | 83 ++++++++++++++++++++++
.../main/java/org/apache/sedona/flink/Catalog.java | 1 +
.../apache/sedona/flink/expressions/Functions.java | 33 +++++++++
.../java/org/apache/sedona/flink/FunctionTest.java | 27 +++++++
python/sedona/spark/sql/st_functions.py | 26 +++++++
python/tests/sql/test_dataframe_api.py | 15 ++++
python/tests/sql/test_function.py | 26 +++++++
.../sedona/snowflake/snowsql/TestFunctions.java | 13 ++++
.../sedona/snowflake/snowsql/TestFunctionsV2.java | 13 ++++
.../org/apache/sedona/snowflake/snowsql/UDFs.java | 12 ++++
.../apache/sedona/snowflake/snowsql/UDFsV2.java | 18 +++++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 +
.../sql/sedona_sql/expressions/Functions.scala | 10 +++
.../sql/sedona_sql/expressions/st_functions.scala | 9 +++
.../org/apache/sedona/sql/PreserveSRIDSuite.scala | 1 +
.../apache/sedona/sql/dataFrameAPITestScala.scala | 17 +++++
.../org/apache/sedona/sql/functionTestScala.scala | 23 ++++++
27 files changed, 675 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 fdec8913c3..78bd874051 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -54,6 +54,7 @@ import org.locationtech.jts.io.kml.KMLWriter;
import org.locationtech.jts.linearref.LengthIndexedLine;
import org.locationtech.jts.operation.buffer.BufferOp;
import org.locationtech.jts.operation.buffer.BufferParameters;
+import org.locationtech.jts.operation.buffer.OffsetCurve;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.locationtech.jts.operation.distance3d.Distance3DOp;
import org.locationtech.jts.operation.linemerge.LineMerger;
@@ -424,6 +425,20 @@ public class Functions {
return bufferParameters;
}
+ public static Geometry offsetCurve(Geometry geometry, double distance) {
+ return offsetCurve(geometry, distance,
BufferParameters.DEFAULT_QUADRANT_SEGMENTS);
+ }
+
+ public static Geometry offsetCurve(Geometry geometry, double distance, int
quadrantSegments) {
+ if (geometry.isEmpty()) {
+ return null;
+ }
+ BufferParameters bufferParameters = new BufferParameters();
+ bufferParameters.setQuadrantSegments(quadrantSegments);
+ OffsetCurve oc = new OffsetCurve(geometry, distance, bufferParameters);
+ return oc.getCurve();
+ }
+
public static int bestSRID(Geometry geometry) throws
IllegalArgumentException {
// Shift longitudes if geometry crosses dateline
if (crossesDateLine(geometry)) {
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index e48f62c546..92b5948905 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -4250,6 +4250,52 @@ public class FunctionsTest extends TestBase {
assertEquals(0.0, result.getLength(), 1e-6);
}
+ @Test
+ public void offsetCurvePositiveDistance() throws ParseException {
+ // Offset to the left of a horizontal line
+ Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0)", 0);
+ Geometry result = Functions.offsetCurve(line, 5.0);
+ String actual = Functions.asWKT(result);
+ assertEquals("LINESTRING (0 5, 10 5)", actual);
+ }
+
+ @Test
+ public void offsetCurveNegativeDistance() throws ParseException {
+ // Offset to the right of a horizontal line
+ Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0)", 0);
+ Geometry result = Functions.offsetCurve(line, -5.0);
+ String actual = Functions.asWKT(result);
+ assertEquals("LINESTRING (0 -5, 10 -5)", actual);
+ }
+
+ @Test
+ public void offsetCurveZeroDistance() throws ParseException {
+ // Zero distance returns a copy of the input
+ Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0)", 0);
+ Geometry result = Functions.offsetCurve(line, 0.0);
+ String actual = Functions.asWKT(result);
+ assertEquals("LINESTRING (0 0, 10 0)", actual);
+ }
+
+ @Test
+ public void offsetCurveEmptyGeometry() throws ParseException {
+ // Empty geometry returns null
+ Geometry empty = Constructors.geomFromWKT("LINESTRING EMPTY", 0);
+ Geometry result = Functions.offsetCurve(empty, 5.0);
+ assertNull(result);
+ }
+
+ @Test
+ public void offsetCurveWithQuadrantSegments() throws ParseException {
+ // Test with custom quadrant segments on a line with a corner
+ Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0, 10 10)",
0);
+ Geometry defaultResult = Functions.offsetCurve(line, -3.0);
+ Geometry customResult = Functions.offsetCurve(line, -3.0, 16);
+ assertNotNull(customResult);
+ // Higher quadrantSegments produces more points on the arc at outer corners
+ assertTrue(customResult.getNumPoints() > defaultResult.getNumPoints());
+ }
+
@Test
public void testZmFlag() throws ParseException {
int _2D = 0, _3DM = 1, _3DZ = 2, _4D = 3;
diff --git a/docs/api/flink/Geometry-Functions.md
b/docs/api/flink/Geometry-Functions.md
index d0c3f42d5c..489ebf11f7 100644
--- a/docs/api/flink/Geometry-Functions.md
+++ b/docs/api/flink/Geometry-Functions.md
@@ -221,6 +221,7 @@ These functions compute geometric constructions, or alter
geometry size or shape
| [ST_LabelPoint](Geometry-Processing/ST_LabelPoint.md) | Geometry |
`ST_LabelPoint` computes and returns a label point for a given polygon or
geometry collection. The label point is chosen to be sufficiently far from
boundaries of the geometry. For a regular Polygo... | v1.7.1 |
| [ST_MinimumBoundingCircle](Geometry-Processing/ST_MinimumBoundingCircle.md)
| Geometry | Returns the smallest circle polygon that contains a geometry. The
optional quadrantSegments parameter determines how many segments to use per
quadrant and the default number of segments is 48. | v1.5.0 |
| [ST_MinimumBoundingRadius](Geometry-Processing/ST_MinimumBoundingRadius.md)
| Struct | Returns a struct containing the center point and radius of the
smallest circle that contains a geometry. | v1.5.0 |
+| [ST_OffsetCurve](Geometry-Processing/ST_OffsetCurve.md) | Geometry | Returns
a line at a given offset distance from a linear geometry. Positive distance
offsets to the left, negative to the right. | v1.9.0 |
| [ST_OrientedEnvelope](Geometry-Processing/ST_OrientedEnvelope.md) | Geometry
| Returns the minimum-area rotated rectangle enclosing a geometry. The
rectangle may be rotated relative to the coordinate axes. Degenerate inputs may
result in a Point or LineString being returned. | v1.8.1 |
| [ST_PointOnSurface](Geometry-Processing/ST_PointOnSurface.md) | Geometry |
Returns a POINT guaranteed to lie on the surface. | v1.2.1 |
| [ST_Polygonize](Geometry-Processing/ST_Polygonize.md) | Geometry | Generates
a GeometryCollection composed of polygons that are formed from the linework of
an input GeometryCollection. When the input does not contain any linework that
forms a polygon, the function... | v1.6.0 |
diff --git a/docs/api/flink/Geometry-Processing/ST_OffsetCurve.md
b/docs/api/flink/Geometry-Processing/ST_OffsetCurve.md
new file mode 100644
index 0000000000..0e70866f00
--- /dev/null
+++ b/docs/api/flink/Geometry-Processing/ST_OffsetCurve.md
@@ -0,0 +1,62 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_OffsetCurve
+
+Introduction: Returns a line at a given offset distance from a linear
geometry. If the distance is positive, the offset is on the left side of the
input line; if it is negative, it is on the right side. Returns null for empty
geometries.
+
+The optional third parameter `quadrantSegments` controls the number of line
segments used to approximate a quarter circle at round joins. The default value
is 8.
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double,
quadrantSegments: Integer)`
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), 5.0))
+```
+
+Output: `LINESTRING (0 5, 5 5, 5 10)`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0))
+```
+
+Output: `11`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0, 16))
+```
+
+Output: `19`
diff --git a/docs/api/snowflake/vector-data/Geometry-Functions.md
b/docs/api/snowflake/vector-data/Geometry-Functions.md
index e22c5d0a30..01ecdda07d 100644
--- a/docs/api/snowflake/vector-data/Geometry-Functions.md
+++ b/docs/api/snowflake/vector-data/Geometry-Functions.md
@@ -214,6 +214,7 @@ These functions compute geometric constructions, or alter
geometry size or shape
|
[ST_MaximumInscribedCircle](Geometry-Processing/ST_MaximumInscribedCircle.md) |
Finds the largest circle that is contained within a (multi)polygon, or which
does not overlap any lines and points. Returns a row with fields: |
| [ST_MinimumBoundingCircle](Geometry-Processing/ST_MinimumBoundingCircle.md)
| Returns the smallest circle polygon that contains a geometry. |
| [ST_MinimumBoundingRadius](Geometry-Processing/ST_MinimumBoundingRadius.md)
| Returns two columns containing the center point and radius of the smallest
circle that contains a geometry. |
+| [ST_OffsetCurve](Geometry-Processing/ST_OffsetCurve.md) | Returns a line at
a given offset distance from a linear geometry. Positive distance offsets to
the left, negative to the right. |
| [ST_OrientedEnvelope](Geometry-Processing/ST_OrientedEnvelope.md) | Returns
the minimum-area rotated rectangle enclosing a geometry. The rectangle may be
rotated relative to the coordinate axes. Degenerate inputs may result in a
Point or LineString being returned. |
| [ST_PointOnSurface](Geometry-Processing/ST_PointOnSurface.md) | Returns a
POINT guaranteed to lie on the surface. |
| [ST_Polygonize](Geometry-Processing/ST_Polygonize.md) | Generates a
GeometryCollection composed of polygons that are formed from the linework of an
input GeometryCollection. When the input does not contain any linework that
forms a polygon, the function... |
diff --git
a/docs/api/snowflake/vector-data/Geometry-Processing/ST_OffsetCurve.md
b/docs/api/snowflake/vector-data/Geometry-Processing/ST_OffsetCurve.md
new file mode 100644
index 0000000000..37ce097cb4
--- /dev/null
+++ b/docs/api/snowflake/vector-data/Geometry-Processing/ST_OffsetCurve.md
@@ -0,0 +1,60 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_OffsetCurve
+
+Introduction: Returns a line at a given offset distance from a linear
geometry. If the distance is positive, the offset is on the left side of the
input line; if it is negative, it is on the right side. Returns null for empty
geometries.
+
+The optional third parameter `quadrantSegments` controls the number of line
segments used to approximate a quarter circle at round joins. The default value
is 8.
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double,
quadrantSegments: Integer)`
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double)`
+
+Return type: `Geometry`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), 5.0))
+```
+
+Output: `LINESTRING (0 5, 5 5, 5 10)`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0))
+```
+
+Output: `11`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0, 16))
+```
+
+Output: `19`
diff --git a/docs/api/sql/Geometry-Functions.md
b/docs/api/sql/Geometry-Functions.md
index 14ce382530..d6d6db7575 100644
--- a/docs/api/sql/Geometry-Functions.md
+++ b/docs/api/sql/Geometry-Functions.md
@@ -223,6 +223,7 @@ These functions compute geometric constructions, or alter
geometry size or shape
|
[ST_MaximumInscribedCircle](Geometry-Processing/ST_MaximumInscribedCircle.md) |
Struct | Finds the largest circle that is contained within a (multi)polygon, or
which does not overlap any lines and points. Returns a row with fields: |
v1.6.1 |
| [ST_MinimumBoundingCircle](Geometry-Processing/ST_MinimumBoundingCircle.md)
| Geometry | Returns the smallest circle polygon that contains a geometry. The
optional quadrantSegments parameter determines how many segments to use per
quadrant and the default number of segments has been ch... | v1.0.1 |
| [ST_MinimumBoundingRadius](Geometry-Processing/ST_MinimumBoundingRadius.md)
| Struct | Returns a struct containing the center point and radius of the
smallest circle that contains a geometry. | v1.0.1 |
+| [ST_OffsetCurve](Geometry-Processing/ST_OffsetCurve.md) | Geometry | Returns
a line at a given offset distance from a linear geometry. Positive distance
offsets to the left, negative to the right. | v1.9.0 |
| [ST_OrientedEnvelope](Geometry-Processing/ST_OrientedEnvelope.md) | Geometry
| Returns the minimum-area rotated rectangle enclosing a geometry. The
rectangle may be rotated relative to the coordinate axes. Degenerate inputs may
result in a Point or LineString being returned. | v1.8.1 |
| [ST_PointOnSurface](Geometry-Processing/ST_PointOnSurface.md) | Geometry |
Returns a POINT guaranteed to lie on the surface. | v1.2.1 |
| [ST_Polygonize](Geometry-Processing/ST_Polygonize.md) | Geometry | Generates
a GeometryCollection composed of polygons that are formed from the linework of
an input GeometryCollection. When the input does not contain any linework that
forms a polygon, the function... | v1.6.0 |
diff --git a/docs/api/sql/Geometry-Processing/ST_OffsetCurve.md
b/docs/api/sql/Geometry-Processing/ST_OffsetCurve.md
new file mode 100644
index 0000000000..0e70866f00
--- /dev/null
+++ b/docs/api/sql/Geometry-Processing/ST_OffsetCurve.md
@@ -0,0 +1,62 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# ST_OffsetCurve
+
+Introduction: Returns a line at a given offset distance from a linear
geometry. If the distance is positive, the offset is on the left side of the
input line; if it is negative, it is on the right side. Returns null for empty
geometries.
+
+The optional third parameter `quadrantSegments` controls the number of line
segments used to approximate a quarter circle at round joins. The default value
is 8.
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double,
quadrantSegments: Integer)`
+
+Format: `ST_OffsetCurve(geometry: Geometry, distance: Double)`
+
+Return type: `Geometry`
+
+Since: `v1.9.0`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), 5.0))
+```
+
+Output: `LINESTRING (0 5, 5 5, 5 10)`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0))
+```
+
+Output: `11`
+
+SQL Example:
+
+
+
+```sql
+SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)'), -3.0, 16))
+```
+
+Output: `19`
diff --git a/docs/image/ST_OffsetCurve/ST_OffsetCurve_negative.svg
b/docs/image/ST_OffsetCurve/ST_OffsetCurve_negative.svg
new file mode 100644
index 0000000000..134472c798
--- /dev/null
+++ b/docs/image/ST_OffsetCurve/ST_OffsetCurve_negative.svg
@@ -0,0 +1,55 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 300" width="500"
height="300" style="background:#ffffff">
+ <defs>
+ <style>
+ text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+ </style>
+ </defs>
+ <text x="250" y="22" text-anchor="middle" font-size="16" font-weight="bold"
fill="#333333">ST_OffsetCurve — Negative Offset on L-shape</text>
+ <!-- Mapping: x range [0,13] → [60,450], y range [-3,10] → [245,35] -->
+ <!-- cx = x*30+60, cy = 245 - (y+3)*16.15 -->
+
+ <!-- Input: LINESTRING(0 0, 10 0, 10 10) -->
+ <polyline points="60,196 360,196 360,35" fill="none" stroke="#4a90d9"
stroke-width="3" />
+ <circle cx="60" cy="196" r="4" fill="#4a90d9" />
+ <circle cx="360" cy="196" r="4" fill="#4a90d9" />
+ <circle cx="360" cy="35" r="4" fill="#4a90d9" />
+ <text x="40" y="192" font-size="11" fill="#4a90d9">(0, 0)</text>
+ <text x="324" y="192" font-size="11" fill="#4a90d9">(10, 0)</text>
+ <text x="364" y="33" font-size="11" fill="#4a90d9">(10, 10)</text>
+
+ <!-- Result: actual JTS output with 9 arc segments (default qs=8) -->
+ <polyline points="60,245 360,245 377,244 394,240 410,235 424,228 435,219
443,209 448,199 450,196 450,35"
+ fill="none" stroke="#e67e22" stroke-width="3" />
+ <!-- Vertex dots on arc segments -->
+ <circle cx="60" cy="245" r="4" fill="#e67e22" />
+ <circle cx="360" cy="245" r="3" fill="#e67e22" />
+ <circle cx="377" cy="244" r="3" fill="#e67e22" />
+ <circle cx="394" cy="240" r="3" fill="#e67e22" />
+ <circle cx="410" cy="235" r="3" fill="#e67e22" />
+ <circle cx="424" cy="228" r="3" fill="#e67e22" />
+ <circle cx="435" cy="219" r="3" fill="#e67e22" />
+ <circle cx="443" cy="209" r="3" fill="#e67e22" />
+ <circle cx="448" cy="199" r="3" fill="#e67e22" />
+ <circle cx="450" cy="196" r="3" fill="#e67e22" />
+ <circle cx="450" cy="35" r="4" fill="#e67e22" />
+ <text x="40" y="253" font-size="11" fill="#e67e22">(0, -3)</text>
+ <text x="453" y="41" font-size="11" fill="#e67e22">(13, 10)</text>
+
+ <!-- Distance marker -->
+ <line x1="200" y1="196" x2="200" y2="245" stroke="#999" stroke-width="1"
stroke-dasharray="3,2" />
+ <line x1="196" y1="196" x2="204" y2="196" stroke="#999" stroke-width="1" />
+ <line x1="196" y1="245" x2="204" y2="245" stroke="#999" stroke-width="1" />
+ <text x="207" y="224" font-size="12" font-weight="bold" fill="#999">d =
3</text>
+
+ <!-- Point count annotation -->
+ <text x="460" y="224" font-size="11" fill="#e67e22">11 points</text>
+ <text x="460" y="237" font-size="11" fill="#e67e22">(9 on arc)</text>
+
+ <!-- Caption -->
+ <text x="250" y="290" text-anchor="middle" font-size="13" fill="#333333"
font-style="italic">Negative offset creates a segmented arc at the outer
corner</text>
+ <!-- Legend -->
+ <rect x="73" y="265" width="12" height="12" rx="2" fill="#4a90d9"
stroke="#4a90d9" stroke-width="1" />
+ <text x="89" y="276" font-size="13" fill="#333333">Input line</text>
+ <rect x="253" y="265" width="12" height="12" rx="2" fill="#e67e22"
stroke="#e67e22" stroke-width="1" />
+ <text x="269" y="276" font-size="13" fill="#333333">Offset curve (distance =
-3.0)</text>
+</svg>
diff --git a/docs/image/ST_OffsetCurve/ST_OffsetCurve_positive.svg
b/docs/image/ST_OffsetCurve/ST_OffsetCurve_positive.svg
new file mode 100644
index 0000000000..a4aa758754
--- /dev/null
+++ b/docs/image/ST_OffsetCurve/ST_OffsetCurve_positive.svg
@@ -0,0 +1,44 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 300" width="500"
height="300" style="background:#ffffff">
+ <defs>
+ <style>
+ text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+ </style>
+ </defs>
+ <text x="250" y="22" text-anchor="middle" font-size="16" font-weight="bold"
fill="#333333">ST_OffsetCurve — Positive Offset on L-shape</text>
+ <!-- Mapping: x range [0,13] → [60,450], y range [-1,10] → [230,35] -->
+ <!-- cx = x*30+60, cy = 230 - y*17.7 -->
+ <!-- (0,0)→(60,230) (10,0)→(360,230) (10,10)→(360,53) -->
+ <!-- (0,5)→(60,141) (5,5)→(210,141) (5,10)→(210,53) -->
+
+ <!-- Input: LINESTRING(0 0, 10 0, 10 10) -->
+ <polyline points="60,230 360,230 360,53" fill="none" stroke="#4a90d9"
stroke-width="3" />
+ <circle cx="60" cy="230" r="4" fill="#4a90d9" />
+ <circle cx="360" cy="230" r="4" fill="#4a90d9" />
+ <circle cx="360" cy="53" r="4" fill="#4a90d9" />
+ <text x="36" y="235" font-size="11" fill="#4a90d9">(0, 0)</text>
+ <text x="363" y="245" font-size="11" fill="#4a90d9">(10, 0)</text>
+ <text x="363" y="50" font-size="11" fill="#4a90d9">(10, 10)</text>
+
+ <!-- Result: LINESTRING(0 5, 5 5, 5 10) — inner corner is clipped, no arc -->
+ <polyline points="60,141 210,141 210,53" fill="none" stroke="#2ecc71"
stroke-width="3" />
+ <circle cx="60" cy="141" r="4" fill="#2ecc71" />
+ <circle cx="210" cy="141" r="4" fill="#2ecc71" />
+ <circle cx="210" cy="53" r="4" fill="#2ecc71" />
+ <text x="36" y="136" font-size="11" fill="#2ecc71">(0, 5)</text>
+ <text x="213" y="155" font-size="11" fill="#2ecc71">(5, 5)</text>
+ <text x="178" y="50" font-size="11" fill="#2ecc71">(5, 10)</text>
+
+ <!-- Distance marker -->
+ <line x1="150" y1="141" x2="150" y2="230" stroke="#999" stroke-width="1"
stroke-dasharray="3,2" />
+ <line x1="146" y1="141" x2="154" y2="141" stroke="#999" stroke-width="1" />
+ <line x1="146" y1="230" x2="154" y2="230" stroke="#999" stroke-width="1" />
+ <text x="156" y="190" font-size="12" font-weight="bold" fill="#999">d =
5</text>
+
+ <!-- Caption -->
+ <text x="250" y="290" text-anchor="middle" font-size="13" fill="#333333"
font-style="italic">LINESTRING (0 5, 5 5, 5 10)</text>
+ <!-- Legend -->
+ <rect x="73" y="265" width="12" height="12" rx="2" fill="#4a90d9"
stroke="#4a90d9" stroke-width="1" />
+ <text x="89" y="276" font-size="13" fill="#333333">Input line</text>
+ <rect x="233" y="265" width="12" height="12" rx="2" fill="#2ecc71"
stroke="#2ecc71" stroke-width="1" />
+ <text x="249" y="276" font-size="13" fill="#333333">Offset curve (distance =
5.0)</text>
+</svg>
diff --git a/docs/image/ST_OffsetCurve/ST_OffsetCurve_quadrant.svg
b/docs/image/ST_OffsetCurve/ST_OffsetCurve_quadrant.svg
new file mode 100644
index 0000000000..11c8c7b478
--- /dev/null
+++ b/docs/image/ST_OffsetCurve/ST_OffsetCurve_quadrant.svg
@@ -0,0 +1,83 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 300" width="500"
height="300" style="background:#ffffff">
+ <defs>
+ <style>
+ text { font-family: 'Segoe UI', Arial, Helvetica, sans-serif; }
+ </style>
+ </defs>
+ <text x="250" y="18" text-anchor="middle" font-size="15" font-weight="bold"
fill="#333333">ST_OffsetCurve — quadrantSegments Effect</text>
+
+ <!-- LEFT CHART: default qs=8 -->
+ <!-- Mapping: x[0,13]→[20,230], y[-3,10]→[230,40] -->
+ <!-- cx = x*16.2+20, cy = 230 - (y+3)*14.6 -->
+ <!-- (0,0)→(20,186) (10,0)→(182,186) (10,10)→(182,40) -->
+ <!-- (0,-3)→(20,230) (13,0)→(231,186) (13,10)→(231,40) -->
+
+ <text x="125" y="36" text-anchor="middle" font-size="13" font-weight="bold"
fill="#e67e22">default (qs=8): 11 points</text>
+
+ <!-- Input -->
+ <polyline points="20,186 182,186 182,40" fill="none" stroke="#4a90d9"
stroke-width="2" />
+
+ <!-- Default arc: 9 arc points from JTS -->
+ <!-- (10,-3)→(182,230) (10.59,-2.94)→(192,229) (11.15,-2.77)→(201,226)
(11.67,-2.49)→(209,222) -->
+ <!-- (12.12,-2.12)→(216,217) (12.49,-1.67)→(222,211) (12.77,-1.15)→(227,203)
(12.94,-0.59)→(230,195) (13,0)→(231,186) -->
+ <polyline points="20,230 182,230 192,229 201,226 209,222 216,217 222,211
227,203 230,195 231,186 231,40"
+ fill="none" stroke="#e67e22" stroke-width="2" />
+ <circle cx="182" cy="230" r="2.5" fill="#e67e22" />
+ <circle cx="192" cy="229" r="2.5" fill="#e67e22" />
+ <circle cx="201" cy="226" r="2.5" fill="#e67e22" />
+ <circle cx="209" cy="222" r="2.5" fill="#e67e22" />
+ <circle cx="216" cy="217" r="2.5" fill="#e67e22" />
+ <circle cx="222" cy="211" r="2.5" fill="#e67e22" />
+ <circle cx="227" cy="203" r="2.5" fill="#e67e22" />
+ <circle cx="230" cy="195" r="2.5" fill="#e67e22" />
+ <circle cx="231" cy="186" r="2.5" fill="#e67e22" />
+
+ <!-- Divider -->
+ <line x1="250" y1="30" x2="250" y2="250" stroke="#ddd" stroke-width="1" />
+
+ <!-- RIGHT CHART: qs=16 -->
+ <!-- Mapping: x[0,13]→[270,480], y[-3,10]→[230,40] -->
+ <!-- cx = x*16.2+270, cy = 230 - (y+3)*14.6 -->
+ <!-- (0,0)→(270,186) (10,0)→(432,186) (10,10)→(432,40) -->
+ <!-- (13,0)→(481,186) (13,10)→(481,40) -->
+
+ <text x="375" y="36" text-anchor="middle" font-size="13" font-weight="bold"
fill="#9b59b6">qs=16: 19 points</text>
+
+ <!-- Input -->
+ <polyline points="270,186 432,186 432,40" fill="none" stroke="#4a90d9"
stroke-width="2" />
+
+ <!-- qs=16 arc: 17 arc points from JTS -->
+ <!-- (10,-3)→(432,230) (10.29,-2.99)→(437,230) (10.59,-2.94)→(442,229)
(10.87,-2.87)→(448,228) -->
+ <!-- (11.15,-2.77)→(451,226) (11.41,-2.65)→(455,223) (11.67,-2.49)→(459,222)
(11.90,-2.32)→(463,219) -->
+ <!-- (12.12,-2.12)→(466,217) (12.32,-1.90)→(470,214) (12.49,-1.67)→(472,211)
(12.65,-1.41)→(475,207) -->
+ <!-- (12.77,-1.15)→(477,203) (12.87,-0.87)→(478,199) (12.94,-0.59)→(480,195)
(12.99,-0.29)→(481,190) (13,0)→(481,186) -->
+ <polyline points="270,230 432,230 437,230 442,229 448,228 451,226 455,223
459,222 463,219 466,217 470,214 472,211 475,207 477,203 478,199 480,195 481,190
481,186 481,40"
+ fill="none" stroke="#9b59b6" stroke-width="2" />
+ <circle cx="432" cy="230" r="2" fill="#9b59b6" />
+ <circle cx="437" cy="230" r="2" fill="#9b59b6" />
+ <circle cx="442" cy="229" r="2" fill="#9b59b6" />
+ <circle cx="448" cy="228" r="2" fill="#9b59b6" />
+ <circle cx="451" cy="226" r="2" fill="#9b59b6" />
+ <circle cx="455" cy="223" r="2" fill="#9b59b6" />
+ <circle cx="459" cy="222" r="2" fill="#9b59b6" />
+ <circle cx="463" cy="219" r="2" fill="#9b59b6" />
+ <circle cx="466" cy="217" r="2" fill="#9b59b6" />
+ <circle cx="470" cy="214" r="2" fill="#9b59b6" />
+ <circle cx="472" cy="211" r="2" fill="#9b59b6" />
+ <circle cx="475" cy="207" r="2" fill="#9b59b6" />
+ <circle cx="477" cy="203" r="2" fill="#9b59b6" />
+ <circle cx="478" cy="199" r="2" fill="#9b59b6" />
+ <circle cx="480" cy="195" r="2" fill="#9b59b6" />
+ <circle cx="481" cy="190" r="2" fill="#9b59b6" />
+ <circle cx="481" cy="186" r="2" fill="#9b59b6" />
+
+ <!-- Caption -->
+ <text x="250" y="280" text-anchor="middle" font-size="13" fill="#333333"
font-style="italic">Higher quadrantSegments adds more line segments to
approximate the arc</text>
+ <!-- Legend -->
+ <rect x="73" y="258" width="12" height="12" rx="2" fill="#4a90d9"
stroke="#4a90d9" stroke-width="1" />
+ <text x="89" y="269" font-size="12" fill="#333333">Input</text>
+ <rect x="163" y="258" width="12" height="12" rx="2" fill="#e67e22"
stroke="#e67e22" stroke-width="1" />
+ <text x="179" y="269" font-size="12" fill="#333333">Offset (qs=8)</text>
+ <rect x="293" y="258" width="12" height="12" rx="2" fill="#9b59b6"
stroke="#9b59b6" stroke-width="1" />
+ <text x="309" y="269" font-size="12" fill="#333333">Offset (qs=16)</text>
+</svg>
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 a10d9f1577..3ef00ce247 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -70,6 +70,7 @@ public class Catalog {
new Functions.ST_BestSRID(),
new Functions.ST_ClosestPoint(),
new Functions.ST_ShortestLine(),
+ new Functions.ST_OffsetCurve(),
new Functions.ST_Centroid(),
new Functions.ST_Collect(),
new Functions.ST_CollectionExtract(),
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 cf5e5267c7..71029be3a4 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
@@ -309,6 +309,39 @@ public class Functions {
}
}
+ public static class ST_OffsetCurve extends ScalarFunction {
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ public Geometry eval(
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ Object o,
+ @DataTypeHint("Double") Double distance) {
+ Geometry geom = (Geometry) o;
+ return org.apache.sedona.common.Functions.offsetCurve(geom, distance);
+ }
+
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ public Geometry eval(
+ @DataTypeHint(
+ value = "RAW",
+ rawSerializer = GeometryTypeSerializer.class,
+ bridgedTo = Geometry.class)
+ Object o,
+ @DataTypeHint("Double") Double distance,
+ @DataTypeHint("Integer") Integer quadrantSegments) {
+ Geometry geom = (Geometry) o;
+ return org.apache.sedona.common.Functions.offsetCurve(geom, distance,
quadrantSegments);
+ }
+ }
+
public static class ST_Centroid extends ScalarFunction {
@DataTypeHint(
value = "RAW",
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 f0d7ffa7f1..2171ecc78e 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -251,6 +251,33 @@ public class FunctionTest extends TestBase {
assertEquals("LINESTRING (0 0, 3 4)", result.toString());
}
+ @Test
+ public void testOffsetCurve() {
+ Table table = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(0 0, 10
0)') AS geom");
+ table = table.select(call(Functions.ST_OffsetCurve.class.getSimpleName(),
$("geom"), 5.0));
+ Geometry result = (Geometry) first(table).getField(0);
+ assertEquals("LINESTRING (0 5, 10 5)", result.toString());
+ }
+
+ @Test
+ public void testOffsetCurveWithQuadrantSegments() {
+ Table table =
+ tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)') AS geom");
+ Table defaultTable =
+ table.select(
+ call(
+ "ST_NPoints",
+ call(Functions.ST_OffsetCurve.class.getSimpleName(),
$("geom"), -3.0)));
+ Table customTable =
+ table.select(
+ call(
+ "ST_NPoints",
+ call(Functions.ST_OffsetCurve.class.getSimpleName(),
$("geom"), -3.0, 16)));
+ int defaultPts = (int) first(defaultTable).getField(0);
+ int customPts = (int) first(customTable).getField(0);
+ assertTrue(customPts > defaultPts);
+ }
+
@Test
public void testCentroid() {
Table polygonTable =
diff --git a/python/sedona/spark/sql/st_functions.py
b/python/sedona/spark/sql/st_functions.py
index f32d57e7ad..186aaadd31 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -1788,6 +1788,32 @@ def ST_OrientedEnvelope(geometry: ColumnOrName) ->
Column:
return _call_st_function("ST_OrientedEnvelope", geometry)
+@validate_argument_types
+def ST_OffsetCurve(
+ geometry: ColumnOrName,
+ distance: ColumnOrNameOrNumber,
+ quadrant_segments: Optional[Union[ColumnOrName, int]] = None,
+) -> Column:
+ """Return a line at a given offset distance from a linear geometry.
+
+ Positive distance offsets to the left, negative to the right.
+
+ :param geometry: Linear geometry column.
+ :type geometry: ColumnOrName
+ :param distance: Offset distance.
+ :type distance: ColumnOrNameOrNumber
+ :param quadrant_segments: Number of segments to approximate a quarter
circle (default 8).
+ :type quadrant_segments: Optional[Union[ColumnOrName, int]]
+ :return: Offset curve as a geometry column.
+ :rtype: Column
+ """
+ if quadrant_segments is None:
+ args = (geometry, distance)
+ else:
+ args = (geometry, distance, quadrant_segments)
+ return _call_st_function("ST_OffsetCurve", args)
+
+
@validate_argument_types
def ST_PointN(geometry: ColumnOrName, n: Union[ColumnOrName, int]) -> Column:
"""Get the n-th point (starts at 1) for a geometry.
diff --git a/python/tests/sql/test_dataframe_api.py
b/python/tests/sql/test_dataframe_api.py
index 920550a1cf..9156b1d48f 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -907,6 +907,20 @@ test_configurations = [
"",
"POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))",
),
+ (
+ stf.ST_OffsetCurve,
+ ("line", 1.0),
+ "linestring_geom",
+ "ST_AsText(geom)",
+ "LINESTRING (0 1, 5 1)",
+ ),
+ (
+ stf.ST_OffsetCurve,
+ ("line", 1.0, 4),
+ "linestring_geom",
+ "ST_AsText(geom)",
+ "LINESTRING (0 1, 5 1)",
+ ),
(stf.ST_PointN, ("line", 2), "linestring_geom", "", "POINT (1 0)"),
(stf.ST_PointOnSurface, ("line",), "linestring_geom", "", "POINT (2 0)"),
(
@@ -1444,6 +1458,7 @@ wrong_type_configurations = [
(stf.ST_MinimumBoundingCircle, (None,)),
(stf.ST_MinimumBoundingRadius, (None,)),
(stf.ST_OrientedEnvelope, (None,)),
+ (stf.ST_OffsetCurve, (None, 1.0)),
(stf.ST_Multi, (None,)),
(stf.ST_Normalize, (None,)),
(stf.ST_NPoints, (None,)),
diff --git a/python/tests/sql/test_function.py
b/python/tests/sql/test_function.py
index 48a24d0ce7..ad7cabe739 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1983,6 +1983,32 @@ class TestPredicateJoin(TestBase):
actual = actual_df.take(1)[0][0]
assert actual is None
+ def test_st_offset_curve(self):
+ # Positive distance offsets to the left
+ actual_df = self.spark.sql(
+ "SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0,
10 0)'), 5.0))"
+ )
+ actual = actual_df.take(1)[0][0]
+ assert actual == "LINESTRING (0 5, 10 5)"
+
+ # Negative distance offsets to the right
+ actual_df = self.spark.sql(
+ "SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0,
10 0)'), -5.0))"
+ )
+ actual = actual_df.take(1)[0][0]
+ assert actual == "LINESTRING (0 -5, 10 -5)"
+
+ # With quadrantSegments parameter on a line with a corner
+ default_df = self.spark.sql(
+ "SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0,
10 0, 10 10)'), -3.0))"
+ )
+ default_pts = default_df.take(1)[0][0]
+ custom_df = self.spark.sql(
+ "SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0,
10 0, 10 10)'), -3.0, 16))"
+ )
+ custom_pts = custom_df.take(1)[0][0]
+ assert custom_pts > default_pts
+
def test_st_collect_on_array_type(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 2c226f70ba..0e66423c10 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
@@ -768,6 +768,19 @@ public class TestFunctions extends TestBase {
"select sedona.ST_NPoints(sedona.ST_GeomFromText('LINESTRING(1 2, 3 4,
5 6)'))", 3);
}
+ @Test
+ public void test_ST_OffsetCurve() {
+ registerUDF("ST_OffsetCurve", byte[].class, double.class);
+ verifySqlSingleRes(
+ "select
sedona.ST_AsText(sedona.ST_OffsetCurve(sedona.ST_GeomFromText('LINESTRING(0 0,
10 0)'), 5.0))",
+ "LINESTRING (0 5, 10 5)");
+ registerUDF("ST_OffsetCurve", byte[].class, double.class, int.class);
+ registerUDF("ST_NPoints", byte[].class);
+ verifySqlSingleRes(
+ "select
sedona.ST_NPoints(sedona.ST_OffsetCurve(sedona.ST_GeomFromText('LINESTRING(0 0,
10 0, 10 10)'), -3.0, 16))",
+ 19);
+ }
+
@Test
public void test_ST_NumGeometries() {
registerUDF("ST_NumGeometries", 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 67bc09b187..df864987fe 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
@@ -716,6 +716,19 @@ public class TestFunctionsV2 extends TestBase {
"select sedona.ST_NPoints(ST_GeometryFromWKT('LINESTRING(1 2, 3 4, 5
6)'))", 3);
}
+ @Test
+ public void test_ST_OffsetCurve() {
+ registerUDFV2("ST_OffsetCurve", String.class, double.class);
+ verifySqlSingleRes(
+ "select
sedona.ST_AsText(sedona.ST_OffsetCurve(ST_GeometryFromWKT('LINESTRING(0 0, 10
0)'), 5.0))",
+ "LINESTRING (0 5, 10 5)");
+ registerUDFV2("ST_OffsetCurve", String.class, double.class, int.class);
+ registerUDFV2("ST_NPoints", String.class);
+ verifySqlSingleRes(
+ "select
sedona.ST_NPoints(sedona.ST_OffsetCurve(ST_GeometryFromWKT('LINESTRING(0 0, 10
0, 10 10)'), -3.0, 16))",
+ 19);
+ }
+
@Test
public void test_ST_NumGeometries() {
registerUDFV2("ST_NumGeometries", 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 dafdc8fc97..f78ad7d3f3 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
@@ -798,6 +798,18 @@ public class UDFs {
return
GeometrySerde.serialize(Functions.normalize(GeometrySerde.deserialize(geometry)));
}
+ @UDFAnnotations.ParamMeta(argNames = {"geometry", "distance"})
+ public static byte[] ST_OffsetCurve(byte[] geometry, double distance) {
+ return GeometrySerde.serialize(
+ Functions.offsetCurve(GeometrySerde.deserialize(geometry), distance));
+ }
+
+ @UDFAnnotations.ParamMeta(argNames = {"geometry", "distance",
"quadrantSegments"})
+ public static byte[] ST_OffsetCurve(byte[] geometry, double distance, int
quadrantSegments) {
+ return GeometrySerde.serialize(
+ Functions.offsetCurve(GeometrySerde.deserialize(geometry), distance,
quadrantSegments));
+ }
+
@UDFAnnotations.ParamMeta(argNames = {"geometry"})
public static int ST_NumGeometries(byte[] geometry) {
return Functions.numGeometries(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 80fbb34d07..18cf8a281f 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
@@ -961,6 +961,24 @@ public class UDFsV2 {
return
GeometrySerde.serGeoJson(Functions.normalize(GeometrySerde.deserGeoJson(geometry)));
}
+ @UDFAnnotations.ParamMeta(
+ argNames = {"geometry", "distance"},
+ argTypes = {"Geometry", "double"},
+ returnTypes = "Geometry")
+ public static String ST_OffsetCurve(String geometry, double distance) {
+ return GeometrySerde.serGeoJson(
+ Functions.offsetCurve(GeometrySerde.deserGeoJson(geometry), distance));
+ }
+
+ @UDFAnnotations.ParamMeta(
+ argNames = {"geometry", "distance", "quadrantSegments"},
+ argTypes = {"Geometry", "double", "int"},
+ returnTypes = "Geometry")
+ public static String ST_OffsetCurve(String geometry, double distance, int
quadrantSegments) {
+ return GeometrySerde.serGeoJson(
+ Functions.offsetCurve(GeometrySerde.deserGeoJson(geometry), distance,
quadrantSegments));
+ }
+
@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 c931694db4..2629d8b178 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
@@ -137,6 +137,7 @@ object Catalog extends AbstractCatalog with Logging {
function[ST_Snap](),
function[ST_ClosestPoint](),
function[ST_ShortestLine](),
+ function[ST_OffsetCurve](),
function[ST_Boundary](),
function[ST_HasZ](),
function[ST_HasM](),
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 0925dfb104..f15efb0e70 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
@@ -1012,6 +1012,16 @@ private[apache] case class
ST_ShortestLine(inputExpressions: Seq[Expression])
}
}
+private[apache] case class ST_OffsetCurve(inputExpressions: Seq[Expression])
+ extends InferredExpression(
+ inferrableFunction2(Functions.offsetCurve),
+ inferrableFunction3(Functions.offsetCurve)) {
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
+
private[apache] case class ST_IsPolygonCW(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.isPolygonCW _) {
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
diff --git
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 07f2e78d3b..f712bf30ff 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
@@ -566,6 +566,15 @@ object st_functions {
def ST_OrientedEnvelope(geometry: String): Column =
wrapExpression[ST_OrientedEnvelope](geometry)
+ def ST_OffsetCurve(geometry: Column, distance: Column): Column =
+ wrapExpression[ST_OffsetCurve](geometry, distance)
+ def ST_OffsetCurve(geometry: String, distance: Double): Column =
+ wrapExpression[ST_OffsetCurve](geometry, distance)
+ def ST_OffsetCurve(geometry: Column, distance: Column, quadrantSegments:
Column): Column =
+ wrapExpression[ST_OffsetCurve](geometry, distance, quadrantSegments)
+ def ST_OffsetCurve(geometry: String, distance: Double, quadrantSegments:
Int): Column =
+ wrapExpression[ST_OffsetCurve](geometry, distance, quadrantSegments)
+
def ST_IsPolygonCCW(geometry: Column): Column =
wrapExpression[ST_IsPolygonCCW](geometry)
def ST_IsPolygonCCW(geometry: String): Column =
wrapExpression[ST_IsPolygonCCW](geometry)
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
index b9e5b7b29e..f66794dd53 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
@@ -75,6 +75,7 @@ class PreserveSRIDSuite extends TestBaseScala with
TableDrivenPropertyChecks {
("ST_SetPoint(geom3, 1, ST_Point(0.5, 0.5))", 1000),
("ST_ClosestPoint(geom1, geom2)", 1000),
("ST_ShortestLine(geom1, geom2)", 1000),
+ ("ST_OffsetCurve(geom3, 1.0)", 1000),
("ST_FlipCoordinates(geom1)", 1000),
("ST_SubDivide(geom4, 5)", 1000),
("ST_Segmentize(geom4, 0.1)", 1000),
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 f10f36e38a..c14608bc31 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
@@ -575,6 +575,23 @@ class dataFrameAPITestScala extends TestBaseScala {
assertEquals(expected, actual)
}
+ it("Passed ST_OffsetCurve") {
+ val lineDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING(0 0, 10
0)') AS geom")
+ val df = lineDf.select(ST_OffsetCurve("geom", 5.0))
+ val actual = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
+ assertEquals("LINESTRING (0 5, 10 5)", actual)
+ }
+
+ it("Passed ST_OffsetCurve with quadrantSegments") {
+ val lineDf =
+ sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING(0 0, 10 0, 10
10)') AS geom")
+ val defaultDf = lineDf.select(ST_NPoints(ST_OffsetCurve("geom", -3.0)))
+ val customDf = lineDf.select(ST_NPoints(ST_OffsetCurve("geom", -3.0,
16)))
+ val defaultPts = defaultDf.take(1)(0).getInt(0)
+ val customPts = customDf.take(1)(0).getInt(0)
+ assertTrue(customPts > defaultPts)
+ }
+
it("Passed ST_BestSRID") {
val pointDf = sparkSession.sql("SELECT ST_Point(-177, -60) AS geom")
val df = pointDf.select(ST_BestSRID("geom").as("geom"))
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 8d311bae90..9629cd406c 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
@@ -3047,6 +3047,29 @@ class functionTestScala
assert(result == null)
}
+ it("should pass ST_OffsetCurve") {
+ // Positive distance offsets to the left
+ var df = sparkSession.sql(
+ "SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10
0)'), 5.0))")
+ var actual = df.take(1)(0).get(0).asInstanceOf[String]
+ assertEquals("LINESTRING (0 5, 10 5)", actual)
+
+ // Negative distance offsets to the right
+ df = sparkSession.sql(
+ "SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10
0)'), -5.0))")
+ actual = df.take(1)(0).get(0).asInstanceOf[String]
+ assertEquals("LINESTRING (0 -5, 10 -5)", actual)
+
+ // With quadrantSegments parameter on a line with a corner
+ val defaultDf = sparkSession.sql(
+ "SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0,
10 10)'), -3.0))")
+ val defaultPts = defaultDf.take(1)(0).get(0).asInstanceOf[Int]
+ val customDf = sparkSession.sql(
+ "SELECT ST_NPoints(ST_OffsetCurve(ST_GeomFromWKT('LINESTRING(0 0, 10 0,
10 10)'), -3.0, 16))")
+ val customPts = customDf.take(1)(0).get(0).asInstanceOf[Int]
+ assertTrue(customPts > defaultPts)
+ }
+
it("Should pass ST_AreaSpheroid") {
val geomTestCases = Map(
("'POINT (-0.56 51.3168)'") -> "0.0",