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/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new f265c9e5af [GH-2272] add new constructor ST_GeogFromGeoHash (#2271)
f265c9e5af is described below
commit f265c9e5af08a3043421285ff9cdfb6ee55e1127
Author: Zhuocheng Shang <[email protected]>
AuthorDate: Thu Aug 14 11:45:52 2025 -0700
[GH-2272] add new constructor ST_GeogFromGeoHash (#2271)
* add new constructor ST_GeogFromGeoHash
* fix constructor api doc
* fix en-of-file in api doc
* fix SRID output for ST_GeogGeoHash
* add back ST_GeogFromWKT header
* resolve conflicts
* Update docs/api/sql/geography/Constructor.md
---------
Co-authored-by: Jia Yu <[email protected]>
---
.../sedona/common/geography/Constructors.java | 12 ++++++++++
.../apache/sedona/common/utils/GeoHashDecoder.java | 26 ++++++++++++++++++++++
.../sedona/common/Geography/ConstructorsTest.java | 16 +++++++++++++
docs/api/sql/geography/Constructor.md | 22 ++++++++++++++++++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 3 +++
.../expressions/geography/Constructors.scala | 15 ++++++++++++-
.../sedona_sql/expressions/st_constructors.scala | 12 ++++++++++
.../geography/ConstructorsDataFrameAPITest.scala | 11 +++++++++
.../sedona/sql/geography/ConstructorsTest.scala | 21 +++++++++++++++++
9 files changed, 137 insertions(+), 1 deletion(-)
diff --git
a/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
b/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
index 5f70b1daca..6efbd8e0b6 100644
--- a/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
+++ b/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
@@ -18,9 +18,11 @@
*/
package org.apache.sedona.common.geography;
+import java.io.IOException;
import org.apache.sedona.common.S2Geography.Geography;
import org.apache.sedona.common.S2Geography.WKBReader;
import org.apache.sedona.common.S2Geography.WKTReader;
+import org.apache.sedona.common.utils.GeoHashDecoder;
import org.locationtech.jts.io.ParseException;
public class Constructors {
@@ -67,4 +69,14 @@ public class Constructors {
}
return geogFromWKT(wkt, srid);
}
+
+ public static Geography geogFromGeoHash(String geoHash, Integer precision) {
+ try {
+ return GeoHashDecoder.decodeGeog(geoHash, precision);
+ } catch (GeoHashDecoder.InvalidGeoHashException e) {
+ return null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git
a/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
b/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
index 7662cd8fb9..b28070310c 100644
--- a/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
+++ b/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
@@ -18,6 +18,11 @@
*/
package org.apache.sedona.common.utils;
+import com.google.common.geometry.*;
+import java.io.IOException;
+import java.util.List;
+import org.apache.sedona.common.S2Geography.Geography;
+import org.apache.sedona.common.S2Geography.PolygonGeography;
import org.locationtech.jts.geom.Geometry;
public class GeoHashDecoder {
@@ -34,6 +39,27 @@ public class GeoHashDecoder {
return decodeGeoHashBBox(geohash, precision).getBbox().toPolygon();
}
+ public static Geography decodeGeog(String geohash, Integer precision)
+ throws InvalidGeoHashException, IOException {
+ BBox box = decodeGeoHashBBox(geohash, precision).getBbox(); //
west,east,south,north
+ double south = box.startLat, north = box.endLat;
+ double west = box.startLon, east = box.endLon;
+
+ // 4 corners (lat, lon) in CCW order: SW → SE → NE → NW
+ S2Point SW = S2LatLng.fromDegrees(south, west).toPoint();
+ S2Point SE = S2LatLng.fromDegrees(south, east).toPoint();
+ S2Point NE = S2LatLng.fromDegrees(north, east).toPoint();
+ S2Point NW = S2LatLng.fromDegrees(north, west).toPoint();
+
+ S2Loop shell = new S2Loop(List.of(SW, SE, NE, NW));
+ shell.normalize(); // ensure CCW & smaller-area orientation (safe even if
already CCW)
+
+ S2Polygon s2poly = new S2Polygon(shell);
+ Geography geog = new PolygonGeography(s2poly);
+ geog.setSRID(4326);
+ return geog;
+ }
+
private static class LatLon {
public Double[] lons;
diff --git
a/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
b/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
index 4d2075920e..8198500bb7 100644
---
a/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
+++
b/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
@@ -28,6 +28,7 @@ import org.apache.sedona.common.S2Geography.WKBReader;
import org.apache.sedona.common.S2Geography.WKBWriter;
import org.apache.sedona.common.geography.Constructors;
import org.junit.Test;
+import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
public class ConstructorsTest {
@@ -123,4 +124,19 @@ public class ConstructorsTest {
assertEquals(expectedGeom, result.toString());
assertEquals(4326, result.getSRID());
}
+
+ @Test
+ public void geogFromGeoHash() throws ParseException {
+ Geography geog = Constructors.geogFromGeoHash("9q9j8ue2v71y5zzy0s4q", 16);
+ String expectedWkt =
+ "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162,
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))";
+ String actualWkt = geog.toText(new PrecisionModel(1e6));
+ assertEquals(expectedWkt, actualWkt);
+
+ geog = Constructors.geogFromGeoHash("s00twy01mt", 4);
+ expectedWkt =
+ "SRID=4326; POLYGON ((0.703125 0.8789062, 1.0546875 0.8789062,
1.0546875 1.0546875, 0.703125 1.0546875, 0.703125 0.8789062))";
+ actualWkt = geog.toText(new PrecisionModel(1e6));
+ assertEquals(expectedWkt, actualWkt);
+ }
}
diff --git a/docs/api/sql/geography/Constructor.md
b/docs/api/sql/geography/Constructor.md
index 8cf2d9b74d..bd5577c8f4 100644
--- a/docs/api/sql/geography/Constructor.md
+++ b/docs/api/sql/geography/Constructor.md
@@ -61,6 +61,28 @@ Output:
SRID: 4326; LINESTRING (-2.1 -0.4, -1.5 -0.7)
```
+## ST_GeogFromGeoHash
+
+Introduction: Create Geography from geohash string and optional precision
+
+Format:
+
+`ST_GeogFromGeoHash(geohash: String, precision: Integer)`
+
+Since: `v1.8.0`
+
+SQL Example
+
+```sql
+SELECT ST_GeogFromGeoHash('9q9j8ue2v71y5zzy0s4q', 16)
+```
+
+Output:
+
+```
+SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162, -122.3061
37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+```
+
## ST_GeogFromWKT
Introduction: Construct a Geography from WKT. If SRID is not set, it defaults
to 0 (unknown).
diff --git
a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 4b948c63a5..074db478d0 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -22,6 +22,8 @@ import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.sedona_sql.expressions.collect.ST_Collect
import org.apache.spark.sql.sedona_sql.expressions.raster._
import org.apache.spark.sql.sedona_sql.expressions._
+import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromGeoHash, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
+import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKB, ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB,
ST_GeogFromWKT}
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.operation.buffer.BufferParameters
@@ -188,6 +190,7 @@ object Catalog extends AbstractCatalog {
function[ST_GeoHash](),
function[ST_GeomFromGeoHash](null),
function[ST_PointFromGeoHash](null),
+ function[ST_GeogFromGeoHash](null),
function[ST_Collect](),
function[ST_Multi](),
function[ST_PointOnSurface](),
diff --git
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
index 13eb40ee81..ad339c146b 100644
---
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
+++
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
@@ -21,7 +21,7 @@ package org.apache.spark.sql.sedona_sql.expressions.geography
import org.apache.sedona.common.geography.Constructors
import org.apache.spark.sql.catalyst.expressions.Expression
import
org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._
-import org.apache.spark.sql.sedona_sql.expressions.InferredExpression
+import org.apache.spark.sql.sedona_sql.expressions.{InferrableFunction,
InferredExpression}
/**
* Return a Geography from a WKT string
@@ -108,3 +108,16 @@ private[apache] case class
ST_GeogFromEWKB(inputExpressions: Seq[Expression])
copy(inputExpressions = newChildren)
}
}
+
+/**
+ * Return a Geography from a GeoHash string
+ *
+ * @param inputExpressions
+ * This function takes a geography geohash
+ */
+private[apache] case class ST_GeogFromGeoHash(inputExpressions:
Seq[Expression])
+ extends
InferredExpression(InferrableFunction.allowRightNull(Constructors.geogFromGeoHash))
{
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+}
diff --git
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
index 7dcfa2b115..38a5469cf4 100644
---
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
+++
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
@@ -22,6 +22,7 @@ import org.apache.spark.sql.Column
import org.apache.spark.sql.sedona_sql.DataFrameShims._
import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKB, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
+import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromGeoHash, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
object st_constructors {
def ST_GeomFromGeoHash(geohash: Column, precision: Column): Column =
@@ -35,6 +36,17 @@ object st_constructors {
def ST_GeomFromGeoHash(geohash: String): Column =
wrapExpression[ST_GeomFromGeoHash](geohash, null)
+ def ST_GeogFromGeoHash(geohash: Column, precision: Column): Column =
+ wrapExpression[ST_GeogFromGeoHash](geohash, precision)
+ def ST_GeogFromGeoHash(geohash: String, precision: Int): Column =
+ wrapExpression[ST_GeogFromGeoHash](geohash, precision)
+
+ def ST_GeogFromGeoHash(geohash: Column): Column =
+ wrapExpression[ST_GeogFromGeoHash](geohash, null)
+
+ def ST_GeogFromGeoHash(geohash: String): Column =
+ wrapExpression[ST_GeogFromGeoHash](geohash, null)
+
def ST_PointFromGeoHash(geohash: Column, precision: Column): Column =
wrapExpression[ST_PointFromGeoHash](geohash, precision)
def ST_PointFromGeoHash(geohash: String, precision: Int): Column =
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
index 24625e9445..9417e4eed8 100644
---
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
+++
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
@@ -85,4 +85,15 @@ class ConstructorsDataFrameAPITest extends TestBaseScala {
assert(actualResult.getSRID == 4269)
}
+ it("Passed ST_GeogFromGeoHash") {
+ val df = sparkSession
+ .sql("SELECT '9q9j8ue2v71y5zzy0s4q' AS geohash")
+ .select(st_constructors.ST_GeogFromGeoHash("geohash", 16))
+ val actualResult =
+ df.take(1)(0).get(0).asInstanceOf[Geography].toText(new
PrecisionModel(1e6))
+ var expectedWkt =
+ "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162,
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+ assertEquals(expectedWkt, actualResult)
+ }
+
}
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
index 6f4a9b376d..092355a691 100644
---
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
+++
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
@@ -19,7 +19,9 @@
package org.apache.sedona.sql.geography
import org.apache.sedona.common.S2Geography.Geography
+import org.apache.sedona.common.geography.Constructors
import org.apache.sedona.sql.TestBaseScala
+import org.junit.Assert.assertEquals
import org.locationtech.jts.geom.PrecisionModel
class ConstructorsTest extends TestBaseScala {
@@ -27,6 +29,25 @@ class ConstructorsTest extends TestBaseScala {
import sparkSession.implicits._
val precisionModel: PrecisionModel = new
PrecisionModel(PrecisionModel.FIXED);
+ it("Passed ST_GeogFromGeoHash") {
+ var geohash = "9q9j8ue2v71y5zzy0s4q";
+ var precision = 16;
+ var row =
+ sparkSession.sql(s"SELECT ST_GeogFromGeoHash('$geohash','$precision') AS
geog").first()
+ var geoStr = row.get(0).asInstanceOf[Geography].toText(new
PrecisionModel(1e6))
+ var expectedWkt =
+ "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162,
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+ assertEquals(expectedWkt, geoStr)
+
+ geohash = "s00twy01mt"
+ precision = 4;
+ row = sparkSession.sql(s"SELECT
ST_GeogFromGeoHash('$geohash','$precision') AS geog").first()
+ geoStr = row.get(0).asInstanceOf[Geography].toText(new PrecisionModel(1e6))
+ expectedWkt =
+ "SRID=4326; POLYGON ((0.703125 0.8789062, 1.0546875 0.8789062, 1.0546875
1.0546875, 0.703125 1.0546875, 0.703125 0.8789062))"
+ assertEquals(expectedWkt, geoStr)
+ }
+
it("Passed ST_GeogFromWKT") {
val wkt = "LINESTRING (1 2, 3 4, 5 6)"
val wktExpected = "SRID=4326; LINESTRING (1 2, 3 4, 5 6)"