Skip to content

Commit a1e7ebf

Browse files
committed
fix: embed EPSG in Zarr attrs for TiTiler CRS detection
TiTiler defaults to EPSG:4326 when ds.rio.crs is None, causing validation errors on UTM coordinates. Extract helper to embed proj:epsg in attrs so rioxarray reconstructs CRS after Zarr I/O.
1 parent 055f090 commit a1e7ebf

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

src/eopf_geozarr/conversion/geozarr.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ def iterative_copy(
352352
return dt_result if isinstance(dt_result, xr.DataTree) else xr.DataTree(dt_result)
353353

354354

355+
def _embed_crs_in_attrs(ds: xr.Dataset) -> None:
356+
"""Embed EPSG code in dataset attrs for Zarr persistence (modifies in-place)."""
357+
if epsg_code := ds.rio.crs.to_epsg():
358+
ds.attrs["proj:epsg"] = epsg_code
359+
ds.attrs["proj:code"] = f"EPSG:{epsg_code}"
360+
361+
355362
def prepare_dataset_with_crs_info(
356363
ds: xr.Dataset, reference_crs: str | None = None
357364
) -> xr.Dataset:
@@ -380,6 +387,7 @@ def prepare_dataset_with_crs_info(
380387
print(f" Adding CRS information: {reference_crs}")
381388
ds = ds.rio.write_crs(reference_crs)
382389
ds.attrs["grid_mapping"] = "spatial_ref"
390+
_embed_crs_in_attrs(ds)
383391

384392
# Ensure spatial_ref variable has proper attributes
385393
if "spatial_ref" in ds:
@@ -1003,9 +1011,10 @@ def create_overview_dataset_all_vars(
10031011
# Create overview dataset
10041012
overview_ds = xr.Dataset(overview_data_vars, coords=overview_coords)
10051013

1006-
# Set CRS using rioxarray first
1014+
# Set CRS using rioxarray
10071015
overview_ds.rio.write_crs(native_crs, inplace=True)
10081016
overview_ds.attrs["grid_mapping"] = grid_mapping_var_name
1017+
_embed_crs_in_attrs(overview_ds)
10091018

10101019
# Add grid_mapping variable after setting CRS
10111020
# TODO: refactor? grid mapping attributes and variables are handled
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Tests for CRS metadata embedding in Zarr attributes."""
2+
3+
import numpy as np
4+
import pytest
5+
import xarray as xr
6+
7+
from eopf_geozarr.conversion.geozarr import prepare_dataset_with_crs_info
8+
9+
EPSG_32632 = 32632
10+
11+
12+
@pytest.fixture
13+
def utm_dataset():
14+
"""Minimal UTM dataset."""
15+
return xr.Dataset(
16+
{"data": (["y", "x"], np.random.rand(10000, 11000))},
17+
coords={
18+
"x": np.arange(0, 110000, 10, dtype=np.float64),
19+
"y": np.arange(5300000, 5200000, -10, dtype=np.float64),
20+
},
21+
)
22+
23+
24+
@pytest.fixture
25+
def zarr_roundtrip(utm_dataset, tmp_path):
26+
"""Dataset after Zarr write/read."""
27+
ds = prepare_dataset_with_crs_info(utm_dataset, reference_crs=f"EPSG:{EPSG_32632}")
28+
path = tmp_path / "test.zarr"
29+
ds.drop_encoding().to_zarr(path, mode="w", zarr_format=3, consolidated=True)
30+
return xr.open_zarr(path, zarr_format=3, consolidated=True)
31+
32+
33+
def test_crs_attrs_embedded(utm_dataset):
34+
"""CRS metadata written to attrs."""
35+
ds = prepare_dataset_with_crs_info(utm_dataset, reference_crs=f"EPSG:{EPSG_32632}")
36+
assert ds.attrs["proj:epsg"] == EPSG_32632
37+
assert ds.attrs["proj:code"] == f"EPSG:{EPSG_32632}"
38+
39+
40+
def test_crs_persists_through_zarr(zarr_roundtrip):
41+
"""CRS survives Zarr cycle."""
42+
assert zarr_roundtrip.attrs["proj:epsg"] == EPSG_32632
43+
44+
45+
def test_rioxarray_reads_crs(zarr_roundtrip):
46+
"""rioxarray reads CRS."""
47+
assert zarr_roundtrip.rio.crs.to_epsg() == EPSG_32632

0 commit comments

Comments
 (0)