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")