You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2021/12/03 15:59:10 UTC

[incubator-sedona] branch master updated: [SEDONA-75] Add support for "3D" geometries (#565)

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/incubator-sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new 2e7bbc1  [SEDONA-75] Add support for "3D" geometries (#565)
2e7bbc1 is described below

commit 2e7bbc198a2d50071203293b31fa46c9dd0b7a9c
Author: Kurtis Seebaldt <ks...@gmail.com>
AuthorDate: Fri Dec 3 09:59:03 2021 -0600

    [SEDONA-75] Add support for "3D" geometries (#565)
---
 docs/api/sql/Constructor.md                        |  1 +
 docs/api/sql/Function.md                           | 29 ++++++++
 python/tests/sql/test_constructor_test.py          | 20 ++++++
 python/tests/sql/test_function.py                  | 60 +++++++++++++++-
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  2 +
 .../sedona/sql/utils/GeometrySerializer.scala      |  6 +-
 .../sql/sedona_sql/expressions/Constructors.scala  | 16 ++++-
 .../sql/sedona_sql/expressions/Functions.scala     | 57 ++++++++++++++-
 .../apache/sedona/sql/constructorTestScala.scala   | 24 +++++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 84 +++++++++++++++++++++-
 10 files changed, 290 insertions(+), 9 deletions(-)

diff --git a/docs/api/sql/Constructor.md b/docs/api/sql/Constructor.md
index a0faeb3..4145593 100644
--- a/docs/api/sql/Constructor.md
+++ b/docs/api/sql/Constructor.md
@@ -107,6 +107,7 @@ System.setProperty("sedona.global.charset", "utf8")
 Introduction: Construct a Point from X and Y
 
 Format: `ST_Point (X:decimal, Y:decimal)`
+Format: `ST_Point (X:decimal, Y:decimal, Z:decimal)`
 
 Since: `v1.0.0`
 
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index d0be0c6..e012305 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -12,6 +12,20 @@ SELECT ST_Distance(polygondf.countyshape, polygondf.countyshape)
 FROM polygondf
 ```
 
+## ST_3DDistance
+
+Introduction: Return the 3-dimensional minimum cartesian distance between A and B
+
+Format: `ST_3DDistance (A:geometry, B:geometry)`
+
+Since: `v1.2.0`
+
+Spark SQL example:
+```SQL
+SELECT ST_3DDistance(polygondf.countyshape, polygondf.countyshape)
+FROM polygondf
+```
+
 ## ST_ConvexHull
 
 Introduction: Return the Convex Hull of polgyon A
@@ -405,6 +419,21 @@ SELECT ST_Y(ST_POINT(0.0 25.0))
 
 Output: `25.0`
 
+## ST_Z
+
+Introduction: Returns Z Coordinate of given Point, null otherwise.
+
+Format: `ST_Z(pointA: Point)`
+
+Since: `v1.2.0`
+
+Spark SQL example:
+```SQL
+SELECT ST_Z(ST_POINT(0.0 25.0 11.0))
+```
+
+Output: `11.0`
+
 ## ST_StartPoint
 
 Introduction: Returns first point of given linestring.
diff --git a/python/tests/sql/test_constructor_test.py b/python/tests/sql/test_constructor_test.py
index 1639be1..4ea5ee2 100644
--- a/python/tests/sql/test_constructor_test.py
+++ b/python/tests/sql/test_constructor_test.py
@@ -33,6 +33,10 @@ class TestConstructors(TestBase):
         point_df = self.spark.sql("select ST_Point(cast(pointtable._c0 as Decimal(24,20)), cast(pointtable._c1 as Decimal(24,20))) as arealandmark from pointtable")
         assert point_df.count() == 1000
 
+    def test_st_point_3d(self):
+        point_df = self.spark.sql("SELECT ST_Point(1.2345, 2.3456, 3.4567)")
+        assert point_df.count() == 1
+
     def test_st_point_from_text(self):
         point_csv_df = self.spark.read.format("csv").\
             option("delimiter", ",").\
@@ -56,6 +60,22 @@ class TestConstructors(TestBase):
         polygon_df.show(10)
         assert polygon_df.count() == 100
 
+    def test_st_geom_from_wkt_3d(self):
+        input_df = self.spark.createDataFrame([
+            ("Point(21 52 87)",),
+            ("Polygon((0 0 1, 0 1 1, 1 1 1, 1 0 1, 0 0 1))",),
+            ("Linestring(0 0 1, 1 1 2, 1 0 3)",),
+            ("MULTIPOINT ((10 40 66), (40 30 77), (20 20 88), (30 10 99))",),
+            ("MULTIPOLYGON (((30 20 11, 45 40 11, 10 40 11, 30 20 11)), ((15 5 11, 40 10 11, 10 20 11, 5 10 11, 15 5 11)))",),
+            ("MULTILINESTRING ((10 10 11, 20 20 11, 10 40 11), (40 40 11, 30 30 11, 40 20 11, 30 10 11))",),
+            ("MULTIPOLYGON (((40 40 11, 20 45 11, 45 30 11, 40 40 11)), ((20 35 11, 10 30 11, 10 10 11, 30 5 11, 45 20 11, 20 35 11), (30 20 11, 20 15 11, 20 25 11, 30 20 11)))",),
+            ("POLYGON((0 0 11, 0 5 11, 5 5 11, 5 0 11, 0 0 11), (1 1 11, 2 1 11, 2 2 11, 1 2 11, 1 1 11))",),
+        ], ["wkt"])
+
+        input_df.createOrReplaceTempView("input_wkt")
+        polygon_df = self.spark.sql("select ST_GeomFromWkt(wkt) as geomn from input_wkt")
+        assert polygon_df.count() == 8
+
     def test_st_geom_from_text(self):
         polygon_wkt_df = self.spark.read.format("csv").\
             option("delimiter", "\t").\
diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py
index 08a92aa..ab8d87f 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -155,6 +155,10 @@ class TestPredicateJoin(TestBase):
         function_df = self.spark.sql("select ST_Distance(polygondf.countyshape, polygondf.countyshape) from polygondf")
         function_df.show()
 
+    def test_st_3ddistance(self):
+        function_df = self.spark.sql("select ST_3DDistance(ST_Point(0.0, 0.0, 5.0), ST_Point(1.0, 1.0, -6.0))")
+        assert function_df.count() == 1
+
     def test_st_transform(self):
         polygon_wkt_df = self.spark.read.format("csv"). \
             option("delimiter", "\t"). \
@@ -237,6 +241,36 @@ class TestPredicateJoin(TestBase):
         wkt_df = self.spark.sql("select ST_AsText(countyshape) as wkt from polygondf")
         assert polygon_df.take(1)[0]["countyshape"].wkt == loads(wkt_df.take(1)[0]["wkt"]).wkt
 
+
+    def test_st_astext_3d(self):
+        input_df = self.spark.createDataFrame([
+            ("Point(21 52 87)",),
+            ("Polygon((0 0 1, 0 1 1, 1 1 1, 1 0 1, 0 0 1))",),
+            ("Linestring(0 0 1, 1 1 2, 1 0 3)",),
+            ("MULTIPOINT ((10 40 66), (40 30 77), (20 20 88), (30 10 99))",),
+            ("MULTIPOLYGON (((30 20 11, 45 40 11, 10 40 11, 30 20 11)), ((15 5 11, 40 10 11, 10 20 11, 5 10 11, 15 5 11)))",),
+            ("MULTILINESTRING ((10 10 11, 20 20 11, 10 40 11), (40 40 11, 30 30 11, 40 20 11, 30 10 11))",),
+            ("MULTIPOLYGON (((40 40 11, 20 45 11, 45 30 11, 40 40 11)), ((20 35 11, 10 30 11, 10 10 11, 30 5 11, 45 20 11, 20 35 11), (30 20 11, 20 15 11, 20 25 11, 30 20 11)))",),
+            ("POLYGON((0 0 11, 0 5 11, 5 5 11, 5 0 11, 0 0 11), (1 1 11, 2 1 11, 2 2 11, 1 2 11, 1 1 11))",),
+        ], ["wkt"])
+
+        input_df.createOrReplaceTempView("input_wkt")
+        polygon_df = self.spark.sql("select ST_AsText(ST_GeomFromWkt(wkt)) as wkt from input_wkt")
+        assert polygon_df.count() == 8
+
+    def test_st_as_text_3d(self):
+        polygon_wkt_df = self.spark.read.format("csv"). \
+            option("delimiter", "\t"). \
+            option("header", "false"). \
+            load(mixed_wkt_geometry_input_location)
+
+        polygon_wkt_df.createOrReplaceTempView("polygontable")
+        polygon_df = self.spark.sql("select ST_GeomFromWKT(polygontable._c0) as countyshape from polygontable")
+        polygon_df.createOrReplaceTempView("polygondf")
+        wkt_df = self.spark.sql("select ST_AsText(countyshape) as wkt from polygondf")
+        assert polygon_df.take(1)[0]["countyshape"].wkt == loads(wkt_df.take(1)[0]["wkt"]).wkt
+
+
     def test_st_n_points(self):
         test = self.spark.sql("SELECT ST_NPoints(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)'))")
 
@@ -308,6 +342,30 @@ class TestPredicateJoin(TestBase):
 
         assert(not polygons.count())
 
+    def test_st_z(self):
+        point_df = self.spark.sql(
+            "select ST_GeomFromWKT('POINT Z (1.1 2.2 3.3)') as geom"
+        )
+        polygon_df = self.spark.sql(
+            "select ST_GeomFromWKT('POLYGON Z ((0 0 2, 0 1 2, 1 1 2, 1 0 2, 0 0 2))') as geom"
+        )
+        linestring_df = self.spark.sql(
+            "select ST_GeomFromWKT('LINESTRING Z (0 0 1, 0 1 2)') as geom"
+        )
+
+        points = point_df \
+            .selectExpr("ST_Z(geom)").collect()
+
+        polygons = polygon_df.selectExpr("ST_Z(geom) as z").filter("z IS NOT NULL")
+
+        linestrings = linestring_df.selectExpr("ST_Z(geom) as z").filter("z IS NOT NULL")
+
+        assert([point[0] for point in points] == [3.3])
+
+        assert(not linestrings.count())
+
+        assert(not polygons.count())
+
     def test_st_start_point(self):
 
         point_df = create_sample_points_df(self.spark, 5)
@@ -387,7 +445,7 @@ class TestPredicateJoin(TestBase):
 
     def test_st_exterior_ring(self):
         polygon_df = create_simple_polygons_df(self.spark, 5)
-        additional_wkt = "POLYGON((0 0 1, 1 1 1, 1 2 1, 1 1 1, 0 0 1))"
+        additional_wkt = "POLYGON((0 0, 1 1, 1 2, 1 1, 0 0))"
         additional_wkt_df = self.spark.createDataFrame([[wkt.loads(additional_wkt)]], self.geo_schema)
 
         polygons_df = polygon_df.union(additional_wkt_df)
diff --git a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 2ce31eb..01d3c6b 100644
--- a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -41,6 +41,7 @@ object Catalog {
     ST_Intersects,
     ST_Within,
     ST_Distance,
+    ST_3DDistance,
     ST_ConvexHull,
     ST_NPoints,
     ST_Buffer,
@@ -71,6 +72,7 @@ object Catalog {
     ST_Azimuth,
     ST_X,
     ST_Y,
+    ST_Z,
     ST_StartPoint,
     ST_Boundary,
     ST_MinimumBoundingRadius,
diff --git a/sql/src/main/scala/org/apache/sedona/sql/utils/GeometrySerializer.scala b/sql/src/main/scala/org/apache/sedona/sql/utils/GeometrySerializer.scala
index a4f752e..c8321bc 100644
--- a/sql/src/main/scala/org/apache/sedona/sql/utils/GeometrySerializer.scala
+++ b/sql/src/main/scala/org/apache/sedona/sql/utils/GeometrySerializer.scala
@@ -34,7 +34,7 @@ object GeometrySerializer {
     * @return Array of bites represents this geometry
     */
   def serialize(geometry: Geometry): Array[Byte] = {
-    val writer = new WKBWriter(2, 2, true)
+    val writer = new WKBWriter(getDimension(geometry), 2, true)
     writer.write(geometry)
   }
 
@@ -48,4 +48,8 @@ object GeometrySerializer {
     val reader = new WKBReader()
     reader.read(values.toByteArray())
   }
+
+  def getDimension(geometry: Geometry): Int = {
+    if (geometry.getCoordinate != null && !java.lang.Double.isNaN(geometry.getCoordinate.getZ)) 3 else 2
+  }
 }
diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Constructors.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Constructors.scala
index 9819ba2..bd99f79 100644
--- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Constructors.scala
+++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Constructors.scala
@@ -27,7 +27,7 @@ import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
 import org.apache.spark.sql.catalyst.util.GenericArrayData
 import org.apache.spark.sql.sedona_sql.UDT.GeometryUDT
 import org.apache.spark.sql.sedona_sql.expressions.geohash.GeoHashDecoder
-import org.apache.spark.sql.sedona_sql.expressions.implicits.{GeometryEnhancer, InputExpressionEnhancer}
+import org.apache.spark.sql.sedona_sql.expressions.implicits.{GeometryEnhancer, InputExpressionEnhancer, SequenceEnhancer}
 import org.apache.spark.sql.types.{DataType, Decimal}
 import org.apache.spark.unsafe.types.UTF8String
 import org.locationtech.jts.geom.{Coordinate, GeometryFactory}
@@ -258,7 +258,7 @@ case class ST_GeomFromGeoJSON(inputExpressions: Seq[Expression])
   */
 case class ST_Point(inputExpressions: Seq[Expression])
   extends Expression with CodegenFallback with UserDataGeneratator {
-  assert(inputExpressions.length == 2)
+  inputExpressions.betweenLength(2, 3)
 
   override def nullable: Boolean = false
 
@@ -272,8 +272,18 @@ case class ST_Point(inputExpressions: Seq[Expression])
       case b: Decimal => b.toDouble
     }
 
+    val coord = if (inputExpressions.length == 2) {
+      new Coordinate(x, y)
+    } else {
+      val z = inputExpressions(2).eval(inputRow) match {
+        case a: Double => a
+        case b: Decimal => b.toDouble
+      }
+      new Coordinate(x, y, z)
+    }
+
     var geometryFactory = new GeometryFactory()
-    var geometry = geometryFactory.createPoint(new Coordinate(x, y))
+    var geometry = geometryFactory.createPoint(coord)
     new GenericArrayData(GeometrySerializer.serialize(geometry))
   }
 
diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index b5894b8..39abeea 100644
--- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -36,10 +36,11 @@ import org.geotools.geometry.jts.JTS
 import org.geotools.referencing.CRS
 import org.locationtech.jts.algorithm.MinimumBoundingCircle
 import org.locationtech.jts.geom.{PrecisionModel, _}
-import org.locationtech.jts.io.{ByteOrderValues, WKBWriter}
+import org.locationtech.jts.io.{ByteOrderValues, WKBWriter, WKTWriter}
 import org.locationtech.jts.linearref.LengthIndexedLine
 import org.locationtech.jts.operation.IsSimpleOp
 import org.locationtech.jts.operation.buffer.BufferParameters
+import org.locationtech.jts.operation.distance3d.Distance3DOp
 import org.locationtech.jts.operation.linemerge.LineMerger
 import org.locationtech.jts.operation.valid.IsValidOp
 import org.locationtech.jts.precision.GeometryPrecisionReducer
@@ -85,6 +86,33 @@ case class ST_Distance(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_3DDistance(inputExpressions: Seq[Expression])
+  extends Expression with CodegenFallback {
+  assert(inputExpressions.length == 2)
+
+  override def nullable: Boolean = true
+
+  override def toString: String = s" **${ST_3DDistance.getClass.getName}**  "
+
+  override def eval(inputRow: InternalRow): Any = {
+    val leftGeometry = inputExpressions(0).toGeometry(inputRow)
+    val rightGeometry = inputExpressions(1).toGeometry(inputRow)
+
+    (leftGeometry, rightGeometry) match {
+      case (leftGeometry: Geometry, rightGeometry: Geometry) => Distance3DOp.distance(leftGeometry, rightGeometry)
+      case _ => null
+    }
+  }
+
+  override def dataType = DoubleType
+
+  override def children: Seq[Expression] = inputExpressions
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
+    copy(inputExpressions = newChildren)
+  }
+}
+
 /**
   * Return the convex hull of a Geometry.
   *
@@ -527,7 +555,8 @@ case class ST_AsText(inputExpressions: Seq[Expression])
 
   override def eval(input: InternalRow): Any = {
     val geometry = GeometrySerializer.deserialize(inputExpressions.head.eval(input).asInstanceOf[ArrayData])
-    UTF8String.fromString(geometry.toText)
+    val writer = new WKTWriter(GeometrySerializer.getDimension(geometry))
+    UTF8String.fromString(writer.write(geometry))
   }
 
   override def dataType: DataType = StringType
@@ -795,6 +824,30 @@ case class ST_Y(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_Z(inputExpressions: Seq[Expression])
+  extends Expression with CodegenFallback {
+  assert(inputExpressions.length == 1)
+
+  override def nullable: Boolean = true
+
+  override def eval(input: InternalRow): Any = {
+    val geometry = inputExpressions.head.toGeometry(input)
+
+    geometry match {
+      case point: Point => point.getCoordinate.getZ
+      case _ => null
+    }
+  }
+
+  override def dataType: DataType = DoubleType
+
+  override def children: Seq[Expression] = inputExpressions
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class ST_StartPoint(inputExpressions: Seq[Expression])
   extends Expression with CodegenFallback {
   assert(inputExpressions.length == 1)
diff --git a/sql/src/test/scala/org/apache/sedona/sql/constructorTestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/constructorTestScala.scala
index 0b6c7bd..fe1f69a 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/constructorTestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/constructorTestScala.scala
@@ -26,6 +26,8 @@ import org.locationtech.jts.geom.Geometry
 
 class constructorTestScala extends TestBaseScala {
 
+  import sparkSession.implicits._
+
   describe("Sedona-SQL Constructor Test") {
     it("Passed ST_Point") {
 
@@ -42,6 +44,11 @@ class constructorTestScala extends TestBaseScala {
       assert(pointDf.count() == 1)
     }
 
+    it("Passed ST_Point 3D") {
+      val pointDf = sparkSession.sql("SELECT ST_Point(1.2345, 2.3456, 3.4567)")
+      assert(pointDf.count() == 1)
+    }
+
     it("Passed ST_PolygonFromEnvelope") {
       val polygonDF = sparkSession.sql("select ST_PolygonFromEnvelope(double(1.234),double(2.234),double(3.345),double(3.345))")
       assert(polygonDF.count() == 1)
@@ -62,6 +69,23 @@ class constructorTestScala extends TestBaseScala {
       assert(polygonDf.count() == 100)
     }
 
+    it("Passed ST_GeomFromWKT 3D") {
+      val geometryDf = Seq(
+        "Point(21 52 87)",
+        "Polygon((0 0 1, 0 1 1, 1 1 1, 1 0 1, 0 0 1))",
+        "Linestring(0 0 1, 1 1 2, 1 0 3)",
+        "MULTIPOINT ((10 40 66), (40 30 77), (20 20 88), (30 10 99))",
+        "MULTIPOLYGON (((30 20 11, 45 40 11, 10 40 11, 30 20 11)), ((15 5 11, 40 10 11, 10 20 11, 5 10 11, 15 5 11)))",
+        "MULTILINESTRING ((10 10 11, 20 20 11, 10 40 11), (40 40 11, 30 30 11, 40 20 11, 30 10 11))",
+        "MULTIPOLYGON (((40 40 11, 20 45 11, 45 30 11, 40 40 11)), ((20 35 11, 10 30 11, 10 10 11, 30 5 11, 45 20 11, 20 35 11), (30 20 11, 20 15 11, 20 25 11, 30 20 11)))",
+        "POLYGON((0 0 11, 0 5 11, 5 5 11, 5 0 11, 0 0 11), (1 1 11, 2 1 11, 2 2 11, 1 2 11, 1 1 11))"
+      ).map(wkt => Tuple1(wkt)).toDF("geom")
+
+      geometryDf.createOrReplaceTempView("geometrytable")
+      var polygonDf = sparkSession.sql("select ST_GeomFromWkt(geometrytable.geom) from geometrytable")
+      assert(polygonDf.count() == 8)
+    }
+
     it("Passed ST_GeomFromText") {
       var polygonWktDf = sparkSession.read.format("csv").option("delimiter", "\t").option("header", "false").load(mixedWktGeometryInputLocation)
       polygonWktDf.createOrReplaceTempView("polygontable")
diff --git a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index f4a0ad8..7d9918f 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -26,7 +26,9 @@ import org.apache.spark.sql.{DataFrame, Row}
 import org.geotools.geometry.jts.WKTReader2
 import org.locationtech.jts.algorithm.MinimumBoundingCircle
 import org.locationtech.jts.geom.{Geometry, Polygon}
+import org.locationtech.jts.io.WKTWriter
 import org.locationtech.jts.linearref.LengthIndexedLine
+import org.locationtech.jts.operation.distance3d.Distance3DOp
 import org.scalatest.{GivenWhenThen, Matchers}
 
 class functionTestScala extends TestBaseScala with Matchers with GeometrySample with GivenWhenThen {
@@ -98,6 +100,16 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
       assert(functionDf.count() > 0);
     }
 
+    it("Passed ST_3DDistance") {
+      val point1 = wktReader.read("POINT Z (0 0 -5)")
+      val point2 = wktReader.read("POINT Z (1 1 -6)")
+      val pointDf = Seq(Tuple2(point1, point2)).toDF("p1", "p2")
+      pointDf.createOrReplaceTempView("pointdf")
+      var functionDf = sparkSession.sql("select ST_3DDistance(p1, p2) from pointdf")
+      val expected = Distance3DOp.distance(point1, point2)
+      assert(functionDf.take(1)(0).get(0).asInstanceOf[Double].equals(expected))
+    }
+
     it("Passed ST_Transform") {
       var polygonWktDf = sparkSession.read.format("csv").option("delimiter", "\t").option("header", "false").load(mixedWktGeometryInputLocation)
       polygonWktDf.createOrReplaceTempView("polygontable")
@@ -266,6 +278,26 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
       assert(polygonDf.take(1)(0).getAs[Geometry]("countyshape").toText.equals(wktDf.take(1)(0).getAs[String]("wkt")))
     }
 
+    it("Passed ST_AsText 3D") {
+      val geometryDf = Seq(
+        "Point Z(21 52 87)",
+        "Polygon Z((0 0 1, 0 1 1, 1 1 1, 1 0 1, 0 0 1))",
+        "Linestring Z(0 0 1, 1 1 2, 1 0 3)",
+        "MULTIPOINT Z((10 40 66), (40 30 77), (20 20 88), (30 10 99))",
+        "MULTIPOLYGON Z(((30 20 11, 45 40 11, 10 40 11, 30 20 11)), ((15 5 11, 40 10 11, 10 20 11, 5 10 11, 15 5 11)))",
+        "MULTILINESTRING Z((10 10 11, 20 20 11, 10 40 11), (40 40 11, 30 30 11, 40 20 11, 30 10 11))",
+        "MULTIPOLYGON Z(((40 40 11, 20 45 11, 45 30 11, 40 40 11)), ((20 35 11, 10 30 11, 10 10 11, 30 5 11, 45 20 11, 20 35 11), (30 20 11, 20 15 11, 20 25 11, 30 20 11)))",
+        "POLYGON Z((0 0 11, 0 5 11, 5 5 11, 5 0 11, 0 0 11), (1 1 11, 2 1 11, 2 2 11, 1 2 11, 1 1 11))"
+      ).map(wkt => Tuple1(wktReader.read(wkt))).toDF("geom")
+
+      geometryDf.createOrReplaceTempView("geometrytable")
+      var wktDf = sparkSession.sql("select ST_AsText(geom) as wkt from geometrytable")
+      val wktWriter = new WKTWriter(3)
+      val expected = geometryDf.collect().map(row => wktWriter.write(row.getAs[Geometry]("geom")))
+      val actual = wktDf.collect().map(row => row.getAs[String]("wkt"))
+      actual should contain theSameElementsAs expected
+    }
+
     it("Passed ST_AsGeoJSON") {
       val df = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON((1 1, 8 1, 8 8, 1 8, 1 1))') AS polygon")
       df.createOrReplaceTempView("table")
@@ -434,6 +466,54 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
       polygons.length shouldBe 0
 
     }
+
+    it("Should pass ST_Z") {
+
+      Given("Given polygon, point and linestring dataframe")
+      val pointDF =  Seq(
+        "POINT Z (1 2 3)"
+      ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
+      val polygonDF =  Seq(
+        "POLYGON Z ((0 0 2, 0 1 2, 1 1 2, 1 0 2, 0 0 2))"
+      ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
+      val lineStringDF =  Seq(
+        "LINESTRING Z (0 0 1, 0 1 2)"
+      ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
+
+      When("Running ST_Z function on polygon, point and linestring data frames")
+
+      val points = pointDF
+        .selectExpr("ST_Z(geom) as z")
+        .as[Double]
+        .collect()
+        .toList
+
+      val polygons = polygonDF
+        .selectExpr("ST_Z(geom) as z")
+        .filter("z IS NOT NULL")
+        .as[Double]
+        .collect()
+        .toList
+
+      val linestrings = lineStringDF
+        .selectExpr("ST_Z(geom) as z")
+        .filter("z IS NOT NULL")
+        .as[Double]
+        .collect()
+        .toList
+
+      Then("Point z coordinates Should match to expected point coordinates")
+
+      points should contain theSameElementsAs List(3)
+
+      And("LineString count should be 0")
+      linestrings.length shouldBe 0
+
+      And("Polygon count should be 0")
+      polygons.length shouldBe 0
+
+    }
+
     it("Should pass ST_StartPoint function") {
       Given("Polygon Data Frame, Point DataFrame, LineString Data Frame")
 
@@ -515,7 +595,7 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
   it("Should pass ST_ExteriorRing") {
     Given("Polygon DataFrame and other geometries DataFrame")
     val polygonDf = createSimplePolygons(5, "geom")
-      .union(Seq("POLYGON((0 0 1, 1 1 1, 1 2 1, 1 1 1, 0 0 1))")
+      .union(Seq("POLYGON((0 0, 1 1, 1 2, 1 1, 0 0))")
         .map(wkt => Tuple1(wktReader.read(wkt))).toDF("geom")
       )
 
@@ -1067,7 +1147,7 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
     Given("Sample geometry dataframe")
     val geometryTable = Seq(
       "LINESTRING(25 50, 100 125, 150 190)",
-      "LINESTRING(1 2 3, 4 5 6, 6 7 8)"
+      "LINESTRING(1 2, 4 5, 6 7)"
     ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
 
     When("Using ST_LineInterpolatePoint")