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 2c8e162a98 [GH-2276] new constructor of convert Geography to Geometry
(#2275)
2c8e162a98 is described below
commit 2c8e162a986d348f2b0bc95da632e0ba7e295ce3
Author: Zhuocheng Shang <[email protected]>
AuthorDate: Sat Aug 16 13:15:33 2025 -0700
[GH-2276] new constructor of convert Geography to Geometry (#2275)
* new constructor of convert Geography to Geometry
* fix S2Latlng from point to normalized
* add ST_GeogToGeometry SQL, dataframe integration
* fix SRID issue
* clean up ST_GeogToGeometry parameters
* Update
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
Co-authored-by: Copilot <[email protected]>
* Refine the error message
---------
Co-authored-by: Jia Yu <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Jia Yu <[email protected]>
---
.../sedona/common/geography/Constructors.java | 185 +++++++++++++++++++++
.../sedona/common/Geography/ConstructorsTest.java | 106 ++++++++++++
docs/api/sql/geography/Constructor.md | 22 +++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 7 +-
.../expressions/geography/Constructors.scala | 16 ++
.../sedona_sql/expressions/st_constructors.scala | 9 +-
.../geography/ConstructorsDataFrameAPITest.scala | 39 ++++-
.../sedona/sql/geography/ConstructorsTest.scala | 67 +++++++-
8 files changed, 438 insertions(+), 13 deletions(-)
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 6efbd8e0b6..7df27d18b0 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,11 +18,15 @@
*/
package org.apache.sedona.common.geography;
+import com.google.common.geometry.*;
import java.io.IOException;
+import java.util.*;
+import org.apache.sedona.common.S2Geography.*;
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.geom.*;
import org.locationtech.jts.io.ParseException;
public class Constructors {
@@ -79,4 +83,185 @@ public class Constructors {
throw new RuntimeException(e);
}
}
+
+ public static Geometry geogToGeometry(Geography geography) {
+ GeometryFactory geometryFactory =
+ new GeometryFactory(new PrecisionModel(), geography.getSRID());
+ return geogToGeometry(geography, geometryFactory);
+ }
+
+ public static Geometry geogToGeometry(Geography geography, GeometryFactory
geometryFactory) {
+ if (geography == null) return null;
+ Geography.GeographyKind kind =
Geography.GeographyKind.fromKind(geography.getKind());
+ switch (kind) {
+ case SINGLEPOINT:
+ case POINT:
+ return pointToGeom(geography, geometryFactory);
+ case SINGLEPOLYLINE:
+ case POLYLINE:
+ return polylineToGeom(geography, geometryFactory);
+ case POLYGON:
+ case MULTIPOLYGON:
+ return polygonToGeom(geography, geometryFactory);
+ case GEOGRAPHY_COLLECTION:
+ return collectionToGeom(geography, geometryFactory);
+ default:
+ throw new IllegalArgumentException("Unsupported Geography type: " +
kind);
+ }
+ }
+
+ // POINT/SINGLEPOINT
+ private static Geometry pointToGeom(Geography g, GeometryFactory gf) {
+ if (g instanceof SinglePointGeography) {
+ S2Point p = ((SinglePointGeography) g).getPoints().get(0);
+ S2LatLng ll = S2LatLng.fromPoint(p);
+ return gf.createPoint(new Coordinate(ll.lngDegrees(), ll.latDegrees()));
+ } else if (g instanceof PointGeography) {
+ List<S2Point> pts = ((PointGeography) g).getPoints();
+ Coordinate[] cs = new Coordinate[pts.size()];
+ for (int i = 0; i < pts.size(); i++) {
+ S2LatLng ll = S2LatLng.fromPoint(pts.get(i));
+ cs[i] = new Coordinate(ll.lngDegrees(), ll.latDegrees());
+ }
+ return gf.createMultiPointFromCoords(cs);
+ }
+ return null;
+ }
+
+ // POLYLINE/SINGLEPOLYLINE
+ private static Geometry polylineToGeom(Geography g, GeometryFactory gf) {
+ if (g instanceof SinglePolylineGeography) {
+ S2Polyline line = ((SinglePolylineGeography) g).getPolylines().get(0);
+ int n = line.numVertices();
+ Coordinate[] cs = new Coordinate[n];
+ for (int k = 0; k < n; k++) {
+ S2LatLng ll = S2LatLng.fromPoint(line.vertex(k));
+ cs[k] = new Coordinate(ll.lngDegrees(), ll.latDegrees());
+ }
+ return gf.createLineString(cs);
+ } else if (g instanceof PolylineGeography) {
+ List<S2Polyline> lines = ((PolylineGeography) g).getPolylines();
+ LineString[] lss = new LineString[lines.size()];
+ for (int i = 0; i < lines.size(); i++) {
+ S2Polyline pl = lines.get(i);
+ int n = pl.numVertices();
+ Coordinate[] cs = new Coordinate[n];
+ for (int k = 0; k < n; k++) {
+ S2LatLng ll = S2LatLng.fromPoint(pl.vertex(k));
+ cs[k] = new Coordinate(ll.lngDegrees(), ll.latDegrees());
+ }
+ lss[i] = gf.createLineString(cs);
+ }
+ return gf.createMultiLineString(lss);
+ }
+ return null;
+ }
+
+ // POLYGON / MULTIPOLYGON
+ private static Geometry polygonToGeom(Geography g, GeometryFactory gf) {
+ if (g instanceof PolygonGeography) {
+ S2Polygon s2p = ((PolygonGeography) g).polygon;
+ return s2LoopsToJts(s2p.getLoops(), gf);
+ } else if (g instanceof MultiPolygonGeography) {
+ List<Geography> parts = ((MultiPolygonGeography) g).getFeatures();
+ Polygon[] polys = new Polygon[parts.size()];
+ for (int i = 0; i < parts.size(); i++) {
+ polys[i] = (Polygon) s2LoopsToJts(((PolygonGeography)
parts.get(i)).polygon.getLoops(), gf);
+ }
+ return gf.createMultiPolygon(polys);
+ }
+ return null;
+ }
+
+ private static Geometry s2LoopsToJts(List<S2Loop> loops, GeometryFactory gf)
{
+ if (loops == null || loops.isEmpty()) return gf.createPolygon();
+
+ List<LinearRing> shells = new ArrayList<>();
+ List<List<LinearRing>> holesPerShell = new ArrayList<>();
+
+ // Stack of current ancestor shells: each frame = {shellIndex, depth}
+ // depth 0: Shell A
+ // depth 1: Hole H1 (a lake in A)
+ // depth 2: Shell S2 (an island in that lake A)
+ // depth 3: Hole H3 (a pond on that island)
+ // depth 0: Shell B (disjoint area)
+ // depth 1: Hole H2 (a lake in B)
+ List<int[]> shellStack = new ArrayList<>();
+
+ for (S2Loop L : loops) {
+ int n = L.numVertices();
+ if (n < 3) continue;
+
+ // Build & close ring once (x=lng, y=lat)
+ Coordinate[] cs = new Coordinate[n + 1];
+ for (int i = 0; i < n; i++) {
+ S2LatLng ll = S2LatLng.fromPoint(L.vertex(i)).normalized();
+ cs[i] = new Coordinate(ll.lngDegrees(), ll.latDegrees());
+ }
+ cs[n] = cs[0];
+
+ // Guard against degenerate collapse
+ if (cs.length < 4 || cs[0].equals2D(cs[1]) || cs[1].equals2D(cs[2]))
continue;
+
+ LinearRing ring = gf.createLinearRing(cs);
+
+ boolean isShell = (L.depth() & 1) == 0;
+ int depth = L.depth();
+
+ // Unwind ancestors until parent depth < current depth
+ while (!shellStack.isEmpty() && shellStack.get(shellStack.size() - 1)[1]
>= depth) {
+ shellStack.remove(shellStack.size() - 1);
+ }
+
+ if (isShell) {
+ // New shell => new polygon component
+ shells.add(ring);
+ holesPerShell.add(new ArrayList<>());
+ shellStack.add(new int[] {shells.size() - 1, depth});
+ } else {
+ ring = ensureOrientation(ring, /*wantCCW=*/ false, gf);
+ // Attach hole to nearest even-depth ancestor shell
+ if (!shellStack.isEmpty()) {
+ int[] shellContainer = shellStack.get(shellStack.size() - 1);
+ holesPerShell.get(shellContainer[0]).add(ring);
+ }
+ // If no ancestor shell (invalid structure), ignore the hole.
+ }
+ }
+
+ if (shells.isEmpty()) return gf.createPolygon();
+ if (shells.size() == 1) {
+ Polygon polygon =
+ gf.createPolygon(shells.get(0), holesPerShell.get(0).toArray(new
LinearRing[0]));
+ return polygon;
+ }
+ Polygon[] polys = new Polygon[shells.size()];
+ for (int i = 0; i < shells.size(); i++) {
+ polys[i] = gf.createPolygon(shells.get(i),
holesPerShell.get(i).toArray(new LinearRing[0]));
+ }
+ return gf.createMultiPolygon(polys);
+ }
+
+ private static LinearRing ensureOrientation(
+ LinearRing ring, boolean wantCCW, GeometryFactory gf) {
+ boolean isCCW =
org.locationtech.jts.algorithm.Orientation.isCCW(ring.getCoordinates());
+ // If the actual orientation doesn't match the desired orientation, fix it.
+ if (isCCW != wantCCW) {
+ Coordinate[] cs = CoordinateArrays.copyDeep(ring.getCoordinates());
+ CoordinateArrays.reverse(cs);
+ return gf.createLinearRing(cs);
+ }
+ // Otherwise, the ring is already correctly oriented, so return it as is.
+ return ring;
+ }
+
+ // COLLECTION
+ private static Geometry collectionToGeom(Geography g, GeometryFactory gf) {
+ List<Geography> parts = ((GeographyCollection) g).getFeatures();
+ Geometry[] gs = new Geometry[parts.size()];
+ for (int i = 0; i < parts.size(); i++) {
+ gs[i] = geogToGeometry(parts.get(i));
+ }
+ return gf.createGeometryCollection(gs);
+ }
}
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 8198500bb7..716b5dcac3 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
@@ -21,13 +21,20 @@ package org.apache.sedona.common.Geography;
import static org.junit.Assert.*;
import com.google.common.geometry.S2LatLng;
+import com.google.common.geometry.S2Loop;
import com.google.common.geometry.S2Point;
+import com.google.common.geometry.S2Polygon;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.sedona.common.S2Geography.*;
import org.apache.sedona.common.S2Geography.Geography;
import org.apache.sedona.common.S2Geography.SinglePointGeography;
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.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
@@ -139,4 +146,103 @@ public class ConstructorsTest {
actualWkt = geog.toText(new PrecisionModel(1e6));
assertEquals(expectedWkt, actualWkt);
}
+
+ @Test
+ public void geogToGeometry() throws ParseException {
+ S2Point pt = S2LatLng.fromDegrees(45, -64).toPoint();
+ S2Point pt_mid = S2LatLng.fromDegrees(45, 0).toPoint();
+ S2Point pt_end = S2LatLng.fromDegrees(0, 0).toPoint();
+ // Build a single polygon and wrap in geography
+ List<S2Point> points = new ArrayList<>();
+ points.add(pt);
+ points.add(pt_mid);
+ points.add(pt_end);
+ S2Loop polyline = new S2Loop(points);
+ S2Polygon poly = new S2Polygon(polyline);
+ PolygonGeography geo = new PolygonGeography(poly);
+ GeometryFactory gf = new GeometryFactory(new
PrecisionModel(PrecisionModel.FIXED));
+ Geometry result = Constructors.geogToGeometry(geo, gf);
+ assertEquals(geo.toString(), result.toString());
+
+ String withHole =
+ "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), " + "(20 30, 35 35, 30
20, 20 30))";
+ String expected = "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (30 20,
20 30, 35 35, 30 20))";
+ Geography geography = new WKTReader().read(withHole);
+ Geometry geom = Constructors.geogToGeometry(geography, gf);
+ assertEquals(expected, geom.toString());
+
+ String multiGeog = "MULTIPOINT ((10 40), (40 30), (20 20), (30 10))";
+ geography = new WKTReader().read(multiGeog);
+ geom = Constructors.geogToGeometry(geography, gf);
+ assertEquals(multiGeog, geom.toString());
+
+ multiGeog = "MULTILINESTRING " + "((90 90, 20 20, 10 40), (40 40, 30 30,
40 20, 30 10))";
+ // Geography can not exceeding to more than 90 degrees / longitude
+ geography = new WKTReader().read(multiGeog);
+ geom = Constructors.geogToGeometry(geography, gf);
+ assertEquals(multiGeog, geom.toString());
+
+ multiGeog =
+ "MULTIPOLYGON "
+ + "(((30 20, 45 40, 10 40, 30 20)), "
+ + "((15 5, 40 10, 10 20, 5 10, 15 5)))";
+ geography = new WKTReader().read(multiGeog);
+ geom = Constructors.geogToGeometry(geography, gf);
+ assertEquals(multiGeog, geom.toString());
+ }
+
+ @Test
+ public void deep_nesting_twoComponents() throws Exception {
+ String wkt =
+ "MULTIPOLYGON ("
+ +
+ // Component A: outer shell + lake
+ "((10 10, 70 10, 70 70, 10 70, 10 10),"
+ + " (20 20, 60 20, 60 60, 20 60, 20 20)),"
+ +
+ // Component B: island with a pond
+ "((30 30, 50 30, 50 50, 30 50, 30 30),"
+ + " (36 36, 44 36, 44 44, 36 44, 36 36))"
+ + ")";
+
+ Geography g = new WKTReader().read(wkt);
+ g.setSRID(4326);
+ Geometry got = Constructors.geogToGeometry(g);
+ String expected =
+ "MULTIPOLYGON (((10 10, 70 10, 70 70, 10 70, 10 10), "
+ + "(20 20, 20 60, 60 60, 60 20, 20 20)), "
+ + "((30 30, 50 30, 50 50, 30 50, 30 30), "
+ + "(36 36, 36 44, 44 44, 44 36, 36 36)))";
+ assertEquals(4326, got.getSRID());
+ org.locationtech.jts.io.WKTWriter wktWriter = new
org.locationtech.jts.io.WKTWriter();
+ wktWriter.setPrecisionModel(new PrecisionModel(PrecisionModel.FIXED));
+ String gotGeom = wktWriter.write(got);
+ assertEquals(expected, gotGeom);
+ }
+
+ @Test
+ public void polygon_threeHoles() throws Exception {
+ String wkt =
+ "POLYGON (("
+ + "0 0, 95 20, 95 85, 10 85, 0 0"
+ + "),("
+ + "20 30, 35 25, 30 40, 20 30"
+ + "),("
+ + "50 50, 65 50, 65 65, 50 65, 50 50"
+ + "),("
+ + "25 60, 35 58, 38 66, 30 72, 22 66, 25 60"
+ + "))";
+
+ Geography g = new WKTReader().read(wkt);
+ String expected =
+ "POLYGON ((0 0, 95 20, 95 85, 10 85, 0 0), "
+ + "(20 30, 30 40, 35 25, 20 30), "
+ + "(50 50, 50 65, 65 65, 65 50, 50 50), "
+ + "(25 60, 22 66, 30 72, 38 66, 35 58, 25 60))";
+ Geometry got =
+ Constructors.geogToGeometry(
+ g, new GeometryFactory(new PrecisionModel(PrecisionModel.FIXED)));
+ assertEquals(expected, got.toString());
+ assertEquals(0, got.getSRID());
+ }
}
diff --git a/docs/api/sql/geography/Constructor.md
b/docs/api/sql/geography/Constructor.md
index bd5577c8f4..4b3ae2331b 100644
--- a/docs/api/sql/geography/Constructor.md
+++ b/docs/api/sql/geography/Constructor.md
@@ -140,3 +140,25 @@ Output:
```
SRID=4326; LINESTRING (0 0, 3 3, 4 4)
```
+
+## ST_GeogToGeometry
+
+Introduction: Construct a Geometry from a Geography.
+
+Format:
+
+`ST_GeogToGeometry (geog: Geography)`
+
+Since: `v1.8.0`
+
+SQL example:
+
+```sql
+SELECT ST_GeogToGeometry(ST_GeogFromWKT('MULTILINESTRING ((90 90, 20 20, 10
40), (40 40, 30 30, 40 20, 30 10))', 4326))
+```
+
+Output:
+
+```
+MULTILINESTRING ((90 90, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))
+```
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 074db478d0..4905d14867 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,9 +22,7 @@ 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.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKB, ST_GeogFromEWKT, ST_GeogFromGeoHash, ST_GeogFromText,
ST_GeogFromWKB, ST_GeogFromWKT, ST_GeogToGeometry}
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.operation.buffer.BufferParameters
@@ -353,7 +351,8 @@ object Catalog extends AbstractCatalog {
function[ST_LocalOutlierFactor](),
function[ST_GLocal](),
function[ST_BinaryDistanceBandColumn](),
- function[ST_WeightedDistanceBandColumn]())
+ function[ST_WeightedDistanceBandColumn](),
+ function[ST_GeogToGeometry]())
val aggregateExpressions: Seq[Aggregator[Geometry, _, _]] =
Seq(new ST_Envelope_Aggr, new ST_Intersection_Aggr, new ST_Union_Aggr())
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 ad339c146b..d010fa4b0f 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
@@ -18,8 +18,10 @@
*/
package org.apache.spark.sql.sedona_sql.expressions.geography
+import org.apache.sedona.common.S2Geography.Geography
import org.apache.sedona.common.geography.Constructors
import org.apache.spark.sql.catalyst.expressions.Expression
+import org.apache.spark.sql.sedona_sql.UDT.GeographyUDT
import
org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._
import org.apache.spark.sql.sedona_sql.expressions.{InferrableFunction,
InferredExpression}
@@ -121,3 +123,17 @@ private[apache] case class
ST_GeogFromGeoHash(inputExpressions: Seq[Expression])
copy(inputExpressions = newChildren)
}
}
+
+/**
+ * Return a Geometry from a Geography
+ *
+ * @param inputExpressions
+ * This function takes a geography object.
+ */
+private[apache] case class ST_GeogToGeometry(inputExpressions: Seq[Expression])
+ extends InferredExpression(Constructors.geogToGeometry(_: Geography)) {
+
+ 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 38a5469cf4..a811384993 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
@@ -19,10 +19,8 @@
package org.apache.spark.sql.sedona_sql.expressions
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}
+import org.apache.spark.sql.sedona_sql.DataFrameShims.{wrapExpression, _}
+import
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText,
ST_GeogFromEWKB, ST_GeogFromEWKT, ST_GeogFromGeoHash, ST_GeogFromText,
ST_GeogFromWKB, ST_GeogFromWKT, ST_GeogToGeometry}
object st_constructors {
def ST_GeomFromGeoHash(geohash: Column, precision: Column): Column =
@@ -304,4 +302,7 @@ object st_constructors {
wrapExpression[ST_MPointFromText](wkt, srid)
def ST_MPointFromText(wkt: String, srid: Int): Column =
wrapExpression[ST_MPointFromText](wkt, srid)
+
+ def ST_GeogToGeometry(geog: Column): Column =
wrapExpression[ST_GeogToGeometry](geog)
+ def ST_GeogToGeometry(geog: String): Column =
wrapExpression[ST_GeogToGeometry](geog)
}
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 9417e4eed8..610e3fba4d 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
@@ -20,10 +20,11 @@ package org.apache.sedona.sql.geography
import org.apache.sedona.common.S2Geography.{Geography, WKBReader}
import org.apache.sedona.sql.TestBaseScala
-import org.apache.spark.sql.functions.col
-import org.apache.spark.sql.sedona_sql.expressions.{implicits, st_constructors}
-import org.junit.Assert.{assertEquals, assertFalse, assertTrue}
-import org.locationtech.jts.geom.PrecisionModel
+import org.apache.spark.sql.functions.{col, lit}
+import org.apache.spark.sql.sedona_sql.expressions.st_constructors
+import org.junit.Assert.assertEquals
+import org.locationtech.jts.geom.{Geometry, PrecisionModel}
+import org.locationtech.jts.io.WKTWriter
class ConstructorsDataFrameAPITest extends TestBaseScala {
import sparkSession.implicits._
@@ -96,4 +97,34 @@ class ConstructorsDataFrameAPITest extends TestBaseScala {
assertEquals(expectedWkt, actualResult)
}
+ it("passed st_geogtogeometry multipolygon") {
+ val wkt =
+ "MULTIPOLYGON (" +
+ "((10 10, 70 10, 70 70, 10 70, 10 10), (20 20, 60 20, 60 60, 20 60, 20
20))," +
+ "((30 30, 50 30, 50 50, 30 50, 30 30), (36 36, 44 36, 44 44, 36 44, 36
36))" +
+ ")"
+
+ val df = sparkSession
+ .sql(s"SELECT '$wkt' AS wkt")
+ .select(st_constructors.ST_GeogFromWKT(col("wkt"), lit(4326)).as("geog"))
+ .select(st_constructors.ST_GeogToGeometry(col("geog")).as("geom"))
+
+ val geom = df.head().getAs[Geometry]("geom")
+ assert(geom.getGeometryType == "MultiPolygon")
+
+ val expectedWkt =
+ "MULTIPOLYGON (((10 10, 70 10, 70 70, 10 70, 10 10), " +
+ "(20 20, 20 60, 60 60, 60 20, 20 20)), " +
+ "((30 30, 50 30, 50 50, 30 50, 30 30), " +
+ "(36 36, 36 44, 44 44, 44 36, 36 36)))"
+
+ val writer = new WKTWriter()
+ writer.setFormatted(false)
+ writer.setPrecisionModel(new PrecisionModel(PrecisionModel.FIXED))
+
+ val got = writer.write(geom)
+ assert(got == expectedWkt)
+ assert(geom.getSRID == 4326)
+ }
+
}
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 092355a691..4ffbe2d191 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,10 +19,11 @@
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
+import org.locationtech.jts.geom.Geometry
+import org.locationtech.jts.io.WKTWriter
class ConstructorsTest extends TestBaseScala {
@@ -168,4 +169,68 @@ class ConstructorsTest extends TestBaseScala {
assert(geography.first().getAs[Geography](0).getSRID == 4326)
assert(geography.first().getAs[Geography](0).toString.equals(expectedGeog))
}
+
+ it("Passed ST_GeogToGeometry polygon") {
+ val wkt =
+ "POLYGON ((" + "0 0, 95 20, 95 85, 10 85, 0 0" + "),(" + "20 30, 35 25,
30 40, 20 30" + "),(" + "50 50, 65 50, 65 65, 50 65, 50 50" + "),(" + "25 60,
35 58, 38 66, 30 72, 22 66, 25 60" + "))"
+ val df = sparkSession.sql(s"""
+ SELECT
+ ST_GeogToGeometry(ST_GeogFromWKT('$wkt')) AS geom
+ """)
+ val geom = df.first().getAs[Geometry](0)
+ val expected =
+ "POLYGON ((0 0, 95 20, 95 85, 10 85, 0 0), " + "(20 30, 30 40, 35 25, 20
30), " + "(50 50, 50 65, 65 65, 65 50, 50 50), " + "(25 60, 22 66, 30 72, 38
66, 35 58, 25 60))"
+ assert(geom.getGeometryType == "Polygon")
+ val writer = new WKTWriter()
+ writer.setPrecisionModel(new PrecisionModel(PrecisionModel.FIXED))
+ val gotGeom = writer.write(geom)
+ assert(gotGeom == expected)
+ }
+
+ it("Passed ST_GeogToGeometry multipolygon") {
+ val wkt = "MULTIPOLYGON (" + // Component A: outer shell + lake
+ "((10 10, 70 10, 70 70, 10 70, 10 10)," + " (20 20, 60 20, 60 60, 20 60,
20 20))," +
+ // Component B: island with a pond
+ "((30 30, 50 30, 50 50, 30 50, 30 30)," + " (36 36, 44 36, 44 44, 36 44,
36 36))" + ")";
+ val df = sparkSession.sql(s"""
+ SELECT
+ ST_GeogToGeometry(ST_GeogFromWKT('$wkt', 4326)) AS geom
+ """)
+ val geom = df.first().getAs[Geometry](0)
+ val expected = "MULTIPOLYGON (((10 10, 70 10, 70 70, 10 70, 10 10), " +
+ "(20 20, 20 60, 60 60, 60 20, 20 20)), " + "((30 30, 50 30, 50 50, 30
50, 30 30), " +
+ "(36 36, 36 44, 44 44, 44 36, 36 36)))";
+ assert(geom.getGeometryType == "MultiPolygon")
+ val writer = new WKTWriter()
+ writer.setPrecisionModel(new PrecisionModel(PrecisionModel.FIXED))
+ val gotGeom = writer.write(geom)
+ assert(gotGeom == expected)
+ assert(geom.getSRID == 4326)
+ }
+
+ it("Passed ST_GeogToGeometry linestring") {
+ var wkt = "MULTILINESTRING " + "((90 90, 20 20, 10 40), (40 40, 30 30, 40
20, 30 10))"
+ var df = sparkSession.sql(s"""
+ SELECT
+ ST_GeogToGeometry(ST_GeogFromWKT('$wkt', 4326)) AS geom
+ """)
+ var geom = df.first().getAs[Geometry](0)
+ val writer = new WKTWriter()
+ writer.setPrecisionModel(new PrecisionModel(PrecisionModel.FIXED))
+ var gotGeom = writer.write(geom)
+ assertEquals(wkt, gotGeom)
+ assertEquals(4326, geom.getSRID)
+ assert(geom.getGeometryType == "MultiLineString")
+
+ wkt = "LINESTRING " + "(90 90, 20 20, 10 40)"
+ df = sparkSession.sql(s"""
+ SELECT
+ ST_GeogToGeometry(ST_GeogFromWKT('$wkt', 4326)) AS geom
+ """)
+ geom = df.first().getAs[Geometry](0)
+ gotGeom = writer.write(geom)
+ assertEquals(wkt, gotGeom)
+ assertEquals(4326, geom.getSRID)
+ assert(geom.getGeometryType == "LineString")
+ }
}