From cdb6d807485b25420ad37328234d0942fb2478c3 Mon Sep 17 00:00:00 2001 From: zhangfengcdt Date: Wed, 22 Apr 2026 08:02:16 -0700 Subject: [PATCH 1/2] Add Geography dual-dispatch to ST_AsText MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - L1 — JTS delegation via getJTSGeometry().toText() - 1 new Spark SQL test - update document --- .../sedona/common/geography/Functions.java | 6 +++ .../sedona/common/Geography/FunctionTest.java | 21 ++++++++++ docs/api/sql/geography/Geography-Functions.md | 1 + .../Geography-Functions/ST_AsText.md | 42 +++++++++++++++++++ .../sedona_sql/expressions/Functions.scala | 4 +- .../sql/geography/GeographyFunctionTest.scala | 11 ++++- 6 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 docs/api/sql/geography/Geography-Functions/ST_AsText.md diff --git a/common/src/main/java/org/apache/sedona/common/geography/Functions.java b/common/src/main/java/org/apache/sedona/common/geography/Functions.java index e3166fa9e0f..d6a0b407b1e 100644 --- a/common/src/main/java/org/apache/sedona/common/geography/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/geography/Functions.java @@ -85,6 +85,12 @@ public static int nPoints(Geography g) { return toJTS(g).getNumPoints(); } + /** Return the WKT text representation of a geography. */ + public static String asText(Geography g) { + if (g == null) return null; + return toJTS(g).toText(); + } + // ─── Level 2: JTS + S2 geodesic metrics ────────────────────────────────── /** diff --git a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java index 6787c5ad876..06e3db4ffc0 100644 --- a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java +++ b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java @@ -144,6 +144,27 @@ public void nPoints_polygon() throws ParseException { assertEquals(5, Functions.nPoints(g)); } + @Test + public void asText_point() throws ParseException { + Geography g = Constructors.geogFromWKT("POINT (1 2)", 4326); + String wkt = Functions.asText(g); + assertNotNull(wkt); + assertTrue("Expected POINT, got: " + wkt, wkt.startsWith("POINT (")); + } + + @Test + public void asText_polygon() throws ParseException { + Geography g = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326); + String wkt = Functions.asText(g); + assertNotNull(wkt); + assertTrue("Expected POLYGON, got: " + wkt, wkt.startsWith("POLYGON (")); + } + + @Test + public void asText_nullHandling() { + assertNull(Functions.asText(null)); + } + // ─── Level 2: ST_Distance ──────────────────────────────────────────────── @Test diff --git a/docs/api/sql/geography/Geography-Functions.md b/docs/api/sql/geography/Geography-Functions.md index 7648b0c0a1e..8d90a2e4cb3 100644 --- a/docs/api/sql/geography/Geography-Functions.md +++ b/docs/api/sql/geography/Geography-Functions.md @@ -42,6 +42,7 @@ These functions operate on geography type objects. | Function | Return type | Description | Since | | :--- | :--- | :--- | :--- | | [ST_AsEWKT](Geography-Functions/ST_AsEWKT.md) | String | Return the Extended Well-Known Text representation of a geography. | v1.8.0 | +| [ST_AsText](Geography-Functions/ST_AsText.md) | String | Return the Well-Known Text (WKT) representation of a geography. | v1.9.0 | | [ST_Envelope](Geography-Functions/ST_Envelope.md) | Geography | Return the bounding box (envelope) of a geography. Supports anti-meridian splitting. | v1.8.0 | | [ST_NPoints](Geography-Functions/ST_NPoints.md) | Integer | Return the number of points (vertices) in a geography. | v1.9.0 | | [ST_Distance](Geography-Functions/ST_Distance.md) | Double | Return the minimum geodesic distance between two geographies in meters. | v1.9.0 | diff --git a/docs/api/sql/geography/Geography-Functions/ST_AsText.md b/docs/api/sql/geography/Geography-Functions/ST_AsText.md new file mode 100644 index 00000000000..ed82704a3b5 --- /dev/null +++ b/docs/api/sql/geography/Geography-Functions/ST_AsText.md @@ -0,0 +1,42 @@ + + +# ST_AsText + +Introduction: Returns the Well-Known Text (WKT) representation of a geography object. + +Format: + +`ST_AsText (A: Geography)` + +Return type: `String` + +Since: `v1.9.0` + +SQL Example + +```sql +SELECT ST_AsText(ST_GeogFromWKT('POINT (1 2)')); +``` + +Output: + +``` +POINT (1 2) +``` diff --git a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala index 839ec2140f8..66f01a6ccf7 100644 --- a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala +++ b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala @@ -571,7 +571,9 @@ private[apache] case class ST_SimplifyPolygonHull(inputExpressions: Seq[Expressi } private[apache] case class ST_AsText(inputExpressions: Seq[Expression]) - extends InferredExpression(Functions.asWKT _) { + extends InferredExpression( + inferrableFunction1(Functions.asWKT), + inferrableFunction1(org.apache.sedona.common.geography.Functions.asText)) { protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { copy(inputExpressions = newChildren) diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala index 7fe76730d27..be181dc3a86 100644 --- a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala +++ b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala @@ -77,7 +77,7 @@ class GeographyFunctionTest extends TestBaseScala { } } - // ─── Level 1: ST_NPoints ─────────────────────────────────────────────── + // ─── Level 1: ST_NPoints, ST_AsText ──────────────────────────────────── describe("Level 1: Structural") { @@ -87,6 +87,15 @@ class GeographyFunctionTest extends TestBaseScala { .first() assertEquals(3, row.getInt(0)) } + + it("ST_AsText") { + val row = sparkSession + .sql("SELECT ST_AsText(ST_GeogFromWKT('POINT (1 2)', 4326)) AS wkt") + .first() + val wkt = row.getString(0) + // S2 round-trip may introduce sub-nanometer floating-point drift + assertTrue(s"Expected POINT near (1, 2), got: $wkt", wkt.startsWith("POINT (")) + } } // ─── Level 2: ST_Distance ────────────────────────────────────────────── From 81e34cee717a07df92eeb4973f450f5decc73f27 Mon Sep 17 00:00:00 2001 From: zhangfengcdt Date: Wed, 22 Apr 2026 08:32:57 -0700 Subject: [PATCH 2/2] address copilot comments --- .../sedona/common/Geography/FunctionTest.java | 18 ++++++++++++++++-- docs/api/sql/geography/Geography-Functions.md | 2 +- .../geography/Geography-Functions/ST_AsText.md | 2 +- .../sql/geography/GeographyFunctionTest.scala | 9 ++++++--- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java index 06e3db4ffc0..ab2daca5df5 100644 --- a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java +++ b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java @@ -29,7 +29,11 @@ import org.apache.sedona.common.geography.Constructors; import org.apache.sedona.common.geography.Functions; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; public class FunctionTest { private static final double EPS = 1e-9; @@ -149,7 +153,10 @@ public void asText_point() throws ParseException { Geography g = Constructors.geogFromWKT("POINT (1 2)", 4326); String wkt = Functions.asText(g); assertNotNull(wkt); - assertTrue("Expected POINT, got: " + wkt, wkt.startsWith("POINT (")); + Point p = (Point) new WKTReader().read(wkt); + // S2 round-trip may introduce sub-nanometer floating-point drift; use a loose tolerance. + assertEquals(1.0, p.getX(), 1e-9); + assertEquals(2.0, p.getY(), 1e-9); } @Test @@ -157,7 +164,14 @@ public void asText_polygon() throws ParseException { Geography g = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326); String wkt = Functions.asText(g); assertNotNull(wkt); - assertTrue("Expected POLYGON, got: " + wkt, wkt.startsWith("POLYGON (")); + Polygon poly = (Polygon) new WKTReader().read(wkt); + Coordinate[] ring = poly.getExteriorRing().getCoordinates(); + assertEquals(5, ring.length); + double[][] expected = {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}; + for (int i = 0; i < expected.length; i++) { + assertEquals("ring[" + i + "].x", expected[i][0], ring[i].x, 1e-9); + assertEquals("ring[" + i + "].y", expected[i][1], ring[i].y, 1e-9); + } } @Test diff --git a/docs/api/sql/geography/Geography-Functions.md b/docs/api/sql/geography/Geography-Functions.md index 8d90a2e4cb3..86b5a12fb5f 100644 --- a/docs/api/sql/geography/Geography-Functions.md +++ b/docs/api/sql/geography/Geography-Functions.md @@ -42,7 +42,7 @@ These functions operate on geography type objects. | Function | Return type | Description | Since | | :--- | :--- | :--- | :--- | | [ST_AsEWKT](Geography-Functions/ST_AsEWKT.md) | String | Return the Extended Well-Known Text representation of a geography. | v1.8.0 | -| [ST_AsText](Geography-Functions/ST_AsText.md) | String | Return the Well-Known Text (WKT) representation of a geography. | v1.9.0 | +| [ST_AsText](Geography-Functions/ST_AsText.md) | String | Return the Well-Known Text (WKT) representation of a geography. | v1.9.1 | | [ST_Envelope](Geography-Functions/ST_Envelope.md) | Geography | Return the bounding box (envelope) of a geography. Supports anti-meridian splitting. | v1.8.0 | | [ST_NPoints](Geography-Functions/ST_NPoints.md) | Integer | Return the number of points (vertices) in a geography. | v1.9.0 | | [ST_Distance](Geography-Functions/ST_Distance.md) | Double | Return the minimum geodesic distance between two geographies in meters. | v1.9.0 | diff --git a/docs/api/sql/geography/Geography-Functions/ST_AsText.md b/docs/api/sql/geography/Geography-Functions/ST_AsText.md index ed82704a3b5..a00c06fc4d0 100644 --- a/docs/api/sql/geography/Geography-Functions/ST_AsText.md +++ b/docs/api/sql/geography/Geography-Functions/ST_AsText.md @@ -27,7 +27,7 @@ Format: Return type: `String` -Since: `v1.9.0` +Since: `v1.9.1` SQL Example diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala index be181dc3a86..7f47ae51826 100644 --- a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala +++ b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala @@ -23,7 +23,8 @@ import org.apache.sedona.sql.TestBaseScala import org.apache.spark.sql.functions.{col, lit} import org.apache.spark.sql.sedona_sql.expressions.{st_constructors, st_functions, st_predicates} import org.junit.Assert.{assertEquals, assertNotNull, assertTrue} -import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.{Geometry, Point} +import org.locationtech.jts.io.WKTReader /** * Spark SQL integration tests for Geography ST functions. Tests one representative function per @@ -93,8 +94,10 @@ class GeographyFunctionTest extends TestBaseScala { .sql("SELECT ST_AsText(ST_GeogFromWKT('POINT (1 2)', 4326)) AS wkt") .first() val wkt = row.getString(0) - // S2 round-trip may introduce sub-nanometer floating-point drift - assertTrue(s"Expected POINT near (1, 2), got: $wkt", wkt.startsWith("POINT (")) + val point = new WKTReader().read(wkt).asInstanceOf[Point] + // S2 round-trip may introduce sub-nanometer floating-point drift; use a loose tolerance. + assertEquals(1.0, point.getX, 1e-9) + assertEquals(2.0, point.getY, 1e-9) } }