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 2026/09/21 05:19:34 UTC

(sedona) branch master updated: [GH-3369] Preserve empty components when setting SRID (#3377)

This is an automated email from the ASF dual-hosted git repository.

jiayuasu 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 6cd7c01a846 [GH-3369] Preserve empty components when setting SRID (#3377)
6cd7c01a846 is described below

commit 6cd7c01a846b45e1b7454cf7887368c1a873c223
Author: Jia Yu <ji...@apache.org>
AuthorDate: Sun Sep 20 22:19:28 2026 -0700

    [GH-3369] Preserve empty components when setting SRID (#3377)
---
 .../java/org/apache/sedona/common/Functions.java   |   8 +-
 .../org/apache/sedona/common/FunctionsTest.java    | 162 +++++++++++++++++++++
 pom.xml                                            |   2 +-
 .../org/apache/sedona/sql/functionTestScala.scala  |  21 +++
 4 files changed, 186 insertions(+), 7 deletions(-)

diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java b/common/src/main/java/org/apache/sedona/common/Functions.java
index e3057e5ae12..030ec563bde 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -38,6 +38,7 @@ import org.apache.sedona.common.jts2geojson.GeoJSONWriter;
 import org.apache.sedona.common.sphere.Spheroid;
 import org.apache.sedona.common.subDivide.GeometrySubDivider;
 import org.apache.sedona.common.utils.*;
+import org.datasyslab.jts.geom.util.GeometryCopier;
 import org.locationtech.jts.algorithm.Angle;
 import org.locationtech.jts.algorithm.MinimumAreaRectangle;
 import org.locationtech.jts.algorithm.MinimumBoundingCircle;
@@ -1103,12 +1104,7 @@ public class Functions {
             geometry.getPrecisionModel(),
             srid,
             geometry.getFactory().getCoordinateSequenceFactory());
-    Geometry newGeom = factory.createGeometry(geometry);
-    // Workaround for JTS bug: GeometryEditor.editPolygon returns the original
-    // empty polygon without copying it to the new factory, so the SRID is not
-    // updated for POLYGON EMPTY (and similar empty geometry types).
-    newGeom.setSRID(srid);
-    return newGeom;
+    return GeometryCopier.copy(geometry, factory);
   }
 
   public static int getSRID(Geometry geometry) {
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 0bd0c833090..3df0d1aacc1 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -37,6 +37,8 @@ import org.geotools.referencing.operation.projection.ProjectionException;
 import org.junit.Test;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory;
+import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
 import org.locationtech.jts.geom.prep.PreparedGeometry;
 import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
 import org.locationtech.jts.io.ParseException;
@@ -4676,6 +4678,166 @@ public class FunctionsTest extends TestBase {
     }
   }
 
+  @Test
+  public void setSRIDPreservesEmptyPolygonHoles() throws ParseException {
+    Polygon source =
+        (Polygon) Constructors.geomFromWKT("POLYGON ((0 0, 10 0, 10 10, 0 0), EMPTY)", 100);
+
+    Polygon result = (Polygon) Functions.setSRID(source, 4326);
+
+    assertEquals(1, result.getNumInteriorRing());
+    assertTrue(result.getInteriorRingN(0).isEmpty());
+    assertEquals(source.getExteriorRing(), result.getExteriorRing());
+    assertNotSame(source.getInteriorRingN(0), result.getInteriorRingN(0));
+    assertGeometryTreeUsesFactory(result, result.getFactory(), 4326);
+    assertEquals(100, source.getSRID());
+    assertEquals(1, source.getNumInteriorRing());
+  }
+
+  @Test
+  public void setSRIDCopiesEmptyPolygonWithoutMutatingInput() throws ParseException {
+    Polygon source = (Polygon) Constructors.geomFromWKT("POLYGON EMPTY", 100);
+    source.setUserData("source metadata");
+    source.getExteriorRing().setUserData("shell metadata");
+
+    Polygon result = (Polygon) Functions.setSRID(source, 4326);
+
+    assertEquals(100, source.getSRID());
+    assertNotSame(source, result);
+    assertNotSame(source.getExteriorRing(), result.getExteriorRing());
+    assertTrue(result.isEmpty());
+    assertGeometryTreeUsesFactory(result, result.getFactory(), 4326);
+    assertNull(result.getUserData());
+    assertNull(result.getExteriorRing().getUserData());
+    assertEquals("source metadata", source.getUserData());
+    assertEquals("shell metadata", source.getExteriorRing().getUserData());
+  }
+
+  @Test
+  public void setSRIDPreservesNestedEmptyComponentsAndCopiesStructure() {
+    GeometryFactory sourceFactory =
+        new GeometryFactory(new PrecisionModel(), 100, CoordinateArraySequenceFactory.instance());
+    Point emptyPoint =
+        sourceFactory.createPoint(sourceFactory.getCoordinateSequenceFactory().create(0, 3, 0));
+    Polygon emptyPolygon =
+        sourceFactory.createPolygon(
+            sourceFactory.createLinearRing(
+                sourceFactory.getCoordinateSequenceFactory().create(0, 4, 1)));
+    Point populatedPoint = sourceFactory.createPoint(new Coordinate(1, 2));
+    GeometryCollection nested =
+        sourceFactory.createGeometryCollection(new Geometry[] {emptyPolygon, populatedPoint});
+    GeometryCollection source =
+        sourceFactory.createGeometryCollection(new Geometry[] {emptyPoint, nested});
+    source.setSRID(100);
+    emptyPoint.setSRID(101);
+    nested.setSRID(102);
+    emptyPolygon.setSRID(103);
+    populatedPoint.setSRID(104);
+    source.setUserData("root metadata");
+    emptyPoint.setUserData("child metadata");
+    nested.setUserData("nested metadata");
+
+    GeometryCollection result = (GeometryCollection) Functions.setSRID(source, 4326);
+
+    assertEquals(2, result.getNumGeometries());
+    assertEquals(2, result.getGeometryN(1).getNumGeometries());
+    assertGeometryTreeUsesFactory(result, result.getFactory(), 4326);
+    assertNull(result.getUserData());
+    assertNull(result.getGeometryN(0).getUserData());
+    assertNull(result.getGeometryN(1).getUserData());
+    assertEquals(3, ((Point) result.getGeometryN(0)).getCoordinateSequence().getDimension());
+    assertEquals(
+        4,
+        ((Polygon) result.getGeometryN(1).getGeometryN(0))
+            .getExteriorRing()
+            .getCoordinateSequence()
+            .getDimension());
+    assertEquals(
+        1,
+        ((Polygon) result.getGeometryN(1).getGeometryN(0))
+            .getExteriorRing()
+            .getCoordinateSequence()
+            .getMeasures());
+    assertNotSame(source, result);
+    assertNotSame(source.getGeometryN(0), result.getGeometryN(0));
+    Point resultPoint = (Point) result.getGeometryN(1).getGeometryN(1);
+    resultPoint.getCoordinateSequence().setOrdinate(0, 0, 9);
+    assertEquals(1, populatedPoint.getX(), 0);
+    assertEquals(100, source.getSRID());
+    assertEquals(101, emptyPoint.getSRID());
+    assertEquals(102, nested.getSRID());
+    assertEquals(103, emptyPolygon.getSRID());
+    assertEquals(104, populatedPoint.getSRID());
+    assertEquals("root metadata", source.getUserData());
+    assertEquals("child metadata", emptyPoint.getUserData());
+    assertEquals("nested metadata", nested.getUserData());
+  }
+
+  @Test
+  public void setSRIDPreservesPackedCoordinateSequenceFactoryAndEmptyLayouts() {
+    GeometryFactory sourceFactory =
+        new GeometryFactory(
+            new PrecisionModel(), 7, PackedCoordinateSequenceFactory.DOUBLE_FACTORY);
+    Point emptyXym =
+        sourceFactory.createPoint(sourceFactory.getCoordinateSequenceFactory().create(0, 3, 1));
+    LineString emptyXyz =
+        sourceFactory.createLineString(
+            sourceFactory.getCoordinateSequenceFactory().create(0, 3, 0));
+    LinearRing emptyXyzmShell =
+        sourceFactory.createLinearRing(
+            sourceFactory.getCoordinateSequenceFactory().create(0, 4, 1));
+    GeometryCollection source =
+        sourceFactory.createGeometryCollection(
+            new Geometry[] {
+              sourceFactory.createMultiPoint(new Point[] {emptyXym}),
+              sourceFactory.createMultiLineString(new LineString[] {emptyXyz}),
+              sourceFactory.createMultiPolygon(
+                  new Polygon[] {sourceFactory.createPolygon(emptyXyzmShell)})
+            });
+
+    GeometryCollection result = (GeometryCollection) Functions.setSRID(source, 3857);
+
+    assertSame(
+        PackedCoordinateSequenceFactory.DOUBLE_FACTORY,
+        result.getFactory().getCoordinateSequenceFactory());
+    assertEquals(1, result.getGeometryN(0).getNumGeometries());
+    assertEquals(1, result.getGeometryN(1).getNumGeometries());
+    assertEquals(1, result.getGeometryN(2).getNumGeometries());
+    CoordinateSequence pointSequence =
+        ((Point) result.getGeometryN(0).getGeometryN(0)).getCoordinateSequence();
+    CoordinateSequence lineSequence =
+        ((LineString) result.getGeometryN(1).getGeometryN(0)).getCoordinateSequence();
+    CoordinateSequence shellSequence =
+        ((Polygon) result.getGeometryN(2).getGeometryN(0))
+            .getExteriorRing()
+            .getCoordinateSequence();
+    assertEquals(3, pointSequence.getDimension());
+    assertEquals(1, pointSequence.getMeasures());
+    assertEquals(3, lineSequence.getDimension());
+    assertEquals(0, lineSequence.getMeasures());
+    assertEquals(4, shellSequence.getDimension());
+    assertEquals(1, shellSequence.getMeasures());
+    assertGeometryTreeUsesFactory(result, result.getFactory(), 3857);
+    assertEquals(7, source.getFactory().getSRID());
+  }
+
+  private static void assertGeometryTreeUsesFactory(
+      Geometry geometry, GeometryFactory factory, int srid) {
+    assertSame(factory, geometry.getFactory());
+    assertEquals(srid, geometry.getSRID());
+    assertEquals(srid, geometry.getFactory().getSRID());
+    for (int i = 0; i < geometry.getNumGeometries(); i++) {
+      Geometry child = geometry.getGeometryN(i);
+      if (child != geometry) assertGeometryTreeUsesFactory(child, factory, srid);
+    }
+    if (geometry instanceof Polygon) {
+      Polygon polygon = (Polygon) geometry;
+      assertGeometryTreeUsesFactory(polygon.getExteriorRing(), factory, srid);
+      for (int i = 0; i < polygon.getNumInteriorRing(); i++)
+        assertGeometryTreeUsesFactory(polygon.getInteriorRingN(i), factory, srid);
+    }
+  }
+
   @Test
   public void closestPoint() {
     Point point1 = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1));
diff --git a/pom.xml b/pom.xml
index ece07168501..61993e67f98 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,7 +74,7 @@
         <hadoop.version>3.2.4</hadoop.version>
         <jackson.version>2.13.4</jackson.version>
         <jts.version>1.20.0</jts.version>
-        <jts.io.patch.version>1.21.0-datasyslab-1</jts.io.patch.version>
+        <jts.io.patch.version>1.21.0-datasyslab-2</jts.io.patch.version>
         <jts2geojson.version>0.16.1</jts2geojson.version>
         <spatial4j.version>0.8</spatial4j.version>
 
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 1bf7154b4a9..e2a02c554b2 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -1036,6 +1036,27 @@ class functionTestScala
       assert(df.first().get(0).asInstanceOf[Polygon].getSRID == 3021)
     }
 
+    it("ST_SetSRID preserves empty polygon holes") {
+      val polygonWithEmptyHole =
+        "01030000000200000005000000000000000000000000000000000000000000000000002440000000000000000000000000000024400000000000002440000000000000000000000000000024400000000000000000000000000000000000000000"
+      val result = sparkSession
+        .sql(s"""
+            |WITH source AS (
+            |  SELECT ST_GeomFromWKB(unhex('$polygonWithEmptyHole')) AS polygon
+            |)
+            |SELECT
+            |  ST_NumInteriorRings(polygon),
+            |  ST_NumInteriorRings(ST_SetSRID(polygon, 4326)),
+            |  ST_SRID(ST_SetSRID(polygon, 4326))
+            |FROM source
+            |""".stripMargin)
+        .first()
+
+      assertEquals(1, result.getInt(0))
+      assertEquals(1, result.getInt(1))
+      assertEquals(4326, result.getInt(2))
+    }
+
     it("Passed ST_AsHEXEWKB") {
       val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POINT(1 2)') as point")
       var actual = baseDf.selectExpr("ST_AsHEXEWKB(point)").first().get(0)