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)