This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch SEDONA-604 in repository https://gitbox.apache.org/repos/asf/sedona.git
commit 76bea270215006fae0ac1815f64b8c5cc26c2682 Author: Furqaan Khan <[email protected]> AuthorDate: Fri Jun 7 02:44:36 2024 -0400 [TASK-92] Add ST_AddMeasure (#207) * feat: add implementation * feat: port to flink and spark * feat: add another declaration to spark DataframeAPI --- .../java/org/apache/sedona/common/Functions.java | 4 ++ .../org/apache/sedona/common/utils/GeomUtils.java | 70 ++++++++++++++++++++++ .../org/apache/sedona/common/FunctionsTest.java | 18 ++++++ docs/api/flink/Function.md | 22 +++++++ docs/api/sql/Function.md | 22 +++++++ .../main/java/org/apache/sedona/flink/Catalog.java | 1 + .../apache/sedona/flink/expressions/Functions.java | 9 +++ .../java/org/apache/sedona/flink/FunctionTest.java | 15 +++++ python/sedona/sql/st_functions.py | 14 +++++ python/tests/sql/test_dataframe_api.py | 3 + python/tests/sql/test_function.py | 10 ++++ .../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 + .../sql/sedona_sql/expressions/Functions.scala | 8 +++ .../sql/sedona_sql/expressions/st_functions.scala | 4 ++ .../apache/sedona/sql/dataFrameAPITestScala.scala | 16 +++++ .../org/apache/sedona/sql/functionTestScala.scala | 11 ++++ 16 files changed, 228 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 161d862db..5688d937d 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -1060,6 +1060,10 @@ public class Functions { return isExteriorRingCCW && isInteriorRingCCW; } + public static Geometry addMeasure(Geometry geom, double measure_start, double measure_end) { + return GeomUtils.addMeasure(geom, measure_start, measure_end); + } + public static double maxDistance(Geometry geom1, Geometry geom2) { return longestLine(geom1, geom2).getLength(); } 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 fc36e79e5..a51f6ccc6 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 @@ -530,6 +530,76 @@ public class GeomUtils { return hausdorffDistanceObj.distance(); } + public static Geometry addMeasure(Geometry geom, double measure_start, double measure_end) { + if (!(geom instanceof LineString) && !(geom instanceof MultiLineString)) { + throw new IllegalArgumentException("Geometry must be a LineString or MultiLineString."); + } + + if (geom instanceof LineString) { + return addMeasure((LineString) geom, measure_start, measure_end); + } else { // MultiLineString + return addMeasure((MultiLineString) geom, measure_start, measure_end); + } + } + + private static Geometry addMeasure(LineString geom, double measureStart, double measureEnd) { + Coordinate[] coordinates = geom.getCoordinates(); + double totalLength = geom.getLength(); + CoordinateList newCoordinates = new CoordinateList(); + + Coordinate c1 = coordinates[0]; + double measure, + measureRange = measureEnd - measureStart; + int numCoordinates = coordinates.length; + + for (int i = 0; i < numCoordinates; i++) { + Coordinate c2 = coordinates[i]; + double length = c1.distance(c2); + + if (totalLength > 0.0) { + measure = measureStart + measureRange * (length / totalLength); + } else if (totalLength == 0.0 && numCoordinates > 1) { // valid zero length LineStrings + measure = measureStart + measureRange * i / (numCoordinates - 1); + } else { + measure = 0.0; + } + + CoordinateXYZM newCoordinate = new CoordinateXYZM(c2.getX(), c2.getY(), c2.getZ(), measure); + newCoordinates.add(newCoordinate); + } + + return geom.getFactory().createLineString(newCoordinates.toCoordinateArray()); + } + + private static Geometry addMeasure(MultiLineString geom, double measureStart, double measureEnd) { + double totalLength = 0, + measureRange = measureEnd - measureStart; + for (int i = 0; i < geom.getNumGeometries(); i++) { + LineString linestring = (LineString) geom.getGeometryN(i); + if (linestring.getCoordinates().length > 1) + totalLength += linestring.getLength(); + } + LineString[] newLineStrings = new LineString[geom.getNumGeometries()]; + double length= 0; + for (int i = 0; i < geom.getNumGeometries(); i++) { + double subMeasureStart, + subMeasureEnd, + subLength = 0; + LineString lineString = (LineString) geom.getGeometryN(i); + if (lineString.getCoordinates().length > 1) + subLength = lineString.getCoordinates().length; + + subMeasureStart = measureStart + measureRange * length / totalLength; + subMeasureEnd = measureStart + measureRange * (length + subLength) / totalLength; + + newLineStrings[i] = (LineString) addMeasure(lineString, subMeasureStart, subMeasureEnd); + + length += subLength; + } + + return geom.getFactory().createMultiLineString(newLineStrings); + } + public static Boolean isMeasuredGeometry(Geometry geom) { Coordinate coordinate = geom.getCoordinate(); return !Double.isNaN(coordinate.getM()); 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 8915c2cab..5b0d60acd 100644 --- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -2660,6 +2660,24 @@ public class FunctionsTest extends TestBase { } + @Test + public void testAddMeasure() throws ParseException { + Geometry geom = Constructors.geomFromEWKT("LINESTRING (1 1, 2 2, 2 2, 3 3)"); + String actual = Functions.asWKT(Functions.addMeasure(geom, 1, 70)); + String expected = "LINESTRING M(1 1 1, 2 2 35.5, 2 2 35.5, 3 3 70)"; + assertEquals(expected, actual); + + actual = Functions.asWKT(Functions.addMeasure(geom, 1, 2)); + expected = "LINESTRING M(1 1 1, 2 2 1.5, 2 2 1.5, 3 3 2)"; + assertEquals(expected, actual); + + geom = Constructors.geomFromEWKT("MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))"); + actual = Functions.asWKT(Functions.addMeasure(geom, 1, 70)); + expected = "MULTILINESTRING M((1 0 1, 2 0 12.5, 4 0 35.5), (1 0 35.5, 2 0 47, 4 0 70))"; + assertEquals(expected, actual); + + } + @Test public void voronoiPolygons() { MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(0, 0, 2, 2)); diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index 660658491..c63a37ab5 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -53,6 +53,28 @@ Output: 1.7320508075688772 ``` +## ST_AddMeasure + +Introduction: Computes a new geometry with measure (M) values linearly interpolated between start and end points. For geometries lacking M dimensions, M values are added. Existing M values are overwritten by the new values. Applies only to LineString and MultiLineString inputs. + +Format: `ST_AddMeasure(geom: Geometry, measureStart: Double, measureEnd: Double)` + +Since: `vTBD` + +SQL Example: + +```sql +SELECT ST_AsText(ST_AddMeasure( + ST_GeomFromWKT('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)') +)) +``` + +Output: + +``` +LINESTRING M(0 0 10, 1 0 16, 2 0 22, 3 0 28, 4 0 34, 5 0 40) +``` + ## ST_AddPoint Introduction: Return Linestring with additional point at the given index, if position is not available the point will be added at the end of line. diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index ff0a58fbe..0a6679d13 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -53,6 +53,28 @@ Output: 1.7320508075688772 ``` +## ST_AddMeasure + +Introduction: Computes a new geometry with measure (M) values linearly interpolated between start and end points. For geometries lacking M dimensions, M values are added. Existing M values are overwritten by the new values. Applies only to LineString and MultiLineString inputs. + +Format: `ST_AddMeasure(geom: Geometry, measureStart: Double, measureEnd: Double)` + +Since: `vTBD` + +SQL Example: + +```sql +SELECT ST_AsText(ST_AddMeasure( + ST_GeomFromWKT('LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)') +)) +``` + +Output: + +``` +LINESTRING M(0 0 10, 1 0 16, 2 0 22, 3 0 28, 4 0 34, 5 0 40) +``` + ## ST_AddPoint Introduction: RETURN Linestring with additional point at the given index, if position is not available the point will be added at the end of line. 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 91eebd9c4..273c0c16e 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -131,6 +131,7 @@ public class Catalog { new Functions.ST_IsSimple(), new Functions.ST_IsValid(), new Functions.ST_Normalize(), + new Functions.ST_AddMeasure(), new Functions.ST_AddPoint(), new Functions.ST_RemovePoint(), new Functions.ST_SetPoint(), 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 79d1a8b26..4ab0faac6 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 @@ -722,6 +722,15 @@ public class Functions { } } + public static class ST_AddMeasure 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 o1, + @DataTypeHint(value = "Double") Double measureStart, @DataTypeHint(value = "Double") Double measureEnd) { + Geometry geom = (Geometry) o1; + return org.apache.sedona.common.Functions.addMeasure(geom, measureStart, measureEnd); + } + } + public static class ST_AddPoint 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 o1, 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 f128b31d2..01be40664 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -954,6 +954,21 @@ public class FunctionTest extends TestBase{ assertEquals("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))", result.toString()); } + @Test + public void testAddMeasure() { + Table baseTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING (1 1, 2 2, 2 2, 3 3)') as line, " + + "ST_GeomFromWKT('MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))') as mline"); + String actual = (String) first(baseTable.select(call(Functions.ST_AddMeasure.class.getSimpleName(), $("line"), + 1.0, 70.0)).as("geom").select(call(Functions.ST_AsText.class.getSimpleName(), $("geom")))).getField(0); + String expected = "LINESTRING M(1 1 1, 2 2 35.5, 2 2 35.5, 3 3 70)"; + assertEquals(expected, actual); + + actual = (String) first(baseTable.select(call(Functions.ST_AddMeasure.class.getSimpleName(), $("mline"), + 10.0, 70.0)).as("geom").select(call(Functions.ST_AsText.class.getSimpleName(), $("geom")))).getField(0); + expected = "MULTILINESTRING M((1 0 10, 2 0 20, 4 0 40), (1 0 40, 2 0 50, 4 0 70))"; + assertEquals(expected, actual); + } + @Test public void testAddPoint() { Table pointTable = tableEnv.sqlQuery("SELECT ST_AddPoint(ST_GeomFromWKT('LINESTRING (0 0, 1 1)'), ST_GeomFromWKT('POINT (2 2)'))"); diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index 1a3dd9809..050b52a15 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -57,6 +57,20 @@ def ST_3DDistance(a: ColumnOrName, b: ColumnOrName) -> Column: """ return _call_st_function("ST_3DDistance", (a, b)) +@validate_argument_types +def ST_AddMeasure(geom: ColumnOrName, measureStart: Union[ColumnOrName, float], measureEnd: Union[ColumnOrName, float]) -> Column: + """Interpolate measure values with the provided start and end points and return the result geometry. + + :param geom: Geometry column to use in the calculation. + :type geom: ColumnOrName + :param measureStart: Start point for the measure. + :type measureStart: ColumnOrName + :param measureEnd: End point for the measure. + :type measureEnd: ColumnOrName + :return: Result geometry column. + :rtype: Column + """ + return _call_st_function("ST_AddMeasure", (geom, measureStart, measureEnd)) @validate_argument_types def ST_AddPoint(line_string: ColumnOrName, point: ColumnOrName, index: Optional[Union[ColumnOrName, int]] = None) -> Column: diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index 2e201e538..687cba0b7 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -78,6 +78,7 @@ test_configurations = [ (stf.ST_3DDistance, ("a", "b"), "two_points", "", 5.0), (stf.ST_Affine, ("geom", 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), "square_geom", "", "POLYGON ((2 3, 4 5, 5 6, 3 4, 2 3))"), (stf.ST_Affine, ("geom", 1.0, 2.0, 1.0, 2.0, 1.0, 2.0,), "square_geom", "", "POLYGON ((2 3, 4 5, 5 6, 3 4, 2 3))"), + (stf.ST_AddMeasure, ("line", 10.0, 40.0), "linestring_geom", "ST_AsText(geom)", "LINESTRING M(0 0 10, 1 0 16, 2 0 22, 3 0 28, 4 0 34, 5 0 40)"), (stf.ST_AddPoint, ("line", lambda: f.expr("ST_Point(1.0, 1.0)")), "linestring_geom", "", "LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 1 1)"), (stf.ST_AddPoint, ("line", lambda: f.expr("ST_Point(1.0, 1.0)"), 1), "linestring_geom", "", "LINESTRING (0 0, 1 1, 1 0, 2 0, 3 0, 4 0, 5 0)"), (stf.ST_Angle, ("p1", "p2", "p3", "p4", ), "four_points", "", 0.4048917862850834), @@ -287,6 +288,8 @@ wrong_type_configurations = [ # functions (stf.ST_3DDistance, (None, "")), (stf.ST_3DDistance, ("", None)), + (stf.ST_AddMeasure, (None, None, None)), + (stf.ST_AddMeasure, ("", None, "")), (stf.ST_AddPoint, (None, "")), (stf.ST_AddPoint, ("", None)), (stf.ST_Area, (None,)), diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py index fcedfb794..c179f666d 100644 --- a/python/tests/sql/test_function.py +++ b/python/tests/sql/test_function.py @@ -849,6 +849,16 @@ class TestPredicateJoin(TestBase): collected_interior_rings = [[*row] for row in number_of_interior_rings.filter("num is not null").collect()] assert (collected_interior_rings == [[2, 0], [11, 1]]) + def test_st_add_measure(self): + baseDf = self.spark.sql("SELECT ST_GeomFromWKT('LINESTRING (1 1, 2 2, 2 2, 3 3)') as line, ST_GeomFromWKT('MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))') as mline") + actual = baseDf.selectExpr("ST_AsText(ST_AddMeasure(line, 1, 70))").first()[0] + expected = "LINESTRING M(1 1 1, 2 2 35.5, 2 2 35.5, 3 3 70)" + assert expected == actual + + actual = baseDf.selectExpr("ST_AsText(ST_AddMeasure(mline, 10, 70))").first()[0] + expected = "MULTILINESTRING M((1 0 10, 2 0 20, 4 0 40), (1 0 40, 2 0 50, 4 0 70))" + assert expected == actual + def test_st_add_point(self): geometry = [ ("Point(21 52)", "Point(21 52)"), 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 6d16bc9be..d935f9bba 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 @@ -145,6 +145,7 @@ object Catalog { function[ST_IsCollection](), function[ST_NumInteriorRings](), function[ST_NumInteriorRing](), + function[ST_AddMeasure](), function[ST_AddPoint](-1), function[ST_RemovePoint](-1), function[ST_SetPoint](), 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 ded1e31f7..bf0eedb8e 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 @@ -713,6 +713,14 @@ case class ST_NumInteriorRing(inputExpressions: Seq[Expression]) } } +case class ST_AddMeasure(inputExpressions: Seq[Expression]) + extends InferredExpression(inferrableFunction3(Functions.addMeasure)) { + + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} + case class ST_AddPoint(inputExpressions: Seq[Expression]) extends InferredExpression(inferrableFunction3(Functions.addPoint)) { 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 9bb23391d..e8df1fa2e 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 @@ -29,6 +29,10 @@ object st_functions extends DataFrameAPI { def ST_3DDistance(a: Column, b: Column): Column = wrapExpression[ST_3DDistance](a, b) def ST_3DDistance(a: String, b: String): Column = wrapExpression[ST_3DDistance](a, b) + def ST_AddMeasure(geom: Column, measureStart: Column, measureEnd: Column): Column = wrapExpression[ST_AddMeasure](geom, measureStart, measureEnd) + def ST_AddMeasure(geom: String, measureStart: Double, measureEnd: Double): Column = wrapExpression[ST_AddMeasure](geom, measureStart, measureEnd) + def ST_AddMeasure(geom: String, measureStart: String, measureEnd: String): Column = wrapExpression[ST_AddMeasure](geom, measureStart, measureEnd) + def ST_AddPoint(lineString: Column, point: Column): Column = wrapExpression[ST_AddPoint](lineString, point, -1) def ST_AddPoint(lineString: String, point: String): Column = wrapExpression[ST_AddPoint](lineString, point, -1) def ST_AddPoint(lineString: Column, point: Column, index: Column): Column = wrapExpression[ST_AddPoint](lineString, point, index) 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 4a1bf9f98..5ae5dcb82 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 @@ -919,6 +919,22 @@ class dataFrameAPITestScala extends TestBaseScala { assert(actualResult == expectedResult) } + it("Passed ST_AddMeasure") { + var baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (1 1, 2 2, 2 2, 3 3)') as line, ST_GeomFromWKT('MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))') as mline") + var actual = baseDf.select(ST_AsText(ST_AddMeasure("line", 1, 70))).first().get(0) + var expected = "LINESTRING M(1 1 1, 2 2 35.5, 2 2 35.5, 3 3 70)" + assertEquals(expected, actual) + + actual = baseDf.select(ST_AsText(ST_AddMeasure("mline", 10, 70))).first().get(0) + expected = "MULTILINESTRING M((1 0 10, 2 0 20, 4 0 40), (1 0 40, 2 0 50, 4 0 70))" + assertEquals(expected, actual) + + baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))') as mline, 10.0 as measureRangeStart, 70.0 as measureRangeEnd") + actual = baseDf.select(ST_AsText(ST_AddMeasure("mline", "measureRangeEnd", "measureRangeStart"))).first().get(0) + expected = "MULTILINESTRING M((1 0 70, 2 0 60, 4 0 40), (1 0 40, 2 0 30, 4 0 10))" + assertEquals(expected, actual) + } + it("Passed ST_AddPoint without index") { val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 0, 1 0)') AS line, ST_Point(1.0, 1.0) AS point") val df = baseDf.select(ST_AddPoint("line", "point")) 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 1b81fb909..a5a073c56 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 @@ -1338,6 +1338,17 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample .collect().toList should contain theSameElementsAs List((2, 0), (11, 1)) } + it("Should pass ST_AddMeasure") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (1 1, 2 2, 2 2, 3 3)') as line, ST_GeomFromWKT('MULTILINESTRING M((1 0 4, 2 0 4, 4 0 4),(1 0 4, 2 0 4, 4 0 4))') as mline") + var actual = baseDf.selectExpr("ST_AsText(ST_AddMeasure(line, 1, 70))").first().get(0) + var expected = "LINESTRING M(1 1 1, 2 2 35.5, 2 2 35.5, 3 3 70)" + assertEquals(expected, actual) + + actual = baseDf.selectExpr("ST_AsText(ST_AddMeasure(mline, 10, 70))").first().get(0) + expected = "MULTILINESTRING M((1 0 10, 2 0 20, 4 0 40), (1 0 40, 2 0 50, 4 0 70))" + assertEquals(expected, actual) + } + it("Should pass ST_AddPoint") { Given("Geometry df") val geometryDf = Seq(
