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/10/20 00:36:18 UTC

(sedona) branch master updated: [GH-3486] Keep the Z or M dimension of WKT tagged Z, M or ZM even when its values are all NaN (#3487)

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 78920fc3c60 [GH-3486] Keep the Z or M dimension of WKT tagged Z, M or ZM even when its values are all NaN (#3487)
78920fc3c60 is described below

commit 78920fc3c60fbbfc799297eb57c4fd2f99a31428
Author: Jia Yu <ji...@apache.org>
AuthorDate: Mon Oct 19 17:36:13 2026 -0700

    [GH-3486] Keep the Z or M dimension of WKT tagged Z, M or ZM even when its values are all NaN (#3487)
---
 .../org/apache/sedona/common/Constructors.java     | 20 ++++------
 .../apache/sedona/common/utils/FormatUtils.java    |  6 +--
 .../org/apache/sedona/common/ConstructorsTest.java | 44 ++++++++++++++++++++++
 .../org/apache/sedona/common/PredicatesTest.java   | 17 +++++++++
 pom.xml                                            |  2 +-
 python/tests/geopandas/test_geoseries.py           |  9 ++---
 6 files changed, 76 insertions(+), 22 deletions(-)

diff --git a/common/src/main/java/org/apache/sedona/common/Constructors.java b/common/src/main/java/org/apache/sedona/common/Constructors.java
index 19e1550ed56..91a8aaf978b 100644
--- a/common/src/main/java/org/apache/sedona/common/Constructors.java
+++ b/common/src/main/java/org/apache/sedona/common/Constructors.java
@@ -29,9 +29,9 @@ import org.apache.sedona.common.geometryObjects.Box3D;
 import org.apache.sedona.common.utils.FormatUtils;
 import org.apache.sedona.common.utils.GeoHashDecoder;
 import org.datasyslab.jts.io.WKBReader;
+import org.datasyslab.jts.io.WKTReader;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.io.ParseException;
-import org.locationtech.jts.io.WKTReader;
 import org.locationtech.jts.io.gml2.GMLReader;
 import org.locationtech.jts.io.kml.KMLReader;
 import org.xml.sax.SAXException;
@@ -44,8 +44,7 @@ public class Constructors {
     if (wkt == null) {
       return null;
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(srid).read(wkt);
   }
 
   public static Geometry geomFromEWKT(String ewkt) throws ParseException {
@@ -65,8 +64,7 @@ public class Constructors {
         throw new ParseException("Invalid EWKT string");
       }
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), SRID);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(SRID).read(wkt);
   }
 
   public static Geometry geomFromWKB(byte[] wkb) throws ParseException {
@@ -114,32 +112,28 @@ public class Constructors {
     if (wkt == null || !wkt.startsWith("MULTIPOINT")) {
       return null;
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(srid).read(wkt);
   }
 
   public static Geometry mLineFromText(String wkt, int srid) throws ParseException {
     if (wkt == null || !wkt.startsWith("MULTILINESTRING")) {
       return null;
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(srid).read(wkt);
   }
 
   public static Geometry mPolyFromText(String wkt, int srid) throws ParseException {
     if (wkt == null || !wkt.startsWith("MULTIPOLYGON")) {
       return null;
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(srid).read(wkt);
   }
 
   public static Geometry geomCollFromText(String wkt, int srid) throws ParseException {
     if (wkt == null || !wkt.startsWith("GEOMETRYCOLLECTION")) {
       return null;
     }
-    GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
-    return new WKTReader(geometryFactory).read(wkt);
+    return WKTReader.forDeclaredDimensions(srid).read(wkt);
   }
 
   /**
diff --git a/common/src/main/java/org/apache/sedona/common/utils/FormatUtils.java b/common/src/main/java/org/apache/sedona/common/utils/FormatUtils.java
index 9994b8945a4..45b851e88e5 100644
--- a/common/src/main/java/org/apache/sedona/common/utils/FormatUtils.java
+++ b/common/src/main/java/org/apache/sedona/common/utils/FormatUtils.java
@@ -27,9 +27,9 @@ import org.apache.sedona.common.enums.FileDataSplitter;
 import org.apache.sedona.common.enums.GeometryType;
 import org.apache.sedona.common.jts2geojson.GeoJSONReader;
 import org.datasyslab.jts.io.WKBReader;
+import org.datasyslab.jts.io.WKTReader;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.io.ParseException;
-import org.locationtech.jts.io.WKTReader;
 import org.locationtech.jts.operation.valid.IsValidOp;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +56,7 @@ public class FormatUtils<T extends Geometry> implements Serializable {
   protected transient GeometryFactory factory = new GeometryFactory();
 
   protected transient GeoJSONReader geoJSONReader = new GeoJSONReader();
-  protected transient WKTReader wktReader = new WKTReader();
+  protected transient WKTReader wktReader = WKTReader.forDeclaredDimensions();
   /** Allow mapping of invalid geometries. */
   public boolean allowTopologicallyInvalidGeometries;
   // For some unknown reasons, the wkb reader cannot be used in transient variable like the wkt
@@ -137,7 +137,7 @@ public class FormatUtils<T extends Geometry> implements Serializable {
       throws IOException, ClassNotFoundException {
     inputStream.defaultReadObject();
     factory = new GeometryFactory();
-    wktReader = new WKTReader();
+    wktReader = WKTReader.forDeclaredDimensions();
     geoJSONReader = new GeoJSONReader();
   }
 
diff --git a/common/src/test/java/org/apache/sedona/common/ConstructorsTest.java b/common/src/test/java/org/apache/sedona/common/ConstructorsTest.java
index 319ecc1e948..77709bb3ada 100644
--- a/common/src/test/java/org/apache/sedona/common/ConstructorsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/ConstructorsTest.java
@@ -22,15 +22,59 @@ import static org.junit.Assert.*;
 
 import java.util.EnumSet;
 import org.apache.sedona.common.geometryObjects.Box2D;
+import org.apache.sedona.common.geometrySerde.CoordinateType;
+import org.apache.sedona.common.geometrySerde.GeometrySerializer;
 import org.apache.sedona.common.utils.GeomUtils;
 import org.junit.Test;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.io.Ordinate;
 import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKBReader;
 import org.locationtech.jts.io.WKBWriter;
 
 public class ConstructorsTest {
 
+  @Test
+  public void geomFromWKTKeepsTaggedDimensionsWhoseValuesAreAllNaN() throws ParseException {
+    // WKT, layout it is stored with
+    Object[][] cases = {
+      {"POINT Z (1 2 NaN)", CoordinateType.XYZ},
+      {"POINT Z EMPTY", CoordinateType.XYZ},
+      {"LINESTRING ZM (0 0 NaN NaN, 1 1 NaN NaN)", CoordinateType.XYZM},
+      {"POLYGON Z ((0 0 NaN, 1 0 NaN, 1 1 NaN, 0 0 NaN))", CoordinateType.XYZ},
+      {"MULTIPOINT Z (0 0 NaN, 1 1 NaN)", CoordinateType.XYZ},
+      {"POINT M (1 2 NaN)", CoordinateType.XYM},
+      // Untagged WKT is read as before, including a Z in the old JTS syntax
+      {"POINT (1 2)", CoordinateType.XY},
+      {"POINT (1 2 3)", CoordinateType.XYZ},
+      {"POINT (1 2 NaN)", CoordinateType.XY},
+      {"POINT EMPTY", CoordinateType.XY},
+    };
+    for (Object[] c : cases) {
+      Geometry geometry = Constructors.geomFromWKT((String) c[0], 0);
+      assertEquals((String) c[0], c[1], GeometrySerializer.getCoordinateType(geometry));
+      Geometry stored = GeometrySerializer.deserialize(GeometrySerializer.serialize(geometry));
+      assertEquals((String) c[0], c[1], GeometrySerializer.getCoordinateType(stored));
+    }
+    Geometry ewkt = Constructors.geomFromEWKT("SRID=4326;POINT Z (1 2 NaN)");
+    assertEquals(4326, ewkt.getSRID());
+    assertEquals(CoordinateType.XYZ, GeometrySerializer.getCoordinateType(ewkt));
+  }
+
+  @Test
+  public void geomFromWKBReadsAPointWithOneNaNOrdinate() throws ParseException {
+    Geometry nanX =
+        Constructors.geomFromWKB(
+            WKBReader.hexToBytes("0101000000000000000000F87F000000000000F03F"));
+    assertFalse(nanX.isEmpty());
+    assertTrue(Double.isNaN(nanX.getCoordinate().getX()));
+    assertEquals(1.0, nanX.getCoordinate().getY(), 0.0);
+    // POINT (NaN NaN) is how WKB encodes POINT EMPTY
+    assertTrue(
+        Constructors.geomFromWKB(WKBReader.hexToBytes("0101000000000000000000F87F000000000000F87F"))
+            .isEmpty());
+  }
+
   @Test
   public void geomFromWKT() throws ParseException {
     assertNull(Constructors.geomFromWKT(null, 0));
diff --git a/common/src/test/java/org/apache/sedona/common/PredicatesTest.java b/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
index c465b759262..f7687c5b68b 100644
--- a/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
+++ b/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
@@ -44,6 +44,7 @@ import org.locationtech.jts.geom.impl.CoordinateArraySequence;
 import org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory;
 import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
 import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKBWriter;
 
 public class PredicatesTest extends TestBase {
 
@@ -596,6 +597,22 @@ public class PredicatesTest extends TestBase {
             geomFromEWKT("MULTIPOINT((0 0))"), geomFromEWKT("GEOMETRYCOLLECTION(POINT(0 0))")));
   }
 
+  @Test
+  public void testEqualsIdenticalComparesTaggedAllNaNZFromWKT() throws ParseException {
+    Geometry xy = Constructors.geomFromWKT("POINT (1 2)", 0);
+    Geometry z = Constructors.geomFromWKT("POINT Z (1 2 NaN)", 0);
+    assertFalse(Predicates.equalsIdentical(xy, z));
+    assertFalse(
+        Predicates.equalsIdentical(
+            GeometrySerializer.deserialize(GeometrySerializer.serialize(xy)), z));
+    Geometry zFromWkb = Constructors.geomFromWKB(new WKBWriter(3, true).write(z));
+    assertTrue(Predicates.equalsIdentical(zFromWkb, z));
+    assertFalse(
+        Predicates.equalsIdentical(
+            Constructors.geomFromWKT("POINT EMPTY", 0),
+            Constructors.geomFromWKT("POINT Z EMPTY", 0)));
+  }
+
   @Test
   public void testEqualsIdenticalMatchesSerializedRoundTrip() throws ParseException {
     // WKT parsing stores 2D coordinates with a NaN Z ordinate, while the serializer reads them
diff --git a/pom.xml b/pom.xml
index 61993e67f98..51ed8d59d08 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-2</jts.io.patch.version>
+        <jts.io.patch.version>1.21.0-datasyslab-3</jts.io.patch.version>
         <jts2geojson.version>0.16.1</jts2geojson.version>
         <spatial4j.version>0.8</spatial4j.version>
 
diff --git a/python/tests/geopandas/test_geoseries.py b/python/tests/geopandas/test_geoseries.py
index a25c8b7268d..b7572eaeefd 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -8039,11 +8039,10 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
             ]
         )
 
-        # JTS cannot distinguish empty XY and XYZ sequences. M and ZM sequence
-        # metadata is explicit, so the GeometryUDT serializers retain it.
+        # WKT tagged Z, M or ZM keeps that layout, even for empty geometries.
         self.check_pd_series_equal(
             empty_2d.geom_equals_identical(empty_dimensional, align=False),
-            pd.Series([True, True, True, False, False], dtype=bool),
+            pd.Series([False, False, False, False, False], dtype=bool),
         )
 
         nan_first_z = GeoSeries.from_wkt(["LINESTRING Z (0 0 NaN, 1 1 2)"])
@@ -8057,10 +8056,10 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
 
         all_nan_z = GeoSeries.from_wkt(["LINESTRING Z (0 0 NaN, 1 1 NaN)"])
 
-        # An all-NaN Z sequence remains indistinguishable from ordinary XY in JTS.
+        # A tagged Z keeps the XYZ layout even when every Z value is NaN.
         self.check_pd_series_equal(
             all_nan_z.geom_equals_identical(two_dimensional, align=False),
-            pd.Series([True], dtype=bool),
+            pd.Series([False], dtype=bool),
         )
 
         mixed_dimension = GeoSeries([MultiPoint([Point(0, 0), Point(1, 1, 2)])])