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

Reply via email to