Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5f8a369
Use published Datasyslab JTS fork
jiayuasu Sep 15, 2026
fa2e142
Adapt geometry contracts for JTS 1.21
jiayuasu Sep 15, 2026
2fb21d2
Exclude Spark JTS from module classpaths
jiayuasu Sep 15, 2026
ae576dd
Document Spark 4.1 JTS setup
jiayuasu Sep 15, 2026
7e90e04
Tighten JTS compatibility checks
jiayuasu Sep 15, 2026
c92b3fe
Preserve declared geometry dimensions across binary serialization
jiayuasu Sep 15, 2026
49de25d
Use isolated JTS IO patch
jiayuasu Sep 15, 2026
672a937
Merge isolated JTS IO dependency
jiayuasu Sep 15, 2026
b074b70
Route shared geometry reader through IO patch
jiayuasu Sep 15, 2026
6c622ff
Cover empty WKB coordinate layouts
jiayuasu Sep 15, 2026
d3cc5da
Merge empty WKB layout coverage
jiayuasu Sep 15, 2026
46e77af
Merge remote-tracking branch 'origin/master' into fix/declared-geomet…
jiayuasu Sep 15, 2026
c65badc
Confine declared coordinate allocations to WKB parsing
jiayuasu Sep 15, 2026
874e608
Document and test collection dimension requirements
jiayuasu Sep 15, 2026
2bb6199
Test XY point generation after geometry decoding
jiayuasu Sep 15, 2026
9c3ca32
Clarify declared layout requirements in collection docs
jiayuasu Sep 15, 2026
87a58cb
Preserve WKB dimensions across Spark readers
jiayuasu Sep 15, 2026
681929c
Set the empty geometry flag in the GeoPackage fixture
jiayuasu Sep 15, 2026
364ead9
Preserve empty components when setting SRID
jiayuasu Sep 16, 2026
8a012e3
Cover empty members in multipart SRID copies
jiayuasu Sep 16, 2026
72e6832
Separate geometry copying from WKB dimension handling
jiayuasu Sep 16, 2026
2a793d4
Include multipart SRID copy regression coverage
jiayuasu Sep 16, 2026
eb93164
Test empty polygon copies and interior rings when setting SRID
jiayuasu Sep 17, 2026
7dfe3fd
Test empty polygon holes through ST_SetSRID
jiayuasu Sep 17, 2026
b7970fd
Use isolated JTS geometry copier when setting SRID
jiayuasu Sep 17, 2026
455f95c
Use isolated JTS copying in dimension preservation
jiayuasu Sep 17, 2026
7d1b8d3
Use shared JTS coordinate layout declarations
jiayuasu Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static Geometry geomFromWKB(byte[] wkb) throws ParseException {
}

public static Geometry geomFromWKB(byte[] wkb, int SRID) throws ParseException {
Geometry geom = new WKBReader().read(wkb);
Geometry geom = WKBReader.forDeclaredDimensions().read(wkb);
if (geom.getFactory().getSRID() != geom.getSRID() || (SRID >= 0 && geom.getSRID() != SRID)) {
// Make sure that the geometry and the geometry factory have the correct SRID
if (SRID < 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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;
Expand Down Expand Up @@ -1103,12 +1104,7 @@ public static Geometry setSRID(Geometry geometry, int srid) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.datasyslab.jts.geom.impl.DeclaredCoordinateSequence;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.CoordinateXYM;
import org.locationtech.jts.geom.CoordinateXYZM;
import org.locationtech.jts.geom.impl.CoordinateArraySequence;

class ByteBufferGeometryBuffer implements GeometryBuffer {
private CoordinateType coordinateType = CoordinateType.XY;
Expand Down Expand Up @@ -126,20 +126,20 @@ public CoordinateSequence getCoordinate(int offset) {
switch (coordinateType) {
case XY:
coordinates[0] = new CoordinateXY(x, y);
return new CoordinateArraySequence(coordinates, 2, 0);
return new DeclaredCoordinateSequence(coordinates, 2, 0);
case XYZ:
z = byteBuffer.getDouble(offset + 16);
coordinates[0] = new Coordinate(x, y, z);
return new CoordinateArraySequence(coordinates, 3, 0);
return new DeclaredCoordinateSequence(coordinates, 3, 0);
case XYM:
m = byteBuffer.getDouble(offset + 16);
coordinates[0] = new CoordinateXYM(x, y, m);
return new CoordinateArraySequence(coordinates, 3, 1);
return new DeclaredCoordinateSequence(coordinates, 3, 1);
case XYZM:
z = byteBuffer.getDouble(offset + 16);
m = byteBuffer.getDouble(offset + 24);
coordinates[0] = new CoordinateXYZM(x, y, z, m);
return new CoordinateArraySequence(coordinates, 4, 1);
return new DeclaredCoordinateSequence(coordinates, 4, 1);
default:
throw new IllegalStateException("coordinateType was not configured properly");
}
Expand Down Expand Up @@ -240,7 +240,7 @@ public CoordinateSequence getCoordinates(int offset, int numCoordinates) {
default:
throw new IllegalStateException("coordinateType was not configured properly");
}
return new CoordinateArraySequence(coordinates, dimension, measures);
return new DeclaredCoordinateSequence(coordinates, dimension, measures);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.sedona.common.geometrySerde;

import org.datasyslab.jts.geom.impl.DeclaredCoordinateSequence;
import org.datasyslab.jts.geom.impl.DeclaredCoordinateSequenceFactory;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
Expand Down Expand Up @@ -476,12 +478,14 @@ private static void collectCoordinateDimensions(

// Measures are explicit CoordinateSequence metadata. A Z dimension is not always explicit:
// JTS's default sequence factory represents ordinary XY coordinates as dimension 3 with NaN Z.
// XYZM is unambiguous, while XYZ is recoverable only when at least one Z value is finite. An
// ambiguous sequence does not constrain a multipart geometry whose other members establish the
// shared layout.
// Trusted binary layouts and XYZM are unambiguous. Unmarked XYZ is recoverable only when at
// least one Z value is non-NaN. An ambiguous sequence does not constrain a multipart geometry
// whose other members establish the shared layout.
CoordinateType coordinateType = null;
if (measures > 0) {
coordinateType = spatialDimensions > 2 ? CoordinateType.XYZM : CoordinateType.XYM;
} else if (coordinates instanceof DeclaredCoordinateSequence && spatialDimensions == 3) {
coordinateType = CoordinateType.XYZ;
} else if (spatialDimensions == 2) {
coordinateType = CoordinateType.XY;
} else {
Expand All @@ -500,7 +504,7 @@ private static int alignedOffset(int offset) {
}

private static GeometryFactory createGeometryFactory(int srid) {
return new GeometryFactory(PRECISION_MODEL, srid);
return new GeometryFactory(PRECISION_MODEL, srid, DeclaredCoordinateSequenceFactory.instance());
}

private static Polygon createEmptyPolygon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
package org.apache.sedona.common.geometrySerde;

import java.lang.reflect.Field;
import org.datasyslab.jts.geom.impl.DeclaredCoordinateSequence;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.CoordinateXYM;
import org.locationtech.jts.geom.CoordinateXYZM;
import org.locationtech.jts.geom.impl.CoordinateArraySequence;
import sun.misc.Unsafe;

class UnsafeGeometryBuffer implements GeometryBuffer {
Expand Down Expand Up @@ -173,20 +173,20 @@ public CoordinateSequence getCoordinate(int offset) {
switch (coordinateType) {
case XY:
coordinates[0] = new CoordinateXY(x, y);
return new CoordinateArraySequence(coordinates, 2, 0);
return new DeclaredCoordinateSequence(coordinates, 2, 0);
case XYZ:
z = UNSAFE.getDouble(bytes, coordOffset + 16);
coordinates[0] = new Coordinate(x, y, z);
return new CoordinateArraySequence(coordinates, 3, 0);
return new DeclaredCoordinateSequence(coordinates, 3, 0);
case XYM:
m = UNSAFE.getDouble(bytes, coordOffset + 16);
coordinates[0] = new CoordinateXYM(x, y, m);
return new CoordinateArraySequence(coordinates, 3, 1);
return new DeclaredCoordinateSequence(coordinates, 3, 1);
case XYZM:
z = UNSAFE.getDouble(bytes, coordOffset + 16);
m = UNSAFE.getDouble(bytes, coordOffset + 24);
coordinates[0] = new CoordinateXYZM(x, y, z, m);
return new CoordinateArraySequence(coordinates, 4, 1);
return new DeclaredCoordinateSequence(coordinates, 4, 1);
default:
throw new IllegalStateException("coordinateType was not configured properly");
}
Expand Down Expand Up @@ -293,7 +293,7 @@ public CoordinateSequence getCoordinates(int offset, int numCoordinates) {
default:
throw new IllegalStateException("coordinateType was not configured properly");
}
return new CoordinateArraySequence(coordinates, dimension, measures);
return new DeclaredCoordinateSequence(coordinates, dimension, measures);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,7 @@ public Geometry readWkt(String line) throws ParseException {
public Geometry readWkb(String line) throws ParseException {
final String[] columns = line.split(splitter.getDelimiter());
final byte[] aux = WKBReader.hexToBytes(columns[this.startOffset]);
// For some unknown reasons, the wkb reader cannot be used in transient variable like the wkt
// reader.
WKBReader wkbReader = new WKBReader();
Geometry geometry = wkbReader.read(aux);
Geometry geometry = WKBReader.forDeclaredDimensions().read(aux);
if (geometry.getSRID() != geometry.getFactory().getSRID()) {
// Make sure that the geometry factory has the correct SRID when the parsed WKB
// contains a non-zero SRID (EWKB)
Expand Down
162 changes: 162 additions & 0 deletions common/src/test/java/org/apache/sedona/common/FunctionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
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;
Expand Down Expand Up @@ -4676,6 +4678,166 @@ public void setSRIDEmptyGeometries() throws ParseException {
}
}

@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));
Expand Down
Loading
Loading