From a6fc78b5b3de2628a9b5d01a08dea9aaef9c20d2 Mon Sep 17 00:00:00 2001 From: Pieter Roggemans Date: Fri, 1 Mar 2024 17:34:18 +0100 Subject: [PATCH 1/5] ENH: Write Unknown cartesian CRS when saving gdf without a CRS to GPKG --- pyogrio/geopandas.py | 17 ++++++++++------- pyogrio/tests/test_geopandas_io.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pyogrio/geopandas.py b/pyogrio/geopandas.py index 2f9be6ac..1cd402e1 100644 --- a/pyogrio/geopandas.py +++ b/pyogrio/geopandas.py @@ -537,14 +537,17 @@ 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}" + 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) else: - crs = geometry.crs.to_wkt(WktVersion.WKT1_GDAL) + 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 20c3d9f4..b2f97e62 100644 --- a/pyogrio/tests/test_geopandas_io.py +++ b/pyogrio/tests/test_geopandas_io.py @@ -910,6 +910,24 @@ def test_write_dataframe_gpkg_multiple_layers(tmp_path, naturalearth_lowres): ) +def test_write_dataframe_gpkg_no_crs(tmp_path, naturalearth_lowres): + input_gdf = gp.GeoDataFrame(geometry=[Point(0, 1)], crs=None) + output_path = tmp_path / "test.gpkg" + + write_dataframe(input_gdf, output_path) + + assert os.path.exists(output_path) + result_gdf = read_dataframe(output_path) + + # None crs is replaced by "Undefined Cartesian SRS" in GPKG. + input_gdf.crs = 'LOCAL_CS["Undefined Cartesian SRS"]' + assert_geodataframe_equal( + result_gdf, + input_gdf, + # check_index_type=False, + ) + + @pytest.mark.parametrize("ext", ALL_EXTS) def test_write_dataframe_append(tmp_path, naturalearth_lowres, ext): if ext == ".fgb" and __gdal_version__ <= (3, 5, 0): From 85af539dffc7c4867cba306241be8acdd34d9cae Mon Sep 17 00:00:00 2001 From: Pieter Roggemans Date: Fri, 1 Mar 2024 17:37:08 +0100 Subject: [PATCH 2/5] Update CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index fe44c5fe..df3e5569 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### Improvements - `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. +- Write "Unknown cartesian CRS" when saving gdf without a CRS to GPKG (#368). ### Bug fixes From cef60e1ede83e3018612001d43f219974dab1e0a Mon Sep 17 00:00:00 2001 From: Pieter Roggemans Date: Fri, 1 Mar 2024 17:39:18 +0100 Subject: [PATCH 3/5] Update CHANGES.md --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index df3e5569..833b11ef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,9 @@ ### Improvements -- `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. +- `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). - Write "Unknown cartesian CRS" when saving gdf without a CRS to GPKG (#368). ### Bug fixes From bde470bf97e5d10ecdbf7943401fc63333516b52 Mon Sep 17 00:00:00 2001 From: Pieter Roggemans Date: Fri, 1 Mar 2024 18:00:31 +0100 Subject: [PATCH 4/5] Only for GPKG + improve test --- pyogrio/geopandas.py | 4 +++- pyogrio/tests/test_geopandas_io.py | 35 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pyogrio/geopandas.py b/pyogrio/geopandas.py index 1cd402e1..e29e941b 100644 --- a/pyogrio/geopandas.py +++ b/pyogrio/geopandas.py @@ -546,7 +546,9 @@ def write_dataframe( crs = f"EPSG:{epsg}" else: crs = geometry.crs.to_wkt(WktVersion.WKT1_GDAL) - else: + 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 diff --git a/pyogrio/tests/test_geopandas_io.py b/pyogrio/tests/test_geopandas_io.py index b2f97e62..438cbbf5 100644 --- a/pyogrio/tests/test_geopandas_io.py +++ b/pyogrio/tests/test_geopandas_io.py @@ -910,24 +910,6 @@ def test_write_dataframe_gpkg_multiple_layers(tmp_path, naturalearth_lowres): ) -def test_write_dataframe_gpkg_no_crs(tmp_path, naturalearth_lowres): - input_gdf = gp.GeoDataFrame(geometry=[Point(0, 1)], crs=None) - output_path = tmp_path / "test.gpkg" - - write_dataframe(input_gdf, output_path) - - assert os.path.exists(output_path) - result_gdf = read_dataframe(output_path) - - # None crs is replaced by "Undefined Cartesian SRS" in GPKG. - input_gdf.crs = 'LOCAL_CS["Undefined Cartesian SRS"]' - assert_geodataframe_equal( - result_gdf, - input_gdf, - # check_index_type=False, - ) - - @pytest.mark.parametrize("ext", ALL_EXTS) def test_write_dataframe_append(tmp_path, naturalearth_lowres, ext): if ext == ".fgb" and __gdal_version__ <= (3, 5, 0): @@ -948,6 +930,23 @@ 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) + + # In GPKG, None crs is replaced by "Undefined Cartesian SRS". + if ext == ".gpkg": + assert result_gdf.crs == 'LOCAL_CS["Undefined Cartesian SRS"]' + 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) From fb3271252f8cc5c671e98a97ffe56df43f052134 Mon Sep 17 00:00:00 2001 From: Pieter Roggemans Date: Mon, 4 Mar 2024 16:16:51 +0100 Subject: [PATCH 5/5] Explicitly check if srs_id == -1 in output gpkg file --- pyogrio/tests/test_geopandas_io.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pyogrio/tests/test_geopandas_io.py b/pyogrio/tests/test_geopandas_io.py index 438cbbf5..79d60773 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 @@ -940,9 +942,25 @@ def test_write_dataframe_crs_None(tmp_path, ext): assert os.path.exists(output_path) result_gdf = read_dataframe(output_path) - # In GPKG, None crs is replaced by "Undefined Cartesian SRS". 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