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 53e9ec787 [SEDONA-602] Add ST_LocateAlong (#1471)
53e9ec787 is described below

commit 53e9ec787f49a04b1cd2acf278121b56422b6b55
Author: Jia Yu <[email protected]>
AuthorDate: Fri Jun 7 15:19:30 2024 -0700

    [SEDONA-602] Add ST_LocateAlong (#1471)
    
    * Merge pull request #204 from wherobots/st-locate-along
    
    [TASK-187] Add ST_LocateAlong
    
    * Merge pull request #208 from wherobots/refactor-st-locate-along
    
    [TASK-187] Refactor ST_LocateAlong implementation to remove instanceof 
operator
    
    * Update versions
    
    * Fix
    
    ---------
    
    Co-authored-by: Feng Zhang <[email protected]>
---
 .../java/org/apache/sedona/common/Functions.java   |   8 ++
 .../common/utils/GeometryLocateAlongProcessor.java | 122 +++++++++++++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    |  40 +++++++
 docs/api/flink/Function.md                         |  30 +++++
 docs/api/sql/Function.md                           |  30 +++++
 .../main/java/org/apache/sedona/flink/Catalog.java |   1 +
 .../apache/sedona/flink/expressions/Functions.java |  17 +++
 .../java/org/apache/sedona/flink/FunctionTest.java |  14 +++
 python/sedona/sql/st_functions.py                  |  16 +++
 python/tests/sql/test_dataframe_api.py             |   5 +
 python/tests/sql/test_function.py                  |  10 ++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  |   1 -
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |   1 +
 .../sql/sedona_sql/expressions/Functions.scala     |   8 ++
 .../sql/sedona_sql/expressions/st_functions.scala  |   5 +
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |  11 ++
 .../org/apache/sedona/sql/functionTestScala.scala  |  11 ++
 17 files changed, 329 insertions(+), 1 deletion(-)

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 e9a37958e..3625fabc4 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1006,6 +1006,14 @@ public class Functions {
         return indexedLine.indexOf(point.getCoordinate()) / length;
     }
 
+    public static Geometry locateAlong(Geometry linear, double measure, double 
offset) {
+        return GeometryLocateAlongProcessor.processGeometry(linear, measure, 
offset);
+    }
+
+    public static Geometry locateAlong(Geometry linear, double measure) {
+        return locateAlong(linear, measure, 0);
+    }
+
     /**
      * Forces a Polygon/MultiPolygon to use counter-clockwise orientation for 
the exterior ring and a clockwise for the interior ring(s).
      * @param geom
diff --git 
a/common/src/main/java/org/apache/sedona/common/utils/GeometryLocateAlongProcessor.java
 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryLocateAlongProcessor.java
new file mode 100644
index 000000000..49bbdc4b4
--- /dev/null
+++ 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryLocateAlongProcessor.java
@@ -0,0 +1,122 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sedona.common.utils;
+
+import org.locationtech.jts.geom.*;
+
+import java.util.*;
+import java.util.function.BiFunction;
+
+public class GeometryLocateAlongProcessor {
+
+    private final Map<Class<?>, BiFunction<Geometry, double[], Geometry>> 
geometryFunctions = new HashMap<>();
+
+    public GeometryLocateAlongProcessor() {
+        geometryFunctions.put(Point.class, (geometry, params) -> 
locateAlongPoint((Point) geometry, params[0], params[1]));
+        geometryFunctions.put(MultiPoint.class, (geometry, params) -> 
locateAlongMultiPoint((MultiPoint) geometry, params[0], params[1]));
+        geometryFunctions.put(LineString.class, (geometry, params) -> 
locateAlongLineString((LineString) geometry, params[0], params[1]));
+        geometryFunctions.put(MultiLineString.class, (geometry, params) -> 
locateAlongMultiLineString((MultiLineString) geometry, params[0], params[1]));
+    }
+
+    public static Geometry processGeometry(Geometry geometry, double measure, 
double offset) {
+        GeometryLocateAlongProcessor processor = new 
GeometryLocateAlongProcessor();
+        BiFunction<Geometry, double[], Geometry> function = 
processor.geometryFunctions.get(geometry.getClass());
+        if (function != null) {
+            return function.apply(geometry, new double[]{measure, offset});
+        }
+        throw new IllegalArgumentException(String.format("%s geometry type not 
supported, supported types are: (Multi)Point and (Multi)LineString.", 
geometry.getGeometryType()));
+    }
+
+    private Geometry locateAlongPoint(Point point, double measure, double 
offset) {
+        if (measure == point.getCoordinate().getM()) {
+            return point;
+        }
+        return null;
+    }
+
+    private Geometry locateAlongMultiPoint(MultiPoint multiPoint, double 
measure, double offset) {
+        Point[] points = new Point[multiPoint.getNumGeometries()];
+        for (int i = 0; i < multiPoint.getNumGeometries(); i++) {
+            points[i] = (Point) locateAlongPoint((Point) 
multiPoint.getGeometryN(i), measure, offset);
+        }
+        return 
multiPoint.getFactory().createMultiPoint(Arrays.stream(points).filter(Objects::nonNull).toArray(Point[]::new));
+    }
+
+    private Geometry locateAlongLineString(LineString lineString, double 
measure, double offset) {
+        Coordinate[] coordinates = lineString.getCoordinates();
+        CoordinateList coordinateList = new CoordinateList();
+
+        for (int i = 1; i < coordinates.length; i++) {
+            Coordinate coordinate1 = coordinates[i - 1];
+            Coordinate coordinate2 = coordinates[i];
+            CoordinateXYZM newCoordinate = new CoordinateXYZM();
+            double position;
+
+            double measure1 = coordinate1.getM(),
+                    measure2 = coordinate2.getM();
+
+            if ((measure < Math.min(measure1, measure2)) || (measure > 
Math.max(measure1, measure2))) {
+                continue;
+            }
+
+            if (measure1 == measure2) {
+                // If the measures are equal then there is no valid 
interpolation range
+                if (coordinate1.equals(coordinate2)) {
+                    newCoordinate.setX(coordinate1.getX());
+                    newCoordinate.setY(coordinate1.getY());
+                    newCoordinate.setZ(coordinate1.getZ());
+                    newCoordinate.setM(coordinate1.getM());
+                    coordinateList.add(newCoordinate, false);
+                    continue;
+                }
+                // the point will be in the midpoint of coordinate1 and 
coordinate2 as measure1 and measure2 are same
+                position = 0.5;
+            } else {
+                // calculate the interpolation factor / position
+                position = (measure - measure1) / (measure2 - measure1);
+            }
+
+            // apply linear interpolation to find the point along the line
+            newCoordinate.setX(coordinate1.x + (coordinate2.x - coordinate1.x) 
* position);
+            newCoordinate.setY(coordinate1.y + (coordinate2.y - coordinate1.y) 
* position);
+            newCoordinate.setZ(coordinate1.z + (coordinate2.z - coordinate1.z) 
* position);
+            newCoordinate.setM(measure);
+
+            if (offset != 0D) {
+                // calculate the angle of the line segment
+                double theta = Math.atan2(coordinate2.y - coordinate1.y, 
coordinate2.x - coordinate1.x);
+                // shift the coordinate left or right by the offset
+                // if the offset is positive then shift to left
+                // else the offset is negative then shift to right
+                newCoordinate.setX(newCoordinate.x - Math.sin(theta) * offset);
+                newCoordinate.setY(newCoordinate.y + Math.cos(theta) * offset);
+            }
+            coordinateList.add(newCoordinate, false);
+        }
+        return 
lineString.getFactory().createMultiPointFromCoords(Arrays.stream(coordinateList.toCoordinateArray()).filter(Objects::nonNull).toArray(Coordinate[]::new));
+    }
+
+    private Geometry locateAlongMultiLineString(MultiLineString 
multiLineString, double measure, double offset) {
+        // iterating through LineStrings in MultiLineString object
+        List<Point> points = new ArrayList<>();
+        for (int i = 0; i < multiLineString.getNumGeometries(); i++) {
+            MultiPoint mPoint = (MultiPoint) 
locateAlongLineString((LineString) multiLineString.getGeometryN(i), measure, 
offset);
+            for (int j = 0; j < mPoint.getNumGeometries(); j++) {
+                points.add((Point) mPoint.getGeometryN(j));
+            }
+        }
+
+        return 
multiLineString.getFactory().createMultiPoint(points.toArray(new Point[0]));
+    }
+}
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 8d2553350..cdf1dea02 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -2821,6 +2821,46 @@ public class FunctionsTest extends TestBase {
         assertEquals(expectedResult3, actual3, FP_TOLERANCE);
     }
 
+    @Test
+    public void locateAlong() throws ParseException {
+        Geometry geom = Constructors.geomFromEWKT("MULTIPOINT M(1 2 3, 3 4 3, 
9 4 3, 3 2 1, 1 2 3, 5 4 2)");
+        String actual = Functions.asWKT(Functions.locateAlong(geom, 3, 0));
+        String expected = "MULTIPOINT M((1 2 3), (3 4 3), (9 4 3), (1 2 3))";
+        assertEquals(expected, actual);
+
+        // offset doesn't affect Point or MultiPoint
+        actual = Functions.asWKT(Functions.locateAlong(geom, 3, 4));
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromEWKT("LINESTRING M(1 2 3, 3 4 3, 5 4 2, 9 
4 3)");
+        actual = Functions.asWKT(Functions.locateAlong(geom, 3, 0));
+        expected = "MULTIPOINT M((2 3 3), (3 4 3), (9 4 3))";
+        assertEquals(expected, actual);
+
+        actual = Functions.asWKT(Functions.locateAlong(geom, 3, 2));
+        expected = "MULTIPOINT M((0.5857864376269051 4.414213562373095 3), (3 
6 3), (9 6 3))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromEWKT("LINESTRING M(1 2 3, 3 4 3, 5 4 2, 5 
4 2, 9 4 3)");
+        actual = Functions.asWKT(Functions.locateAlong(geom, 2, 0));
+        expected = "MULTIPOINT M((5 4 2))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromEWKT("MULTILINESTRING M((1 2 3, 3 4 2, 9 4 
3),(1 2 3, 5 4 5))");
+        actual = Functions.asWKT(Functions.locateAlong(geom, 2, 0));
+        expected = "MULTIPOINT M((3 4 2))";
+        assertEquals(expected, actual);
+
+        actual = Functions.asWKT(Functions.locateAlong(geom, 2, -3));
+        expected = "MULTIPOINT M((5.121320343559642 1.8786796564403572 2), (3 
1 2))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromEWKT("POLYGON M((0 0 1, 1 1 1, 5 1 1, 5 0 
1, 1 0 1, 0 0 1))");
+        Geometry finalGeom = geom;
+        Exception e = assertThrows(IllegalArgumentException.class, () -> 
Functions.locateAlong(finalGeom, 1));
+        assertEquals("Polygon geometry type not supported, supported types 
are: (Multi)Point and (Multi)LineString.", e.getMessage());
+    }
+
     @Test
     public void isValidReason() {
         // Valid geometry
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 44acffffc..02068ca9d 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -2325,6 +2325,36 @@ Output:
 LINESTRING (69.28469348539744 94.28469348539744, 100 125, 111.70035626068274 
140.21046313888758)
 ```
 
+## ST_LocateAlong
+
+Introduction: This function computes Point or MultiPoint geometries 
representing locations along a measured input geometry (LineString or 
MultiLineString) corresponding to the provided measure value(s). Polygonal 
geometry inputs are not supported. The output points lie directly on the input 
line at the specified measure positions.
+
+Additionally, an optional `offset` parameter can shift the resulting points 
left or right from the input line. A positive offset displaces the points to 
the left side, while a negative value offsets them to the right side by the 
given distance.
+
+This allows identifying precise locations along a measured linear geometry 
based on supplied measure values, with the ability to offset the output points 
if needed.
+
+Format:
+
+`ST_LocateAlong(linear: Geometry, measure: Double, offset: Double)`
+
+`ST_LocateAlong(linear: Geometry, measure: Double)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_LocateAlong(
+        ST_GeomFromText('LINESTRING M (10 30 1, 50 50 1, 30 110 2, 70 90 2, 
180 140 3, 130 190 3)')
+)
+```
+
+Output:
+
+```
+MULTIPOINT M((30 110 2), (50 100 2), (70 90 2))
+```
+
 ## ST_LongestLine
 
 Introduction: Returns the LineString geometry representing the maximum 
distance between any two points from the input geometries.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index eb2485278..858412a20 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -2332,6 +2332,36 @@ Output:
 LINESTRING (69.28469348539744 94.28469348539744, 100 125, 111.70035626068274 
140.21046313888758)
 ```
 
+## ST_LocateAlong
+
+Introduction: This function computes Point or MultiPoint geometries 
representing locations along a measured input geometry (LineString or 
MultiLineString) corresponding to the provided measure value(s). Polygonal 
geometry inputs are not supported. The output points lie directly on the input 
line at the specified measure positions.
+
+Additionally, an optional `offset` parameter can shift the resulting points 
left or right from the input line. A positive offset displaces the points to 
the left side, while a negative value offsets them to the right side by the 
given distance.
+
+This allows identifying precise locations along a measured linear geometry 
based on supplied measure values, with the ability to offset the output points 
if needed.
+
+Format:
+
+`ST_LocateAlong(linear: Geometry, measure: Double, offset: Double)`
+
+`ST_LocateAlong(linear: Geometry, measure: Double)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_LocateAlong(
+        ST_GeomFromText('LINESTRING M (10 30 1, 50 50 1, 30 110 2, 70 90 2, 
180 140 3, 130 190 3)')
+)
+```
+
+Output:
+
+```
+MULTIPOINT M((30 110 2), (50 100 2), (70 90 2))
+```
+
 ## ST_LongestLine
 
 Introduction: Returns the LineString geometry representing the maximum 
distance between any two points from the input geometries.
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 e421669d6..3b9837732 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -87,6 +87,7 @@ public class Catalog {
                 new Functions.ST_LengthSpheroid(),
                 new Functions.ST_LineInterpolatePoint(),
                 new Functions.ST_LineLocatePoint(),
+                new Functions.ST_LocateAlong(),
                 new Functions.ST_LongestLine(),
                 new FunctionsGeoTools.ST_Transform(),
                 new Functions.ST_FlipCoordinates(),
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 4c254fcb1..9ab64beb4 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
@@ -354,6 +354,23 @@ public class Functions {
         }
     }
 
+    public static class ST_LocateAlong extends ScalarFunction {
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint(value = "Double") Double measure,
+                             @DataTypeHint(value = "Double") Double offset) {
+            Geometry linear = (Geometry) o;
+            return org.apache.sedona.common.Functions.locateAlong(linear, 
measure, offset);
+        }
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint(value = "Double") Double measure) {
+            Geometry linear = (Geometry) o;
+            return org.apache.sedona.common.Functions.locateAlong(linear, 
measure);
+        }
+    }
+
     public static class ST_LongestLine 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 g1,
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 e237584d9..570186126 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -452,6 +452,20 @@ public class FunctionTest extends TestBase{
         assertEquals(expected, actual, 0.1);
     }
 
+    @Test
+    public void testLocateAlong() {
+        Table tbl = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('MULTILINESTRING 
M((1 2 3, 3 4 2, 9 4 3),(1 2 3, 5 4 5))') AS geom");
+        String actual = (String) 
first(tbl.select(call(Functions.ST_LocateAlong.class.getSimpleName(), 
$("geom"), 2)).as("geom")
+                .select(call(Functions.ST_AsEWKT.class.getSimpleName(), 
$("geom")))).getField(0);
+        String expected = "MULTIPOINT M((3 4 2))";
+        assertEquals(expected, actual);
+
+        actual = (String) 
first(tbl.select(call(Functions.ST_LocateAlong.class.getSimpleName(), 
$("geom"), 2, -3)).as("geom")
+                .select(call(Functions.ST_AsEWKT.class.getSimpleName(), 
$("geom")))).getField(0);
+        expected = "MULTIPOINT M((5.121320343559642 1.8786796564403572 2), (3 
1 2))";
+        assertEquals(expected, actual);
+    }
+
     @Test
     public void testLongestLine() {
         Table tbl = tableEnv.sqlQuery(
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index b1120a2ba..9ab2747d8 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -895,6 +895,22 @@ def ST_LineSubstring(line_string: ColumnOrName, 
start_fraction: ColumnOrNameOrNu
     """
     return _call_st_function("ST_LineSubstring", (line_string, start_fraction, 
end_fraction))
 
+@validate_argument_types
+def ST_LocateAlong(geom: ColumnOrName, measure: Union[ColumnOrName, float], 
offset: Optional[Union[ColumnOrName, float]] = None) -> Column:
+    """return locations along a measure geometry that have the given measure 
value.
+
+    :param geom:
+    :type geom: ColumnOrName
+    :param measure:
+    :type measure: Union[ColumnOrName, float]
+    :param offset:
+    :type offset: Union[ColumnOrNameOrNumber, float]
+    :return: Locations along a measure geometry that have the given measure 
value.
+    :rtype: Column
+    """
+    args = (geom, measure) if offset is None else (geom, measure, offset)
+    return _call_st_function("ST_LocateAlong", args)
+
 @validate_argument_types
 def ST_LongestLine(geom1: ColumnOrName, geom2: ColumnOrName) -> Column:
     """Compute the longest line between the two geometries
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index ac8f8f7fa..506b698f8 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -161,6 +161,8 @@ test_configurations = [
     (stf.ST_LineMerge, ("geom",), "multiline_geom", "", "LINESTRING (0 0, 1 0, 
1 1, 0 0)"),
     (stf.ST_LineSubstring, ("line", 0.5, 1.0), "linestring_geom", "", 
"LINESTRING (2.5 0, 3 0, 4 0, 5 0)"),
     (stf.ST_LongestLine, ("geom", "geom"), "geom_collection", "", "LINESTRING 
(0 0, 1 0)"),
+    (stf.ST_LocateAlong, ("line", 1.0), "4D_line", "ST_AsText(geom)", 
"MULTIPOINT ZM((1 1 1 1))"),
+    (stf.ST_LocateAlong, ("line", 1.0, 2.0), "4D_line", "ST_AsText(geom)", 
"MULTIPOINT ZM((-0.4142135623730949 2.414213562373095 1 1), (2.414213562373095 
-0.4142135623730949 1 1))"),
     (stf.ST_HasZ, ("a",), "two_points", "", True),
     (stf.ST_HasM, ("point",), "4D_point", "", True),
     (stf.ST_M, ("point",), "4D_point", "", 4.0),
@@ -368,6 +370,9 @@ wrong_type_configurations = [
     (stf.ST_LongestLine, (None, "")),
     (stf.ST_LongestLine, (None, None)),
     (stf.ST_LongestLine, ("", None)),
+    (stf.ST_LocateAlong, (None, "")),
+    (stf.ST_LocateAlong, (None, None)),
+    (stf.ST_LocateAlong, ("", None)),
     (stf.ST_HasZ, (None,)),
     (stf.ST_HasM, (None,)),
     (stf.ST_M, (None,)),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index de1b2e715..44de51214 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1435,6 +1435,16 @@ class TestPredicateJoin(TestBase):
                 "select 
ST_AsText(ST_LineFromMultiPoint(ST_GeomFromText({})))".format(input_geom))
             assert line_geometry.take(1)[0][0] == expected_geom
 
+    def test_st_locate_along(self):
+        baseDf = self.spark.sql("SELECT ST_GeomFromWKT('MULTILINESTRING M((1 2 
3, 3 4 2, 9 4 3),(1 2 3, 5 4 5))') as geom")
+        actual = baseDf.selectExpr("ST_AsText(ST_LocateAlong(geom, 
2))").take(1)[0][0]
+        expected = "MULTIPOINT M((3 4 2))"
+        assert expected == actual
+
+        actual = baseDf.selectExpr("ST_AsText(ST_LocateAlong(geom, 2, 
-3))").take(1)[0][0]
+        expected = "MULTIPOINT M((5.121320343559642 1.8786796564403572 2), (3 
1 2))"
+        assert expected == actual
+
     def test_st_longest_line(self):
         basedf = self.spark.sql("SELECT ST_GeomFromWKT('POLYGON ((40 180, 110 
160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 99 
77.5, 90 140, 60 140))') as geom")
         actual = basedf.selectExpr("ST_AsText(ST_LongestLine(geom, 
geom))").take(1)[0][0]
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 20d9f5389..b47fd7778 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
@@ -600,7 +600,6 @@ public class TestFunctionsV2
                 "LINESTRING(45.173118104 45.743370112,50 20,90 
80,112.975930502 49.365425998)"
         );
     }
-
     @Test
     public void test_ST_LongestLine() {
         registerUDFV2("ST_LongestLine", String.class, String.class);
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 7b0b4ebc9..d0229a146 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
@@ -160,6 +160,7 @@ object Catalog {
     function[ST_LineSubstring](),
     function[ST_LineInterpolatePoint](),
     function[ST_LineLocatePoint](),
+    function[ST_LocateAlong](),
     function[ST_LongestLine](),
     function[ST_SubDivideExplode](),
     function[ST_SubDivide](),
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 4af8325a3..e20e034ca 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
@@ -1240,6 +1240,14 @@ case class ST_LengthSpheroid(inputExpressions: 
Seq[Expression])
   }
 }
 
+case class ST_LocateAlong(inputExpressions: Seq[Expression])
+  extends InferredExpression(inferrableFunction3(Functions.locateAlong), 
inferrableFunction2(Functions.locateAlong)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class ST_LongestLine(inputExpressions: Seq[Expression])
   extends InferredExpression(Functions.longestLine _) {
 
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 a6449d521..093ca58cd 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
@@ -228,6 +228,11 @@ object st_functions extends DataFrameAPI {
   def ST_LongestLine(geom1: Column, geom2: Column): Column = 
wrapExpression[ST_LongestLine](geom1, geom2)
   def ST_LongestLine(geom1: String, geom2: String): Column = 
wrapExpression[ST_LongestLine](geom1, geom2)
 
+  def ST_LocateAlong(geom: Column, measure: Column, offset: Column): Column = 
wrapExpression[ST_LocateAlong](geom, measure, offset)
+  def ST_LocateAlong(geom: String, measure: Double, offset: Double): Column = 
wrapExpression[ST_LocateAlong](geom, measure, offset)
+  def ST_LocateAlong(geom: Column, measure: Column): Column = 
wrapExpression[ST_LocateAlong](geom, measure)
+  def ST_LocateAlong(geom: String, measure: Double): Column = 
wrapExpression[ST_LocateAlong](geom, measure)
+
   def ST_HasZ(geoms: Column): Column = wrapExpression[ST_HasZ](geoms)
   def ST_HasZ(geoms: String): Column = wrapExpression[ST_HasZ](geoms)
 
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 a11901447..74ef0b519 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
@@ -1015,6 +1015,17 @@ class dataFrameAPITestScala extends TestBaseScala {
       assert(actualResult == expectedResult)
     }
 
+    it("Passed ST_LocateAlong") {
+      val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTILINESTRING 
M((1 2 3, 3 4 2, 9 4 3),(1 2 3, 5 4 5))') AS geom")
+      var actual = baseDf.select(ST_AsText(ST_LocateAlong("geom", 
2))).first().get(0)
+      var expected = "MULTIPOINT M((3 4 2))"
+      assertEquals(expected, actual)
+
+      actual = baseDf.select(ST_AsText(ST_LocateAlong("geom", 2, 
-3))).first().get(0)
+      expected = "MULTIPOINT M((5.121320343559642 1.8786796564403572 2), (3 1 
2))"
+      assertEquals(expected, actual)
+    }
+
     it("Passed ST_LongestLine") {
       val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((40 180, 
110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 
140, 99 77.5, 90 140, 60 140))') as geom")
       val actual = baseDf.select(ST_LongestLine("geom", 
"geom")).first().get(0).asInstanceOf[Geometry].toText
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 2ff20d06f..6a04a46ad 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
@@ -1662,6 +1662,17 @@ class functionTestScala extends TestBaseScala with 
Matchers with GeometrySample
         "GEOMETRYCOLLECTION EMPTY")
   }
 
+  it("Should pass ST_LocateAlong") {
+    val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTILINESTRING M((1 
2 3, 3 4 2, 9 4 3),(1 2 3, 5 4 5))') AS geom")
+    var actual = baseDf.selectExpr("ST_AsText(ST_LocateAlong(geom, 
2))").first().get(0)
+    var expected = "MULTIPOINT M((3 4 2))"
+    assertEquals(expected, actual)
+
+    actual = baseDf.selectExpr("ST_AsText(ST_LocateAlong(geom, 2, 
-3))").first().get(0)
+    expected = "MULTIPOINT M((5.121320343559642 1.8786796564403572 2), (3 1 
2))"
+    assertEquals(expected, actual)
+  }
+
   it("Should pass ST_LongestLine") {
     val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((40 180, 
110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 
140, 99 77.5, 90 140, 60 140))') as geom")
     val actual = baseDf.selectExpr("ST_LongestLine(geom, 
geom)").first().get(0).asInstanceOf[Geometry].toText

Reply via email to