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 98f41a9f8 [SEDONA-587] Add ST_Force3DM, ST_Force4D (#1461)
98f41a9f8 is described below

commit 98f41a9f8011c8bb120d67386e570add8b29c4f6
Author: Jia Yu <[email protected]>
AuthorDate: Fri Jun 7 10:46:14 2024 -0700

    [SEDONA-587] Add ST_Force3DM, ST_Force4D (#1461)
    
    * [TASK-64] Add ST_Force3DM (#166)
    
    * feat: add ST_Force3DM
    
    * fix: lint and remove experimental snowflake porting
    
    * refactor: GeometryForce3DMTransformer to a more concise implementation
    
    * fix: docs typo and add test to verify M ordinate value
    
    * Update version
    
    * [TASK-271] Add ST_Force4D (#168)
    
    * feat: add ST_Force4D
    
    * remove print
    
    * Update version
    
    ---------
    
    Co-authored-by: Furqaan Khan <[email protected]>
---
 .../java/org/apache/sedona/common/Functions.java   |  22 ++++-
 .../common/utils/GeometryForce3DMTransformer.java  |  49 ++++++++++
 .../common/utils/GeometryForce4DTransformer.java   |  54 +++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    | 108 +++++++++++++++++++++
 docs/api/flink/Function.md                         |  86 ++++++++++++++++
 docs/api/sql/Function.md                           |  86 ++++++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |   2 +
 .../apache/sedona/flink/expressions/Functions.java |  32 ++++++
 .../java/org/apache/sedona/flink/FunctionTest.java |  24 +++++
 python/sedona/sql/st_functions.py                  |  25 +++++
 python/tests/sql/test_dataframe_api.py             |   4 +
 python/tests/sql/test_function.py                  |  11 +++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |   2 +
 .../sql/sedona_sql/expressions/Functions.scala     |  16 +++
 .../sql/sedona_sql/expressions/st_functions.scala  |  10 ++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |  22 +++++
 .../org/apache/sedona/sql/functionTestScala.scala  |  36 +++++++
 17 files changed, 584 insertions(+), 5 deletions(-)

diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java 
b/common/src/main/java/org/apache/sedona/common/Functions.java
index ac88ce04f..ad85bea36 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -21,11 +21,7 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.sedona.common.geometryObjects.Circle;
 import org.apache.sedona.common.sphere.Spheroid;
 import org.apache.sedona.common.subDivide.GeometrySubDivider;
-import org.apache.sedona.common.utils.GeomUtils;
-import org.apache.sedona.common.utils.GeometryGeoHashEncoder;
-import org.apache.sedona.common.utils.GeometrySplitter;
-import org.apache.sedona.common.utils.H3Utils;
-import org.apache.sedona.common.utils.S2Utils;
+import org.apache.sedona.common.utils.*;
 import org.locationtech.jts.algorithm.MinimumBoundingCircle;
 import org.locationtech.jts.algorithm.Orientation;
 import org.locationtech.jts.algorithm.hull.ConcaveHull;
@@ -1545,6 +1541,22 @@ public class Functions {
         return geometry.getNumPoints();
     }
 
+    public static Geometry force3DM(Geometry geom, double mValue) {
+        return GeometryForce3DMTransformer.transform(geom, mValue);
+    }
+
+    public static Geometry force3DM(Geometry geom) {
+        return force3DM(geom, 0.0);
+    }
+
+    public static Geometry force4D(Geometry geom, double zValue, double 
mValue) {
+        return GeometryForce4DTransformer.transform(geom, zValue, mValue);
+    }
+
+    public static Geometry force4D(Geometry geom) {
+        return force4D(geom, 0.0, 0.0);
+    }
+
     public static Geometry force3D(Geometry geometry, double zValue) {
         return GeomUtils.get3DGeom(geometry, zValue);
     }
diff --git 
a/common/src/main/java/org/apache/sedona/common/utils/GeometryForce3DMTransformer.java
 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce3DMTransformer.java
new file mode 100644
index 000000000..bd76ee76a
--- /dev/null
+++ 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce3DMTransformer.java
@@ -0,0 +1,49 @@
+/**
+ * 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.apache.sedona.common.Functions;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
+import org.locationtech.jts.geom.util.GeometryTransformer;
+
+import java.util.Arrays;
+
+public class GeometryForce3DMTransformer extends GeometryTransformer {
+
+    private final double mValue;
+
+    public GeometryForce3DMTransformer(double mValue) {
+        this.mValue = mValue;
+    }
+
+    @Override
+    protected CoordinateSequence transformCoordinates(CoordinateSequence 
coords, Geometry parent) {
+        CoordinateXYM[] newCoords = new CoordinateXYM[coords.size()];
+        for (int i = 0; i < coords.size(); i++) {
+            Coordinate coordinate = coords.getCoordinate(i);
+            newCoords[i] = new CoordinateXYM(coordinate.getX(), 
coordinate.getY(), mValue);
+        }
+
+        return createCoordinateSequence(newCoords);
+    }
+
+    public static Geometry transform(Geometry geometry, double mValue) {
+        if (geometry.getCoordinates().length == 0) return geometry;
+        if (Functions.hasM(geometry)) return geometry;
+
+        GeometryForce3DMTransformer transformer = new 
GeometryForce3DMTransformer(mValue);
+        return transformer.transform(geometry);
+    }
+}
diff --git 
a/common/src/main/java/org/apache/sedona/common/utils/GeometryForce4DTransformer.java
 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce4DTransformer.java
new file mode 100644
index 000000000..f61f8b0a2
--- /dev/null
+++ 
b/common/src/main/java/org/apache/sedona/common/utils/GeometryForce4DTransformer.java
@@ -0,0 +1,54 @@
+/**
+ * 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.apache.sedona.common.Functions;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.util.GeometryTransformer;
+
+public class GeometryForce4DTransformer extends GeometryTransformer {
+
+    private static boolean hasZ;
+    private static boolean hasM;
+    private final double mValue;
+    private final double zValue;
+
+    public GeometryForce4DTransformer(double zValue, double mValue) {
+        this.zValue = zValue;
+        this.mValue = mValue;
+    }
+
+    @Override
+    protected CoordinateSequence transformCoordinates(CoordinateSequence 
coords, Geometry parent) {
+        CoordinateXYZM[] newCoords = new CoordinateXYZM[coords.size()];
+        for (int i = 0; i < coords.size(); i++) {
+            Coordinate coordinate = coords.getCoordinate(i);
+            newCoords[i] = new CoordinateXYZM(coordinate.getX(), 
coordinate.getY(),
+                    hasZ ? coordinate.getZ() : zValue, hasM ? 
coordinate.getM() : mValue);
+        }
+
+        return createCoordinateSequence(newCoords);
+    }
+
+    public static Geometry transform(Geometry geometry, double zValue, double 
mValue) {
+        if (geometry.getCoordinates().length == 0) return geometry;
+        hasZ = Functions.hasZ(geometry);
+        hasM = Functions.hasM(geometry);
+        if (hasZ && hasM) return geometry;
+
+
+        GeometryForce4DTransformer transformer = new 
GeometryForce4DTransformer(zValue, mValue);
+        return transformer.transform(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 017f65e64..9ce130930 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -1333,6 +1333,114 @@ public class FunctionsTest extends TestBase {
         assertEquals(expectedWKT, actualWKT);
     }
 
+    @Test
+    public void testForce3DM() throws ParseException {
+        Geometry geom = Constructors.geomFromWKT("POINT (1 2)",0);
+        String actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        String expected = "POINT M(1 2 5)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTIPOINT ((1 2), (2 3))",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "MULTIPOINT M((1 2 5), (2 3 5))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("LINESTRING (1 2, 2 3, 3 4)",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "LINESTRING M(1 2 5, 2 3 5, 3 4 5)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTILINESTRING ((10 10, 20 20, 30 
30), (15 15, 25 25, 35 35))",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "MULTILINESTRING M((10 10 5, 20 20 5, 30 30 5), (15 15 5, 
25 25 5, 35 35 5))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("LINEARRING (30 10, 40 40, 20 40, 10 
20, 30 10)",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "LINEARRING M(30 10 5, 40 40 5, 20 40 5, 10 20 5, 30 10 5)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 
0), (4 4, 4 6, 6 6, 6 4, 4 4))",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "POLYGON M((0 0 5, 10 0 5, 10 10 5, 0 10 5, 0 0 5), (4 4 5, 
4 6 5, 6 6 5, 6 4 5, 4 4 5))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTIPOLYGON (((30 10, 40 40, 20 40, 
10 20, 30 10)), ((15 5, 10 20, 20 30, 15 5)))",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "MULTIPOLYGON M(((30 10 5, 40 40 5, 20 40 5, 10 20 5, 30 10 
5)), ((15 5 5, 10 20 5, 20 30 5, 15 5 5)))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("GEOMETRYCOLLECTION (POINT (10 10), 
LINESTRING (15 15, 25 25, 35 35), POLYGON ((30 10, 40 40, 20 40, 10 20, 30 
10)))",0);
+        actual = Functions.asWKT(Functions.force3DM(geom, 5));
+        expected = "GEOMETRYCOLLECTION M(POINT M(10 10 5), LINESTRING M(15 15 
5, 25 25 5, 35 35 5), POLYGON M((30 10 5, 40 40 5, 20 40 5, 10 20 5, 30 10 
5)))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("POLYGON M((0 0 3, 0 5 3, 5 0 3, 0 0 
3), (1 1 3, 3 1 3, 1 3 3, 1 1 3))", 0);
+        Geometry actualGeom = Functions.force3DM(geom, 10);
+        assertTrue(Predicates.equals(geom, actualGeom));
+    }
+
+    @Test
+    public void force4D() throws ParseException {
+        // testing all geom types
+        Geometry geom = Constructors.geomFromWKT("POINT (1 2)",0);
+        String actual = Functions.asWKT(Functions.force4D(geom,2, 5));
+        String expected = "POINT ZM(1 2 2 5)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTIPOINT ((1 2), (2 3))",0);
+        actual = Functions.asWKT(Functions.force4D(geom, 2, 5));
+        expected = "MULTIPOINT ZM((1 2 2 5), (2 3 2 5))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("LINESTRING (1 2, 2 3, 3 4)",0);
+        actual = Functions.asWKT(Functions.force4D(geom));
+        expected = "LINESTRING ZM(1 2 0 0, 2 3 0 0, 3 4 0 0)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTILINESTRING ((10 10, 20 20, 30 
30), (15 15, 25 25, 35 35))",0);
+        actual = Functions.asWKT(Functions.force4D(geom, 3, 5));
+        expected = "MULTILINESTRING ZM((10 10 3 5, 20 20 3 5, 30 30 3 5), (15 
15 3 5, 25 25 3 5, 35 35 3 5))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("LINEARRING (30 10, 40 40, 20 40, 10 
20, 30 10)",0);
+        actual = Functions.asWKT(Functions.force4D(geom, 5, 5));
+        expected = "LINEARRING ZM(30 10 5 5, 40 40 5 5, 20 40 5 5, 10 20 5 5, 
30 10 5 5)";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 
0), (4 4, 4 6, 6 6, 6 4, 4 4))",0);
+        actual = Functions.asWKT(Functions.force4D(geom));
+        expected = "POLYGON ZM((0 0 0 0, 10 0 0 0, 10 10 0 0, 0 10 0 0, 0 0 0 
0), (4 4 0 0, 4 6 0 0, 6 6 0 0, 6 4 0 0, 4 4 0 0))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTIPOLYGON (((30 10, 40 40, 20 40, 
10 20, 30 10)), ((15 5, 10 20, 20 30, 15 5)))",0);
+        actual = Functions.asWKT(Functions.force4D(geom, 2, 5));
+        expected = "MULTIPOLYGON ZM(((30 10 2 5, 40 40 2 5, 20 40 2 5, 10 20 2 
5, 30 10 2 5)), ((15 5 2 5, 10 20 2 5, 20 30 2 5, 15 5 2 5)))";
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("GEOMETRYCOLLECTION (POINT (10 10), 
LINESTRING (15 15, 25 25, 35 35), POLYGON ((30 10, 40 40, 20 40, 10 20, 30 
10)))",0);
+        actual = Functions.asWKT(Functions.force4D(geom, 2, 5));
+        expected = "GEOMETRYCOLLECTION ZM(POINT ZM(10 10 2 5), LINESTRING 
ZM(15 15 2 5, 25 25 2 5, 35 35 2 5), POLYGON ZM((30 10 2 5, 40 40 2 5, 20 40 2 
5, 10 20 2 5, 30 10 2 5)))";
+        assertEquals(expected, actual);
+
+        // return 4D input geom as is
+        geom = Constructors.geomFromWKT("POLYGON ZM ((30 10 5 1, 40 40 10 2, 
20 40 15 3, 10 20 20 4, 30 10 5 1))", 0);
+        Geometry actualGeom = Functions.force4D(geom, 10, 10);
+        assertTrue(Predicates.equals(geom, actualGeom));
+
+        // if input geom has z value, keep it and add m
+        geom = Constructors.geomFromWKT("LINESTRING Z(0 1 3, 1 0 3, 2 0 3)", 
0);
+        actual = Functions.asWKT(Functions.force4D(geom, 10, 10));
+        expected = "LINESTRING ZM(0 1 3 10, 1 0 3 10, 2 0 3 10)";
+        assertEquals(expected, actual);
+
+        // if input geom has m value, keep it and add z
+        geom = Constructors.geomFromWKT("LINESTRING M(0 1 3, 1 0 3, 2 0 3)", 
0);
+        actual = Functions.asWKT(Functions.force4D(geom, 10, 10));
+        expected = "LINESTRING ZM(0 1 10 3, 1 0 10 3, 2 0 10 3)";
+        assertEquals(expected, actual);
+    }
+
     @Test
     public void force3DObject3DDefaultValue() {
         int expectedDims = 3;
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 9a51cae82..81b7414b8 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -1266,6 +1266,53 @@ Output:
 LINESTRING EMPTY
 ```
 
+## ST_Force3DM
+
+Introduction: Forces the geometry into XYM mode. Retains any existing M 
coordinate, but removes the Z coordinate if present. Assigns a default M value 
of 0.0 if `mValue` is not specified.
+
+!!!Note
+Example output is after calling ST_AsText() on returned geometry, which adds M 
for in the WKT.
+
+Format: `ST_Force3DM(geometry: Geometry, mValue: Double = 0.0)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('POLYGON M((0 0 3,0 5 3,5 0 3,0 0 
3),(1 1 3,3 1 3,1 3 3,1 1 3))'), 2.3))
+```
+
+Output:
+
+```
+POLYGON M((0 0 3, 0 5 3, 5 0 3, 0 0 3), (1 1 3, 3 1 3, 1 3 3, 1 1 3))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 2.3))
+```
+
+Output:
+
+```
+LINESTRING M(0 1 2.3, 1 0 2.3, 2 0 2.3)
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('LINESTRING Z(0 1 3,1 0 3,2 0 
3)'), 5))
+```
+
+Output:
+
+```
+LINESTRING M(0 1 5, 1 0 5, 2 0 5)
+```
+
 ## ST_Force3DZ
 
 Introduction: Forces the geometry into a 3-dimensional model so that all 
output representations will have X, Y and Z coordinates.
@@ -1304,6 +1351,45 @@ Output:
 LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)
 ```
 
+## ST_Force4D
+
+Introduction: Converts the input geometry to 4D XYZM representation. Retains 
original Z and M values if present. Assigning 0.0 defaults if `mValue` and 
`zValue` aren't specified. The output contains X, Y, Z, and M coordinates. For 
geometries already in 4D form, the function returns the original geometry 
unmodified.
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds Z for in the WKT for 3D geometries
+
+Format:
+
+`ST_Force4D(geom: Geometry, zValue: Double, mValue: Double)`
+
+`ST_Force4D(geom: Geometry`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force4D(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 
2),(1 1 2,3 1 2,1 3 2,1 1 2))'), 5, 10))
+```
+
+Output:
+
+```
+POLYGON ZM((0 0 2 10, 0 5 2 10, 5 0 2 10, 0 0 2 10), (1 1 2 10, 3 1 2 10, 1 3 
2 10, 1 1 2 10))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force4D(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 3, 1))
+```
+
+Output:
+
+```
+LINESTRING ZM(0 1 3 1, 1 0 3 1, 2 0 3 1)
+```
+
 ## ST_ForceCollection
 
 Introduction: This function converts the input geometry into a 
GeometryCollection, regardless of the original geometry type. If the input is a 
multipart geometry, such as a MultiPolygon or MultiLineString, it will be 
decomposed into a GeometryCollection containing each individual Polygon or 
LineString element from the original multipart geometry.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 9a7a3eafe..59fb5f40f 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1271,6 +1271,53 @@ Output:
 LINESTRING EMPTY
 ```
 
+## ST_Force3DM
+
+Introduction: Forces the geometry into XYM mode. Retains any existing M 
coordinate, but removes the Z coordinate if present. Assigns a default M value 
of 0.0 if `mValue` is not specified.
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds M for in the WKT.
+
+Format: `ST_Force3DM(geometry: Geometry, mValue: Double = 0.0)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('POLYGON M((0 0 3,0 5 3,5 0 3,0 0 
3),(1 1 3,3 1 3,1 3 3,1 1 3))'), 2.3))
+```
+
+Output:
+
+```
+POLYGON M((0 0 3, 0 5 3, 5 0 3, 0 0 3), (1 1 3, 3 1 3, 1 3 3, 1 1 3))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 2.3))
+```
+
+Output:
+
+```
+LINESTRING M(0 1 2.3, 1 0 2.3, 2 0 2.3)
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DM(ST_GeomFromText('LINESTRING Z(0 1 3,1 0 3,2 0 
3)'), 5))
+```
+
+Output:
+
+```
+LINESTRING M(0 1 5, 1 0 5, 2 0 5)
+```
+
 ## ST_Force3DZ
 
 Introduction: Forces the geometry into a 3-dimensional model so that all 
output representations will have X, Y and Z coordinates.
@@ -1309,6 +1356,45 @@ Output:
 LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)
 ```
 
+## ST_Force4D
+
+Introduction: Converts the input geometry to 4D XYZM representation. Retains 
original Z and M values if present. Assigning 0.0 defaults if `mValue` and 
`zValue` aren't specified. The output contains X, Y, Z, and M coordinates. For 
geometries already in 4D form, the function returns the original geometry 
unmodified.
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds Z for in the WKT for 3D geometries
+
+Format:
+
+`ST_Force4D(geom: Geometry, zValue: Double, mValue: Double)`
+
+`ST_Force4D(geom: Geometry`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force4D(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 
2),(1 1 2,3 1 2,1 3 2,1 1 2))'), 5, 10))
+```
+
+Output:
+
+```
+POLYGON ZM((0 0 2 10, 0 5 2 10, 5 0 2 10, 0 0 2 10), (1 1 2 10, 3 1 2 10, 1 3 
2 10, 1 1 2 10))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force4D(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 3, 1))
+```
+
+Output:
+
+```
+LINESTRING ZM(0 1 3 1, 1 0 3 1, 2 0 3 1)
+```
+
 ## ST_ForceCollection
 
 Introduction: This function converts the input geometry into a 
GeometryCollection, regardless of the original geometry type. If the input is a 
multipart geometry, such as a MultiPolygon or MultiLineString, it will be 
decomposed into a GeometryCollection containing each individual Polygon or 
LineString element from the original multipart geometry.
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 df8c86186..102289ed7 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -158,7 +158,9 @@ public class Catalog {
                 new Functions.ST_GeometricMedian(),
                 new Functions.ST_NumPoints(),
                 new Functions.ST_Force3D(),
+                new Functions.ST_Force3DM(),
                 new Functions.ST_Force3DZ(),
+                new Functions.ST_Force4D(),
                 new Functions.ST_ForceCollection(),
                 new Functions.ST_ForcePolygonCW(),
                 new Functions.ST_ForceRHR(),
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 c1e031b83..217d86cfc 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
@@ -1101,6 +1101,22 @@ public class Functions {
         }
     }
 
+    public static class ST_Force3DM extends ScalarFunction {
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint("Double") Double zValue) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force3DM(geometry, 
zValue);
+        }
+
+        @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) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force3DM(geometry);
+        }
+    }
+
     public static class ST_Force3DZ extends ScalarFunction {
 
         @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
@@ -1117,6 +1133,22 @@ public class Functions {
         }
     }
 
+    public static class ST_Force4D extends ScalarFunction {
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint("Double") Double zValue, 
@DataTypeHint("Double") Double mValue) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force4D(geometry, 
zValue, mValue);
+        }
+
+        @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) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force4D(geometry);
+        }
+    }
+
     public static class ST_ForceCollection 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) {
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 6db3f9493..ab4e0e522 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -1326,6 +1326,30 @@ public class FunctionTest extends TestBase{
         assertEquals(expectedDims, actual);
     }
 
+    @Test
+    public void testForce3DM() {
+        Table geomTable = tableEnv.sqlQuery("SELECT 
ST_Force3DM(ST_GeomFromText('LINESTRING (1 2, 2 3, 3 4)')) AS geom");
+        Boolean actual = (Boolean) 
first(geomTable.select(call(Functions.ST_HasM.class.getSimpleName(), 
$("geom")))).getField(0);
+        assertEquals(Boolean.TRUE, actual);
+
+        geomTable = tableEnv.sqlQuery("SELECT 
ST_Force3DM(ST_GeomFromText('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (4 4, 4 6, 
6 6, 6 4, 4 4))')) AS geom");
+        actual = (Boolean) 
first(geomTable.select(call(Functions.ST_HasM.class.getSimpleName(), 
$("geom")))).getField(0);
+        assertEquals(Boolean.TRUE, actual);
+    }
+
+    @Test
+    public void testForce4D() {
+        Table geomTable = tableEnv.sqlQuery("SELECT 
ST_Force4D(ST_GeomFromText('LINESTRING (1 2, 2 3, 3 4)')) AS geom");
+        String actual = (String) 
first(geomTable.select(call(Functions.ST_AsText.class.getSimpleName(), 
$("geom")))).getField(0);
+        String expected = "LINESTRING ZM(1 2 0 0, 2 3 0 0, 3 4 0 0)";
+        assertEquals(expected, actual);
+
+        geomTable = tableEnv.sqlQuery("SELECT 
ST_Force4D(ST_GeomFromText('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (4 4, 4 6, 
6 6, 6 4, 4 4))')) AS geom");
+        actual = (String) 
first(geomTable.select(call(Functions.ST_AsText.class.getSimpleName(), 
$("geom")))).getField(0);
+        expected = "POLYGON ZM((0 0 0 0, 10 0 0 0, 10 10 0 0, 0 10 0 0, 0 0 0 
0), (4 4 0 0, 4 6 0 0, 6 6 0 0, 6 4 0 0, 4 4 0 0))";
+        assertEquals(expected, actual);
+    }
+
     @Test
     public void testForceCollection() {
         int actual = (int) first(
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index 82ca9c2fe..d93408916 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1574,6 +1574,17 @@ def ST_Force3D(geometry: ColumnOrName, zValue: 
Optional[Union[ColumnOrName, floa
     args = (geometry, zValue)
     return _call_st_function("ST_Force3D", args)
 
+@validate_argument_types
+def ST_Force3DM(geometry: ColumnOrName, mValue: Optional[Union[ColumnOrName, 
float]] = 0.0) -> Column:
+    """
+    Return a geometry with a 3D coordinate of value 'mValue' forced upon it. 
No change happens if the geometry is already 3D
+    :param mValue: Optional value of m coordinate to be potentially added, 
default value is 0.0
+    :param geometry: Geometry column to make 3D
+    :return: 3D geometry with either already present m coordinate if any, or m 
coordinate with given mValue
+    """
+    args = (geometry, mValue)
+    return _call_st_function("ST_Force3DM", args)
+
 @validate_argument_types
 def ST_Force3DZ(geometry: ColumnOrName, zValue: Optional[Union[ColumnOrName, 
float]] = 0.0) -> Column:
     """
@@ -1585,6 +1596,20 @@ def ST_Force3DZ(geometry: ColumnOrName, zValue: 
Optional[Union[ColumnOrName, flo
     args = (geometry, zValue)
     return _call_st_function("ST_Force3DZ", args)
 
+@validate_argument_types
+def ST_Force4D(geometry: ColumnOrName, zValue: Optional[Union[ColumnOrName, 
float]] = 0.0,
+               mValue: Optional[Union[ColumnOrName, float]] = 0.0) -> Column:
+    """
+    Return a geometry with a 4D coordinate of value 'zValue' and mValue forced 
upon it. No change happens if the
+    geometry is already 4D, if geometry either has z or m, it will not change 
the existing z or m value.
+
+    :param zValue: Optional value of z coordinate to be potentially added, 
default value is 0.0
+    :param geometry: Geometry column to make 4D
+    :return: 4D geometry with either already 4D geom or z and m component 
provided by zValue and mValue respectively
+    """
+    args = (geometry, zValue, mValue)
+    return _call_st_function("ST_Force4D", args)
+
 @validate_argument_types
 def ST_ForceCollection(geometry: ColumnOrName) -> Column:
     """
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 42818f07f..35d92dee8 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -124,7 +124,9 @@ test_configurations = [
     (stf.ST_FlipCoordinates, ("point",), "point_geom", "", "POINT (1 0)"),
     (stf.ST_Force_2D, ("point",), "point_geom", "", "POINT (0 1)"),
     (stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
+    (stf.ST_Force3DM, ("point", 1.0), "point_geom", "ST_AsText(geom)", "POINT 
M(0 1 1)"),
     (stf.ST_Force3DZ, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
+    (stf.ST_Force4D, ("point", 1.0, 1.0), "point_geom", "ST_AsText(geom)", 
"POINT ZM(0 1 1 1)"),
     (stf.ST_ForceCollection, ("multipoint",), "multipoint_geom", 
"ST_NumGeometries(geom)", 4),
     (stf.ST_ForcePolygonCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 
3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
     (stf.ST_ForcePolygonCCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 
3 0, 3 3, 0 0), (1 1, 2 2, 2 1, 1 1))"),
@@ -304,7 +306,9 @@ wrong_type_configurations = [
     (stf.ST_ExteriorRing, (None,)),
     (stf.ST_FlipCoordinates, (None,)),
     (stf.ST_Force_2D, (None,)),
+    (stf.ST_Force3DM, (None,)),
     (stf.ST_Force3DZ, (None,)),
+    (stf.ST_Force4D, (None,)),
     (stf.ST_ForceCollection, (None,)),
     (stf.ST_ForcePolygonCW, (None,)),
     (stf.ST_ForcePolygonCCW, (None,)),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index 7686eb81a..c50dda6ab 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1468,12 +1468,23 @@ class TestPredicateJoin(TestBase):
         actual = actualDf.selectExpr("ST_NDims(geom)").take(1)[0][0]
         assert expected == actual
 
+    def test_force3DM(self):
+        actualDf = self.spark.sql("SELECT 
ST_Force3DM(ST_GeomFromText('LINESTRING(0 1, 1 0, 2 0)'), 1.1) AS geom")
+        actual = actualDf.selectExpr("ST_HasM(geom)").take(1)[0][0]
+        assert actual
+
     def test_force3DZ(self):
         expected = 3
         actualDf = self.spark.sql("SELECT 
ST_Force3DZ(ST_GeomFromText('LINESTRING(0 1, 1 0, 2 0)'), 1.1) AS geom")
         actual = actualDf.selectExpr("ST_NDims(geom)").take(1)[0][0]
         assert expected == actual
 
+    def test_force4D(self):
+        expected = 4
+        actualDf = self.spark.sql("SELECT 
ST_Force4D(ST_GeomFromText('LINESTRING(0 1, 1 0, 2 0)'), 1.1, 1.1) AS geom")
+        actual = actualDf.selectExpr("ST_NDims(geom)").take(1)[0][0]
+        assert expected == actual
+
     def test_st_force_collection(self):
         basedf = self.spark.sql("SELECT ST_GeomFromWKT('MULTIPOINT (30 10, 40 
40, 20 20, 10 30, 10 10, 20 50)') AS mpoint, ST_GeomFromWKT('POLYGON ((30 10, 
40 40, 20 40, 10 20, 30 10))') AS poly")
         actual = 
basedf.selectExpr("ST_NumGeometries(ST_ForceCollection(mpoint))").take(1)[0][0]
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 509687a6d..7a2f79176 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
@@ -191,7 +191,9 @@ object Catalog {
     function[ST_LengthSpheroid](),
     function[ST_NumPoints](),
     function[ST_Force3D](0.0),
+    function[ST_Force3DM](0.0),
     function[ST_Force3DZ](0.0),
+    function[ST_Force4D](),
     function[ST_ForceCollection](),
     function[ST_NRings](),
     function[ST_Translate](0.0),
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 d414d30d0..0f91ecd08 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
@@ -1208,6 +1208,22 @@ case class ST_Force3DZ(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_Force3DM(inputExpressions: Seq[Expression])
+  extends InferredExpression(inferrableFunction2(Functions.force3DM)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+case class ST_Force4D(inputExpressions: Seq[Expression])
+  extends InferredExpression(inferrableFunction3(Functions.force4D), 
inferrableFunction1(Functions.force4D)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class ST_ForceCollection(inputExpressions: Seq[Expression])
   extends InferredExpression(Functions.forceCollection _) {
 
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 a54e59044..02b0cb17b 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
@@ -427,11 +427,21 @@ object st_functions extends DataFrameAPI {
 
   def ST_Force3D(geometry: String, zValue: Double): Column = 
wrapExpression[ST_Force3D](geometry, zValue)
 
+  def ST_Force3DM(geometry: Column): Column = 
wrapExpression[ST_Force3DM](geometry, 0.0)
+  def ST_Force3DM(geometry: String): Column = 
wrapExpression[ST_Force3DM](geometry, 0.0)
+  def ST_Force3DM(geometry: Column, zValue: Column): Column = 
wrapExpression[ST_Force3DM](geometry, zValue)
+  def ST_Force3DM(geometry: String, zValue: Double): Column = 
wrapExpression[ST_Force3DM](geometry, zValue)
+
   def ST_Force3DZ(geometry: Column): Column = 
wrapExpression[ST_Force3DZ](geometry, 0.0)
   def ST_Force3DZ(geometry: String): Column = 
wrapExpression[ST_Force3DZ](geometry, 0.0)
   def ST_Force3DZ(geometry: Column, zValue: Column): Column = 
wrapExpression[ST_Force3DZ](geometry, zValue)
   def ST_Force3DZ(geometry: String, zValue: Double): Column = 
wrapExpression[ST_Force3DZ](geometry, zValue)
 
+  def ST_Force4D(geometry: Column): Column = 
wrapExpression[ST_Force4D](geometry, 0.0, 0.0)
+  def ST_Force4D(geometry: String): Column = 
wrapExpression[ST_Force4D](geometry, 0.0, 0.0)
+  def ST_Force4D(geometry: Column, zValue: Column, mValue: Column): Column = 
wrapExpression[ST_Force4D](geometry, zValue, mValue)
+  def ST_Force4D(geometry: String, zValue: Double, mValue: Double): Column = 
wrapExpression[ST_Force4D](geometry, zValue, mValue)
+
   def ST_ForceCollection(geometry: Column): Column = 
wrapExpression[ST_ForceCollection](geometry)
 
   def ST_ForceCollection(geometry: String): Column = 
wrapExpression[ST_ForceCollection](geometry)
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 72a55323d..7c6e05b7e 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
@@ -1348,6 +1348,17 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertEquals(expectedGeomDefaultValue, 
wktWriter.write(actualGeomDefaultValue))
     }
 
+    it("Passed ST_Force3DM") {
+      val lineDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 1, 1 
0, 2 0)') AS geom")
+      val expectedGeom = "LINESTRING M(0 1 2.3, 1 0 2.3, 2 0 2.3)"
+      val expectedGeomDefaultValue = "LINESTRING M(0 1 0, 1 0 0, 2 0 0)"
+      val forcedGeom = lineDf.select(ST_AsText(ST_Force3DM("geom", 
2.3))).take(1)(0).get(0).asInstanceOf[String]
+      assertEquals(expectedGeom, forcedGeom)
+      val lineDfDefaultValue = sparkSession.sql("SELECT 
ST_GeomFromWKT('LINESTRING (0 1, 1 0, 2 0)') AS geom")
+      val actualGeomDefaultValue = 
lineDfDefaultValue.select(ST_AsText(ST_Force3DM("geom"))).take(1)(0).get(0).asInstanceOf[String]
+      assertEquals(expectedGeomDefaultValue, actualGeomDefaultValue)
+    }
+
     it("Passed ST_Force3DZ") {
       val lineDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 1, 1 
0, 2 0)') AS geom")
       val expectedGeom = "LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)"
@@ -1360,6 +1371,17 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertEquals(expectedGeomDefaultValue, 
wktWriter.write(actualGeomDefaultValue))
     }
 
+    it("Passed ST_Force4D") {
+      val lineDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 1, 1 
0, 2 0)') AS geom")
+      val expectedGeom = "LINESTRING ZM(0 1 4 4, 1 0 4 4, 2 0 4 4)"
+      val expectedGeomDefaultValue = "LINESTRING ZM(0 1 0 0, 1 0 0 0, 2 0 0 0)"
+      val forcedGeom = lineDf.select(ST_AsText(ST_Force4D("geom", 4, 
4))).take(1)(0).get(0).asInstanceOf[String]
+      assertEquals(expectedGeom, forcedGeom)
+      val lineDfDefaultValue = sparkSession.sql("SELECT 
ST_GeomFromWKT('LINESTRING (0 1, 1 0, 2 0)') AS geom")
+      val actualGeomDefaultValue = 
lineDfDefaultValue.select(ST_AsText(ST_Force4D("geom"))).take(1)(0).get(0).asInstanceOf[String]
+      assertEquals(expectedGeomDefaultValue, actualGeomDefaultValue)
+    }
+
     it("Passed ST_ForceCollection") {
       val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTIPOINT (30 10, 
40 40, 20 20, 10 30, 10 10, 20 50)') AS mpoint, ST_GeomFromWKT('POLYGON ((30 
10, 40 40, 20 40, 10 20, 30 10))') AS poly")
       var actual = 
baseDf.select(ST_NumGeometries(ST_ForceCollection("mpoint"))).first().get(0)
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 cf7e40460..dd76d3bdd 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
@@ -2292,6 +2292,42 @@ class functionTestScala extends TestBaseScala with 
Matchers with GeometrySample
     }
   }
 
+  it("Should pass ST_Force3DM") {
+    val geomTestCases = Map(
+      ("'LINESTRING (0 1, 1 0, 2 0)'") -> ("'LINESTRING M(0 1 1, 1 0 1, 2 0 
1)'", "'LINESTRING M(0 1 0, 1 0 0, 2 0 0)'"),
+      ("'LINESTRING M(0 1 3, 1 0 3, 2 0 3)'") -> ("'LINESTRING M(0 1 3, 1 0 3, 
2 0 3)'", "'LINESTRING M(0 1 3, 1 0 3, 2 0 3)'"),
+      ("'LINESTRING EMPTY'") -> ("'LINESTRING EMPTY'", "'LINESTRING EMPTY'")
+    )
+    for (((geom), expectedResult) <- geomTestCases) {
+      val df = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force3DM(ST_GeomFromWKT($geom), 1)) AS geom, " + 
s"$expectedResult")
+      val dfDefaultValue = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force3DM(ST_GeomFromWKT($geom))) AS geom, " + s"$expectedResult")
+      val actual = df.take(1)(0).get(0).asInstanceOf[String]
+      val expected = 
df.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(0).asInstanceOf[String]
+      val actualDefaultValue = 
dfDefaultValue.take(1)(0).get(0).asInstanceOf[String]
+      val expectedDefaultValue = 
dfDefaultValue.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(1).asInstanceOf[String]
+      assertEquals(expected, actual)
+      assertEquals(expectedDefaultValue, actualDefaultValue);
+    }
+  }
+
+  it("Should pass ST_Force4D") {
+    val geomTestCases = Map(
+      ("'LINESTRING (0 1, 1 0, 2 0)'") -> ("'LINESTRING ZM(0 1 1 1, 1 0 1 1, 2 
0 1 1)'", "'LINESTRING ZM(0 1 0 0, 1 0 0 0, 2 0 0 0)'"),
+      ("'LINESTRING ZM(0 1 3 2, 1 0 3 2, 2 0 3 2)'") -> ("'LINESTRING ZM(0 1 3 
2, 1 0 3 2, 2 0 3 2)'", "'LINESTRING ZM(0 1 3 2, 1 0 3 2, 2 0 3 2)'"),
+      ("'LINESTRING EMPTY'") -> ("'LINESTRING EMPTY'", "'LINESTRING EMPTY'")
+    )
+    for (((geom), expectedResult) <- geomTestCases) {
+      val df = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force4D(ST_GeomFromWKT($geom), 1, 1)) AS geom, " + 
s"$expectedResult")
+      val dfDefaultValue = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force4D(ST_GeomFromWKT($geom))) AS geom, " + s"$expectedResult")
+      val actual = df.take(1)(0).get(0).asInstanceOf[String]
+      val expected = 
df.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(0).asInstanceOf[String]
+      val actualDefaultValue = 
dfDefaultValue.take(1)(0).get(0).asInstanceOf[String]
+      val expectedDefaultValue = 
dfDefaultValue.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(1).asInstanceOf[String]
+      assertEquals(expected, actual)
+      assertEquals(expectedDefaultValue, actualDefaultValue);
+    }
+  }
+
   it("Passed ST_ForceCollection") {
     var actual = sparkSession.sql("SELECT 
ST_NumGeometries(ST_ForceCollection(ST_GeomFromWKT('MULTIPOINT (30 10, 40 40, 
20 20, 10 30, 10 10, 20 50)')))").first().get(0)
     assert(actual == 6)


Reply via email to