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/03/12 09:06:13 UTC

[incubator-sedona] branch master updated: [SEDONA-24] Add LineString interpolation functions (#514)

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 f74d057  [SEDONA-24] Add LineString interpolation functions (#514)
f74d057 is described below

commit f74d057364e81ffb3d40c044330b8d598880be2a
Author: Tongxing Ren <rt...@gmail.com>
AuthorDate: Fri Mar 12 10:06:07 2021 +0100

    [SEDONA-24] Add LineString interpolation functions (#514)
    
    * Add ST_LineSubString and ST_LineInterpolatePoint functions
    
    * Adapt Format in Doc
    
    * Fix Typo
    
    - change ST_LineSubString to ST_LineSubstring in order to match PostGIS function
    
    Co-authored-by: Tongxing.Ren <to...@daimler.com>
---
 docs/api/sql/GeoSparkSQL-Function.md               | 29 ++++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  4 +-
 .../sql/sedona_sql/expressions/Functions.scala     | 83 ++++++++++++++++++++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 51 +++++++++++++
 4 files changed, 166 insertions(+), 1 deletion(-)

diff --git a/docs/api/sql/GeoSparkSQL-Function.md b/docs/api/sql/GeoSparkSQL-Function.md
index d466191..b33d32a 100644
--- a/docs/api/sql/GeoSparkSQL-Function.md
+++ b/docs/api/sql/GeoSparkSQL-Function.md
@@ -612,3 +612,32 @@ Spark SQL example:
 ```SQL
 SELECT ST_MinimumBoundingCircle(ST_GeomFromText('POLYGON((1 1,0 0, -1 1, 1 1))'))
 ```
+
+## ST_LineSubstring
+
+Return a linestring being a substring of the input one starting and ending at the given fractions of total 2d length. Second and third arguments are Double values between 0 and 1. This only works with LINESTRINGs.
+
+Format: `ST_LineSubstring(geom: LineString, startFraction: Double, endFraction: Double) `
+
+Since: `v1.0.1`
+
+Spark SQL example:
+```SQL
+SELECT ST_LineSubstring(df.geometry, 0.333, 0.666)
+FROM df
+```
+
+
+## ST_LineInterpolatePoint
+
+Returns a point interpolated along a line. First argument must be a LINESTRING. Second argument is a Double between 0 and 1 representing fraction of total linestring length the point has to be located.
+
+Format: `ST_LineInterpolatePoint(geom: LineString, fraction: Double) `
+
+Since: `v1.0.1`
+
+Spark SQL example:
+```SQL
+SELECT ST_LineInterpolatePoint(df.geometry, 0.5)
+FROM 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 01d18e5..31e7112 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
@@ -79,7 +79,9 @@ object Catalog {
     ST_AddPoint,
     ST_RemovePoint,
     ST_IsRing,
-    ST_FlipCoordinates
+    ST_FlipCoordinates,
+    ST_LineSubstring,
+    ST_LineInterpolatePoint
   )
 
   val aggregateExpressions: Seq[Aggregator[Geometry, Geometry, Geometry]] = Seq(
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 8d80c08..ec2afea 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
@@ -41,6 +41,7 @@ import org.locationtech.jts.operation.linemerge.LineMerger
 import org.locationtech.jts.operation.valid.IsValidOp
 import org.locationtech.jts.precision.GeometryPrecisionReducer
 import org.locationtech.jts.simplify.TopologyPreservingSimplifier
+import org.locationtech.jts.linearref.LengthIndexedLine
 import org.opengis.referencing.operation.MathTransform
 
 import java.util
@@ -727,6 +728,88 @@ case class ST_MinimumBoundingCircle(inputExpressions: Seq[Expression])
 }
 
 
+/**
+ * Return a linestring being a substring of the input one starting and ending at the given fractions of total 2d length.
+ * Second and third arguments are Double values between 0 and 1. This only works with LINESTRINGs.
+ *
+ * @param inputExpressions
+ */
+case class ST_LineSubstring(inputExpressions: Seq[Expression])
+  extends Expression with CodegenFallback {
+
+  override def nullable: Boolean = true
+
+  override def eval(input: InternalRow): Any = {
+    inputExpressions.validateLength(3)
+
+    val geometry = inputExpressions.head.toGeometry(input)
+    val fractions = inputExpressions.slice(1, 3).map{
+      x => x.eval(input) match {
+        case a: Decimal => a.toDouble
+        case a: Double => a
+        case a: Int => a
+      }
+    }
+
+    (geometry, fractions) match {
+      case (g:LineString, r:Seq[Double]) if r.head >= 0 && r.last <= 1 && r.last >= r.head => getLineSubstring(g, r)
+      case _ => null
+    }
+  }
+
+  private def getLineSubstring(geom: Geometry, fractions: Seq[Double]): Any = {
+    val length = geom.getLength()
+    val indexedLine = new LengthIndexedLine(geom)
+    val subLine = indexedLine.extractLine(length * fractions.head, length * fractions.last)
+    subLine.toGenericArrayData
+  }
+
+  override def dataType: DataType = GeometryUDT
+
+  override def children: Seq[Expression] = inputExpressions
+}
+
+/**
+ * Returns a point interpolated along a line. First argument must be a LINESTRING.
+ * Second argument is a Double between 0 and 1 representing fraction of
+ * total linestring length the point has to be located.
+ *
+ * @param inputExpressions
+ */
+case class ST_LineInterpolatePoint(inputExpressions: Seq[Expression])
+  extends Expression with CodegenFallback {
+
+  override def nullable: Boolean = true
+
+  override def eval(input: InternalRow): Any = {
+    inputExpressions.validateLength(2)
+
+    val geometry = inputExpressions.head.toGeometry(input)
+    val fraction: Double = inputExpressions.last.eval(input) match {
+      case a: Decimal => a.toDouble
+      case a: Double => a
+      case a: Int => a
+    }
+
+    (geometry, fraction) match {
+      case (g:LineString, f:Double) if f >= 0 && f <= 1 => getLineInterpolatePoint(g, f)
+      case _ => null
+    }
+  }
+
+  private def getLineInterpolatePoint(geom: Geometry, fraction: Double): Any = {
+    val length = geom.getLength()
+    val indexedLine = new LengthIndexedLine(geom)
+    val interPoint = indexedLine.extractPoint(length * fraction)
+    new GeometryFactory().createPoint(interPoint).toGenericArrayData
+  }
+
+  override def dataType: DataType = GeometryUDT
+
+  override def children: Seq[Expression] = inputExpressions
+}
+
+
 case class ST_EndPoint(inputExpressions: Seq[Expression])
   extends Expression with CodegenFallback {
   override def nullable: Boolean = true
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 e8713af..6758831 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -25,6 +25,7 @@ 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
+import org.locationtech.jts.linearref.LengthIndexedLine
 import org.scalatest.{GivenWhenThen, Matchers}
 
 class functionTestScala extends TestBaseScala with Matchers with GeometrySample with GivenWhenThen {
@@ -907,4 +908,54 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
       .as[Double].collect().toList should contain theSameElementsAs List(0, 1, 1)
   }
 
+  it ("Should pass ST_LineSubstring") {
+    Given("Sample geometry dataframe")
+    val geometryTable = Seq(
+      "LINESTRING(25 50, 100 125, 150 190)"
+    ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
+
+    When("Using ST_LineSubstring")
+
+    val substringTable = geometryTable.selectExpr("ST_LineSubstring(geom, 0.333, 0.666) as subgeom")
+
+    Then("Result should match")
+
+    val lineString = geometryTable.collect()(0)(0).asInstanceOf[Geometry]
+    val indexedLineString = new LengthIndexedLine(lineString)
+    val substring = indexedLineString.extractLine(lineString.getLength() * 0.333, lineString.getLength() * 0.666)
+
+    substringTable.selectExpr("ST_AsText(subgeom)")
+      .as[String].collect() should contain theSameElementsAs
+      List(
+        substring.toText
+      )
+  }
+
+  it ("Should pass ST_LineInterpolatePoint") {
+    Given("Sample geometry dataframe")
+    val geometryTable = Seq(
+      "LINESTRING(25 50, 100 125, 150 190)",
+      "LINESTRING(1 2 3, 4 5 6, 6 7 8)"
+    ).map(geom => Tuple1(wktReader.read(geom))).toDF("geom")
+
+    When("Using ST_LineInterpolatePoint")
+
+    val interpolatedPointTable = geometryTable.selectExpr("ST_LineInterpolatePoint(geom, 0.50) as interpts")
+
+    Then("Result should match")
+
+    val lineString2D = geometryTable.collect()(0)(0).asInstanceOf[Geometry]
+    val lineString3D = geometryTable.collect()(1)(0).asInstanceOf[Geometry]
+
+    val interPoint2D = new LengthIndexedLine(lineString2D).extractPoint(lineString2D.getLength() * 0.5)
+    val interPoint3D = new LengthIndexedLine(lineString3D).extractPoint(lineString3D.getLength() * 0.5)
+
+    interpolatedPointTable.selectExpr("ST_AsText(interpts)")
+      .as[String].collect() should contain theSameElementsAs
+      List(
+        lineString2D.getFactory.createPoint(interPoint2D).toText,
+        lineString2D.getFactory.createPoint(interPoint3D).toText
+      )
+  }
+
 }