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 2022/04/24 05:08:47 UTC
[incubator-sedona] branch master updated: [SEDONA-113] Add ST_PointN to Apache Sedona (#621)
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 2e5c6ef6 [SEDONA-113] Add ST_PointN to Apache Sedona (#621)
2e5c6ef6 is described below
commit 2e5c6ef6e3cb64468a2986760b722aeea6ee03fd
Author: Pooja Kulkarni <pa...@gmail.com>
AuthorDate: Sat Apr 23 22:08:42 2022 -0700
[SEDONA-113] Add ST_PointN to Apache Sedona (#621)
Co-authored-by: Jia Yu <ji...@apache.org>
---
.../org/apache/sedona/core/utils/GeomUtils.java | 42 ++++++++++++++++++
docs/api/flink/Function.md | 50 +++++++++++++++++++++-
docs/api/sql/Function.md | 27 +++++++++++-
.../main/java/org/apache/sedona/flink/Catalog.java | 2 +
.../apache/sedona/flink/expressions/Functions.java | 23 ++++++++--
.../java/org/apache/sedona/flink/FunctionTest.java | 39 +++++++++++++++--
python/tests/sql/test_function.py | 23 +++++++++-
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 +
.../sql/sedona_sql/expressions/Functions.scala | 36 ++++++++++++++++
.../org/apache/sedona/sql/functionTestScala.scala | 35 ++++++++++++++-
10 files changed, 266 insertions(+), 12 deletions(-)
diff --git a/core/src/main/java/org/apache/sedona/core/utils/GeomUtils.java b/core/src/main/java/org/apache/sedona/core/utils/GeomUtils.java
index 145c6bbd..edc7c3c0 100644
--- a/core/src/main/java/org/apache/sedona/core/utils/GeomUtils.java
+++ b/core/src/main/java/org/apache/sedona/core/utils/GeomUtils.java
@@ -14,6 +14,11 @@
package org.apache.sedona.core.utils;
import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.CoordinateSequenceFilter;
+import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKTWriter;
import java.util.Objects;
@@ -81,6 +86,43 @@ public class GeomUtils
return geometry.getInteriorPoint();
}
+ /**
+ * Return the nth point from the given geometry (which could be a linestring or a circular linestring)
+ * If the value of n is negative, return a point backwards
+ * E.g. if n = 1, return 1st point, if n = -1, return last point
+ *
+ * @param lineString from which the nth point is to be returned
+ * @param n is the position of the point in the geometry
+ * @return a point
+ */
+ public static Geometry getNthPoint(LineString lineString, int n) {
+ if (lineString == null || n == 0) {
+ return null;
+ }
+
+ int p = lineString.getNumPoints();
+ if (Math.abs(n) > p) {
+ return null;
+ }
+
+ Coordinate[] nthCoordinate = new Coordinate[1];
+ if (n > 0) {
+ nthCoordinate[0] = lineString.getCoordinates()[n - 1];
+ } else {
+ nthCoordinate[0] = lineString.getCoordinates()[p + n];
+ }
+ return new Point(new CoordinateArraySequence(nthCoordinate), lineString.getFactory());
+ }
+
+ public static Geometry getExteriorRing(Geometry geometry) {
+ try {
+ Polygon polygon = (Polygon) geometry;
+ return polygon.getExteriorRing();
+ } catch(ClassCastException e) {
+ return null;
+ }
+ }
+
public static String getEWKT(Geometry geometry) {
if(geometry==null) {
return null;
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index cec8e180..bac2942f 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -195,6 +195,54 @@ Input: `POLYGON ((-0.5 -0.5, -0.5 0.5, 0.5 0.5, 0.5 -0.5, -0.5 -0.5))`
Output: `POLYGON ((-0.5 -0.5, 0.5 -0.5, 0.5 0.5, -0.5 0.5, -0.5 -0.5))`
+## ST_PointN
+
+Introduction: Return the Nth point in a single linestring or circular linestring in the geometry. Negative values are counted backwards from the end of the LineString, so that -1 is the last point. Returns NULL if there is no linestring in the geometry.
+
+Format: `ST_PointN(A:geometry, B:integer)`
+
+Since: `v1.2.1`
+
+Examples:
+
+```SQL
+SELECT ST_PointN(df.geometry, 2)
+FROM df
+```
+
+Input: `LINESTRING(0 0, 1 2, 2 4, 3 6), 2`
+
+Output: `POINT (1 2)`
+
+Input: `LINESTRING(0 0, 1 2, 2 4, 3 6), -2`
+
+Output: `POINT (2 4)`
+
+Input: `CIRCULARSTRING(1 1, 1 2, 2 4, 3 6, 1 2, 1 1), -1`
+
+Output: `POINT (1 1)`
+
+
+## ST_ExteriorRing
+
+Introduction: Returns a LINESTRING representing the exterior ring (shell) of a POLYGON. Returns NULL if the geometry is not a polygon.
+
+Format: `ST_ExteriorRing(A:geometry)`
+
+Since: `v1.2.1`
+
+Examples:
+
+```SQL
+SELECT ST_ExteriorRing(df.geometry)
+FROM df
+```
+
+Input: `POLYGON ((0 0, 1 1, 2 1, 0 1, 1 -1, 0 0))`
+
+Output: `LINESTRING (0 0, 1 1, 2 1, 0 1, 1 -1, 0 0)`
+
+
## ST_Force_2D
Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates
@@ -268,4 +316,4 @@ FROM df
Input: `POLYGON ((-1 -11, 0 10, 1 11, 2 12, -1 -11))`
-Output: `-1`
\ No newline at end of file
+Output: `-1`
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 1833b9dd..f4c35430 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1153,6 +1153,31 @@ Result:
+---------------------------------------------------------------+
```
+## ST_PointN
+
+Introduction: Return the Nth point in a single linestring or circular linestring in the geometry. Negative values are counted backwards from the end of the LineString, so that -1 is the last point. Returns NULL if there is no linestring in the geometry.
+
+Format: `ST_PointN(geom: geometry, n: integer)`
+
+Since: `v1.2.1`
+
+Spark SQL example:
+```SQL
+SELECT ST_PointN(ST_GeomFromText("LINESTRING(0 0, 1 2, 2 4, 3 6)"), 2) AS geom
+```
+
+Result:
+
+```
++---------------------------------------------------------------+
+|geom |
++---------------------------------------------------------------+
+|POINT (1 2) |
++---------------------------------------------------------------+
+```
+
+Result:
+=======
## ST_Force_2D
Introduction: Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates
@@ -1175,7 +1200,7 @@ Result:
+---------------------------------------------------------------+
|geom |
+---------------------------------------------------------------+
-|POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) |
+|POLYGON((0 0,0 5,5 0,0 0),(1 1,3 1,1 3,1 1)) |
+---------------------------------------------------------------+
```
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 22374d48..1c326c57 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -33,6 +33,8 @@ public class Catalog {
new Functions.ST_GeoHash(),
new Functions.ST_PointOnSurface(),
new Functions.ST_Reverse(),
+ new Functions.ST_PointN(),
+ new Functions.ST_ExteriorRing(),
new Functions.ST_AsEWKT(),
new Functions.ST_Force_2D(),
new Functions.ST_IsEmpty(),
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 0d9d4e4e..4d369e17 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
@@ -21,10 +21,8 @@ import org.apache.spark.sql.sedona_sql.expressions.geohash.GeometryGeoHashEncode
import org.apache.spark.sql.sedona_sql.expressions.geohash.PointGeoHashEncoder;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Coordinate;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -136,6 +134,25 @@ public class Functions {
}
}
+ public static class ST_PointN 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, int n) {
+ if(!(o instanceof LineString)) {
+ return null;
+ }
+ LineString lineString = (LineString) o;
+ return GeomUtils.getNthPoint(lineString, n);
+ }
+ }
+
+ public static class ST_ExteriorRing 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) {
+ Geometry geometry = (Geometry) o;
+ return GeomUtils.getExteriorRing(geometry);
+ }
+ }
+
public static class ST_AsEWKT extends ScalarFunction {
@DataTypeHint("String")
public String 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 c23f84a5..97176551 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -14,16 +14,17 @@
package org.apache.sedona.flink;
import org.apache.flink.table.api.Table;
-import org.apache.flink.types.Row;
import org.apache.sedona.flink.expressions.Constructors;
import org.apache.sedona.flink.expressions.Functions;
+import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
-import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
-import java.util.List;
+import java.util.Arrays;
import java.util.Optional;
import static org.apache.flink.table.api.Expressions.$;
@@ -106,6 +107,36 @@ public class FunctionTest extends TestBase{
}
@Test
+ public void testPointN_positiveN() {
+ int n = 1;
+ Table polygonTable = createPolygonTable(1);
+ Table linestringTable = polygonTable.select(call(Functions.ST_ExteriorRing.class.getSimpleName(), $(polygonColNames[0])));
+ Table pointTable = linestringTable.select(call(Functions.ST_PointN.class.getSimpleName(), $("_c0"), n));
+ Point point = (Point) first(pointTable).getField(0);
+ assert point != null;
+ Assert.assertEquals("POINT (-0.5 -0.5)", point.toString());
+ }
+
+ @Test
+ public void testPointN_negativeN() {
+ int n = -3;
+ Table polygonTable = createPolygonTable(1);
+ Table linestringTable = polygonTable.select(call(Functions.ST_ExteriorRing.class.getSimpleName(), $(polygonColNames[0])));
+ Table pointTable = linestringTable.select(call(Functions.ST_PointN.class.getSimpleName(), $("_c0"), n));
+ Point point = (Point) first(pointTable).getField(0);
+ assert point != null;
+ Assert.assertEquals("POINT (0.5 0.5)", point.toString());
+ }
+
+ @Test
+ public void testExteriorRing() {
+ Table polygonTable = createPolygonTable(1);
+ Table linearRingTable = polygonTable.select(call(Functions.ST_ExteriorRing.class.getSimpleName(), $(polygonColNames[0])));
+ LinearRing linearRing = (LinearRing) first(linearRingTable).getField(0);
+ assert linearRing != null;
+ Assert.assertEquals("LINEARRING (-0.5 -0.5, -0.5 0.5, 0.5 0.5, 0.5 -0.5, -0.5 -0.5)", linearRing.toString());
+ }
+
public void testAsEWKT() {
Table polygonTable = createPolygonTable(testDataSize);
polygonTable = polygonTable.select(call(Functions.ST_AsEWKT.class.getSimpleName(), $(polygonColNames[0])));
diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py
index adc5259c..bfa7477b 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -944,7 +944,7 @@ class TestPredicateJoin(TestBase):
"'POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))'":"POINT (2.5 2.5)",
"'LINESTRING(0 5 1, 0 0 1, 0 10 2)'":"POINT Z(0 0 1)"
}
-
+
for input_geom, expected_geom in tests1.items():
pointOnSurface = self.spark.sql("select ST_AsText(ST_PointOnSurface(ST_GeomFromText({})))".format(input_geom))
assert pointOnSurface.take(1)[0][0] == expected_geom
@@ -958,6 +958,25 @@ class TestPredicateJoin(TestBase):
assert pointOnSurface.take(1)[0][0] == expected_geom
'''
+ def test_st_pointn(self):
+ linestring = "'LINESTRING(0 0, 1 2, 2 4, 3 6)'"
+ tests = [
+ [linestring, 1, "POINT (0 0)"],
+ [linestring, 2, "POINT (1 2)"],
+ [linestring, -1, "POINT (3 6)"],
+ [linestring, -2, "POINT (2 4)"],
+ [linestring, 3, "POINT (2 4)"],
+ [linestring, 4, "POINT (3 6)"],
+ [linestring, 5, None],
+ [linestring, -5, None],
+ ["'POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))'", 2, None],
+ ["'POINT(1 2)'", 1, None]
+ ]
+
+ for test in tests:
+ point = self.spark.sql(f"select ST_AsText(ST_PointN(ST_GeomFromText({test[0]}), {test[1]}))")
+ assert point.take(1)[0][0] == test[2]
+
def test_st_force2d(self):
tests1 = {
"'POINT(0 5)'": "POINT (0 5)",
@@ -969,4 +988,4 @@ class TestPredicateJoin(TestBase):
for input_geom, expected_geom in tests1.items():
geom_2d = self.spark.sql(
"select ST_AsText(ST_Force_2D(ST_GeomFromText({})))".format(input_geom))
- assert geom_2d.take(1)[0][0] == expected_geom
\ No newline at end of file
+ assert geom_2d.take(1)[0][0] == expected_geom
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 db112709..eba06450 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
@@ -106,6 +106,7 @@ object Catalog {
ST_Multi,
ST_PointOnSurface,
ST_Reverse,
+ ST_PointN,
ST_AsEWKT,
ST_Force_2D,
ST_YMax,
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 4612465f..919e3385 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
@@ -1620,6 +1620,42 @@ case class ST_Reverse(inputExpressions: Seq[Expression])
}
/**
+ * Returns the nth point in the geometry, provided it is a linestring
+ *
+ * @param inputExpressions sequence of 2 input arguments, a geometry and a value 'n'
+ */
+case class ST_PointN(inputExpressions: Seq[Expression])
+ extends Expression with CodegenFallback {
+ inputExpressions.validateLength(2)
+
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ val geometry = inputExpressions.head.toGeometry(input)
+ val n = inputExpressions(1).toInt(input)
+ getNthPoint(geometry, n)
+ }
+
+ private def getNthPoint(geometry: Geometry, n: Int): GenericArrayData = {
+ geometry match {
+ case linestring: LineString => val point = GeomUtils.getNthPoint(linestring, n)
+ point match {
+ case geometry: Geometry => new GenericArrayData(GeometrySerializer.serialize(geometry))
+ case _ => null
+ }
+ case _ => null
+ }
+ }
+ override def dataType: DataType = GeometryUDT
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
+ copy(inputExpressions = newChildren)
+ }
+}
+
+ /*
* Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates.
*
* @param inputExpressions
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 397a1f7b..2831f77e 100644
--- a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -1296,7 +1296,7 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
}
/* ST_AsEWKT Has not been implemented yet
-
+
val geomTestCases2 = Map(
"'LINESTRING(0 5 1, 0 0 1, 0 10 2)'"
-> "POINT (0 0 1)"
@@ -1346,6 +1346,39 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
}
}
+ it("Should pass ST_PointN") {
+
+ Given("Some different types of geometries in a DF")
+
+ val sampleLineString = "LINESTRING(0 0, 1 2, 2 4, 3 6)"
+ val testData = Seq(
+ (sampleLineString, 1),
+ (sampleLineString, 2),
+ (sampleLineString, -1),
+ (sampleLineString, -2),
+ (sampleLineString, 3),
+ (sampleLineString, 4),
+ (sampleLineString, 5),
+ (sampleLineString, -5),
+ ("POLYGON((-1 0 0, 1 0 0, 0 0 1, 0 1 0, -1 0 0))", 2),
+ ("POINT(1 2)", 1)
+ ).toDF("Geometry", "N")
+
+ When("Using ST_PointN for getting the nth point in linestring type of Geometries")
+
+ val testDF = testData.selectExpr("ST_PointN(ST_GeomFromText(Geometry), N) as geom")
+
+ Then("Result should match the list of nth points")
+
+ testDF.selectExpr("ST_AsText(geom)")
+ .as[String].collect() should contain theSameElementsAs
+ List(
+ "POINT (0 0)", "POINT (1 2)", "POINT (3 6)",
+ "POINT (2 4)", "POINT (2 4)", "POINT (3 6)",
+ null, null, null, null
+ )
+ }
+
it ("Should pass ST_AsEWKT") {
var df = sparkSession.sql("SELECT ST_SetSrid(ST_GeomFromWKT('POLYGON((0 0,0 1,1 1,1 0,0 0))'), 4326) as point")
df.createOrReplaceTempView("table")