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
+ )
+ }
+
}