Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -158,11 +158,21 @@ public void testProjJsonRoundTrip_Geographic_NAD83_4269() throws FactoryExceptio
assertProjJsonRoundTrip(4269);
}

@Test
public void testProjJsonRoundTrip_Geographic_NAD27_4267() throws FactoryException {
assertProjJsonRoundTrip(4267);
}

@Test
public void testProjJsonRoundTrip_TransverseMercator_32617() throws FactoryException {
assertProjJsonRoundTrip(32617);
}

@Test
public void testProjJsonRoundTrip_TransverseMercator_NAD27_26711() throws FactoryException {
assertProjJsonRoundTrip(26711);
}

@Test
public void testProjJsonRoundTrip_PseudoMercator_3857() throws FactoryException {
assertProjJsonRoundTrip(3857);
Expand Down Expand Up @@ -361,11 +371,21 @@ public void testWkt2RoundTrip_Geographic_NAD83_4269() throws FactoryException {
assertWkt2RoundTrip(4269);
}

@Test
public void testWkt2RoundTrip_Geographic_NAD27_4267() throws FactoryException {
assertWkt2RoundTrip(4267);
}

@Test
public void testWkt2RoundTrip_TransverseMercator_32617() throws FactoryException {
assertWkt2RoundTrip(32617);
}

@Test
public void testWkt2RoundTrip_TransverseMercator_NAD27_26711() throws FactoryException {
assertWkt2RoundTrip(26711);
}

@Test
public void testWkt2RoundTrip_PseudoMercator_3857() throws FactoryException {
assertWkt2RoundTrip(3857);
Expand Down Expand Up @@ -447,13 +467,13 @@ public void testWkt2RoundTrip_ObliqueStereographic_28992() throws FactoryExcepti
}

// ---------------------------------------------------------------------------
// PROJJSON import failures — spherical datums not parseable after round-trip
// Spherical datum coverage
// ---------------------------------------------------------------------------

@Test
public void testProjJsonRoundTrip_LambertAzimuthalEqualArea_Spherical_2163_importFails()
public void testProjJsonRoundTrip_LambertAzimuthalEqualArea_Spherical_2163()
throws FactoryException {
assertProjJsonImportFails(2163);
assertProjJsonRoundTrip(2163);
}

@Test
Expand Down Expand Up @@ -534,6 +554,10 @@ private void assertProjJsonRoundTrip(int epsg) throws FactoryException {
// First export from EPSG
String export1 = RasterAccessors.crs(raster1, "projjson");
assertNotNull("EPSG:" + epsg + " export to PROJJSON should not be null", export1);
assertEquals(
"EPSG:" + epsg + " default CRS export should equal explicit PROJJSON",
export1,
RasterAccessors.crs(raster1));

// Re-import from PROJJSON string and re-export
GridCoverage2D raster2 = RasterEditors.setCrs(baseRaster, export1);
Expand Down Expand Up @@ -580,29 +604,6 @@ private void assertWkt2RoundTrip(int epsg) throws FactoryException {
export3);
}

/**
* Assert that PROJJSON export succeeds but re-import fails (spherical datum CRS that proj4sedona
* can export but GeoTools cannot re-parse).
*/
private void assertProjJsonImportFails(int epsg) throws FactoryException {
GridCoverage2D baseRaster = RasterConstructors.makeEmptyRaster(1, 4, 4, 0, 0, 1);
GridCoverage2D raster1 = RasterEditors.setCrs(baseRaster, "EPSG:" + epsg);

// Export should succeed
String exported = RasterAccessors.crs(raster1, "projjson");
assertNotNull("EPSG:" + epsg + " export to PROJJSON should succeed", exported);

// Re-import should fail
Exception thrown =
assertThrows(
"EPSG:" + epsg + " PROJJSON re-import should fail for spherical datum",
IllegalArgumentException.class,
() -> RasterEditors.setCrs(baseRaster, exported));
assertTrue(
"Error message should mention CRS parsing",
thrown.getMessage().contains("Cannot parse CRS string"));
}

/**
* Assert a full WKT1 format round trip: EPSG → RS_CRS("wkt1") → RS_SetCRS → RS_CRS("wkt1"). WKT1
* includes AUTHORITY["EPSG","xxxx"] so SRID is always preserved.
Expand Down
7 changes: 6 additions & 1 deletion docs/tutorial/files/geoparquet-sedona-spark.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,12 @@ When no `geoparquet.crs` option is explicitly provided, Sedona will automaticall
* If geometries in a column have mixed SRIDs, the `crs` field defaults to `null`.
* If an explicit `geoparquet.crs` or `geoparquet.crs.<column_name>` option is provided, it always takes precedence over the SRID-derived CRS.

Sedona geoparquet reader and writer do NOT check the axis order (lon/lat or lat/lon) and assume they are handled by the users themselves when writing / reading the files. You can always use [`ST_FlipCoordinates`](../../api/sql/Geometry-Editors/ST_FlipCoordinates.md) to swap the axis order of your geometries.
GeoParquet stores WKB coordinates in `(x, y)` order. Per the GeoParquet 1.1
specification, this ordering overrides any axis order declared by the CRS. Sedona
therefore preserves the authoritative CRS axis metadata and does not reorder geometry
coordinates while reading or writing GeoParquet. You can use
[`ST_FlipCoordinates`](../../api/sql/Geometry-Editors/ST_FlipCoordinates.md) when the
coordinates themselves need to be swapped.

## Save GeoParquet with Covering Metadata

Expand Down
5 changes: 4 additions & 1 deletion docs/tutorial/files/geoparquet-sedona-spark.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ df.write.format("geoparquet")
* 若同一列中的几何对象 SRID 不一致,`crs` 字段默认置为 `null`。
* 若显式提供了 `geoparquet.crs` 或 `geoparquet.crs.<column_name>` 选项,则始终优先于 SRID 推导出的 CRS。

Sedona 的 GeoParquet 读写器**不会**校验坐标轴顺序(lon/lat 还是 lat/lon),假定使用者在读写时自行处理。可以使用 [`ST_FlipCoordinates`](../../api/sql/Geometry-Editors/ST_FlipCoordinates.md) 来交换几何对象的坐标轴顺序。
GeoParquet 以 `(x, y)` 顺序存储 WKB 坐标。根据 GeoParquet 1.1 规范,此顺序会覆盖 CRS
中声明的坐标轴顺序。因此,Sedona 会保留权威的 CRS 坐标轴元数据,并且在读写 GeoParquet
时不会重新排列几何坐标。如果坐标本身需要交换,可以使用
[`ST_FlipCoordinates`](../../api/sql/Geometry-Editors/ST_FlipCoordinates.md)。

## 写出带 covering 元数据的 GeoParquet

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
<scala-collection-compat.version>2.5.0</scala-collection-compat.version>
<geoglib.version>1.52</geoglib.version>
<caffeine.version>2.9.2</caffeine.version>
<proj4sedona.version>0.1.2</proj4sedona.version>
<proj4sedona.version>0.1.5</proj4sedona.version>

<geotools.scope>provided</geotools.scope>
<!-- Because it's not in Maven central, make it provided by default -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ class CRSTransformProj4Test extends TestBaseScala {
assertEquals(4180998.88, result.getCoordinate.y, COORD_TOLERANCE)
}

it("should apply the RT90 datum shift for EPSG:3021 to EPSG:3006 (GH-3161)") {
val result = sparkSession
.sql("SELECT ST_Transform(ST_Point(1272691.622, 6404578.16), 'epsg:3021', 'epsg:3006')")
.first()
.getAs[Geometry](0)

assertNotNull(result)
assertEquals(3006, result.getSRID)
// PostGIS reference reported in GH-3161. The one-metre suite tolerance allows for
// projection-algorithm differences while guarding against the reported 180-metre error.
assertEquals(320728.44351324334, result.getCoordinate.x, COORD_TOLERANCE)
assertEquals(6400228.104394391, result.getCoordinate.y, COORD_TOLERANCE)
}

it("should transform using PROJ string") {
val projString =
"+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +no_defs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,35 @@ class geoparquetIOTests extends TestBaseScala with BeforeAndAfterAll {
}
}

describe("GeoParquetMetaData.sridToProjJson") {
it("should preserve authoritative polar axis metadata") {
implicit val formats: org.json4s.Formats = org.json4s.DefaultFormats
val crs =
GeoParquetMetaData.sridToProjJson(32661).getOrElse(fail("EPSG:32661 should resolve"))

assert((crs \ "id" \ "authority").extract[String] == "EPSG")
assert((crs \ "id" \ "code").extract[Int] == 32661)

val axes = (crs \ "coordinate_system" \ "axis").children
assert(axes.map(axis => (axis \ "abbreviation").extract[String]) == List("N", "E"))
assert(axes.map(axis => (axis \ "direction").extract[String]) == List("south", "south"))
assert(
axes.map(axis => (axis \ "meridian" \ "longitude" \ "value").extract[Double]) ==
List(180.0, 90.0))
}

it("should generate and recover NAD27 projected CRS metadata") {
implicit val formats: org.json4s.Formats = org.json4s.DefaultFormats
val crs =
GeoParquetMetaData.sridToProjJson(26711).getOrElse(fail("EPSG:26711 should resolve"))

assert((crs \ "type").extract[String] == "ProjectedCRS")
assert((crs \ "id" \ "authority").extract[String] == "EPSG")
assert((crs \ "id" \ "code").extract[Int] == 26711)
assert(GeoParquetMetaData.extractSridFromCrs(Some(crs)) == 26711)
}
}

describe("GeoParquetMetaData.extractSridFromCrs") {
it("should return 4326 when CRS is omitted (None)") {
assert(GeoParquetMetaData.extractSridFromCrs(None) == 4326)
Expand Down
Loading