diff --git a/CHANGES.md b/CHANGES.md index 7086b1e9..a5e4667b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### Improvements +- Write "Unknown cartesian CRS" when saving gdf without a CRS to GPKG (#368). - `read_arrow` and `open_arrow` now provide [GeoArrow-compliant extension metadata](https://geoarrow.org/extension-types.html), including the CRS, when using GDAL 3.8 or higher (#366). diff --git a/pyogrio/geopandas.py b/pyogrio/geopandas.py index 1cbcb113..41b498d2 100644 --- a/pyogrio/geopandas.py +++ b/pyogrio/geopandas.py @@ -537,14 +537,19 @@ def write_dataframe( geometry_type = f"{geometry_type} Z" crs = None - if geometry_column is not None and geometry.crs: - # TODO: this may need to be WKT1, due to issues - # if possible use EPSG codes instead - epsg = geometry.crs.to_epsg() - if epsg: - crs = f"EPSG:{epsg}" - else: - crs = geometry.crs.to_wkt(WktVersion.WKT1_GDAL) + if geometry_column is not None: + if geometry.crs: + # TODO: this may need to be WKT1, due to issues + # if possible use EPSG codes instead + epsg = geometry.crs.to_epsg() + if epsg: + crs = f"EPSG:{epsg}" + else: + crs = geometry.crs.to_wkt(WktVersion.WKT1_GDAL) + elif driver == "GPKG": + # In GPKG, None crs must be replaced by "Undefined Cartesian SRS", otherwise + # the default "Undefined geographic SRS" will be used. + crs = 'LOCAL_CS["Undefined Cartesian SRS"]' # If there is geometry data, prepare it to be written if geometry_column is not None: diff --git a/pyogrio/tests/test_geopandas_io.py b/pyogrio/tests/test_geopandas_io.py index 412b95b6..a8f72d31 100644 --- a/pyogrio/tests/test_geopandas_io.py +++ b/pyogrio/tests/test_geopandas_io.py @@ -1,6 +1,8 @@ import contextlib from datetime import datetime import os +import sqlite3 + import numpy as np import pytest @@ -993,6 +995,39 @@ def test_write_dataframe_append(tmp_path, naturalearth_lowres, ext): assert len(read_dataframe(output_path)) == 354 +@pytest.mark.parametrize("ext", [".gpkg", ".shp"]) +def test_write_dataframe_crs_None(tmp_path, ext): + input_gdf = gp.GeoDataFrame(geometry=[Point(0, 1)], crs=None) + output_path = tmp_path / f"test{ext}" + + write_dataframe(input_gdf, output_path) + + assert os.path.exists(output_path) + result_gdf = read_dataframe(output_path) + + if ext == ".gpkg": + # In GPKG, cartesian data without specified crs needs to get srs_id -1 according + # to the specs: https://www.geopackage.org/spec/#r11 + # Verify the name of the projection in de GeoDataFrame read. + assert result_gdf.crs == 'LOCAL_CS["Undefined Cartesian SRS"]' + + # Verify that srs_id == -1 in the output GPKG file + con = sqlite3.connect(output_path) + try: + result = con.execute( + "SELECT srs_id FROM gpkg_geometry_columns WHERE table_name = 'test'" + ) + result_srs_id = result.fetchone() + finally: + con.close() + + assert result_srs_id is not None + assert result_srs_id[0] == -1 + + else: + assert result_gdf.crs is None + + @pytest.mark.parametrize("spatial_index", [False, True]) def test_write_dataframe_gdal_options(tmp_path, naturalearth_lowres, spatial_index): df = read_dataframe(naturalearth_lowres)