Skip to content

Commit 3773858

Browse files
authored
Merge pull request #43 from stac-utils/guhidalgo/inferepsg4326fromcf
Infer epsg:4326 CRS if latitude and longitude coordinates detected
2 parents 31bf517 + 0d1df2d commit 3773858

File tree

5 files changed

+223
-15
lines changed

5 files changed

+223
-15
lines changed

.github/workflows/continuous-integration.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python-version: ${{ matrix.python-version }}
2323

2424
- name: Install dependencies
25-
run: python -m pip install .[dev]
25+
run: python -m pip install -r requirements.dev.txt && python -m pip install .[dev] --no-deps
2626

2727
- name: Run tests
2828
run: pytest tests -v

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ requires-python = ">=3.10"
2828
dynamic = ["version", "description"]
2929

3030
[project.optional-dependencies]
31-
dev = ["pytest", "pre-commit", "shapely", "cftime", "kerchunk", "h5netcdf", "fsspec"]
31+
dev = ["pytest", "pre-commit", "shapely", "cftime", "kerchunk", "h5netcdf", "fsspec", "pooch"]
3232

3333
[project.scripts]
3434
xstac = "xstac._generate:main"

requirements.dev.txt

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# This file was autogenerated by uv via the following command:
2+
# uv pip compile -o ./requirements.dev.txt ./pyproject.toml --extra dev
3+
aiohappyeyeballs==2.4.6
4+
# via aiohttp
5+
aiohttp==3.11.12
6+
# via fsspec
7+
aiosignal==1.3.2
8+
# via aiohttp
9+
annotated-types==0.7.0
10+
# via pydantic
11+
asciitree==0.3.3
12+
# via zarr
13+
async-timeout==5.0.1
14+
# via aiohttp
15+
attrs==25.1.0
16+
# via
17+
# aiohttp
18+
# jsonschema
19+
# referencing
20+
certifi==2025.1.31
21+
# via
22+
# pyproj
23+
# requests
24+
cf-xarray==0.10.0
25+
# via xstac (./pyproject.toml)
26+
cfgv==3.4.0
27+
# via pre-commit
28+
cftime==1.6.4.post1
29+
# via xstac (./pyproject.toml)
30+
charset-normalizer==3.4.1
31+
# via requests
32+
click==8.1.8
33+
# via dask
34+
cloudpickle==3.1.1
35+
# via dask
36+
dask==2025.2.0
37+
# via xstac (./pyproject.toml)
38+
distlib==0.3.9
39+
# via virtualenv
40+
exceptiongroup==1.2.2
41+
# via pytest
42+
fasteners==0.19
43+
# via zarr
44+
filelock==3.17.0
45+
# via virtualenv
46+
frozenlist==1.5.0
47+
# via
48+
# aiohttp
49+
# aiosignal
50+
fsspec==2025.2.0
51+
# via
52+
# xstac (./pyproject.toml)
53+
# dask
54+
# kerchunk
55+
h5netcdf==1.5.0
56+
# via xstac (./pyproject.toml)
57+
h5py==3.13.0
58+
# via h5netcdf
59+
identify==2.6.7
60+
# via pre-commit
61+
idna==3.10
62+
# via
63+
# requests
64+
# yarl
65+
importlib-metadata==8.6.1
66+
# via dask
67+
iniconfig==2.0.0
68+
# via pytest
69+
jsonschema==4.23.0
70+
# via xstac (./pyproject.toml)
71+
jsonschema-specifications==2024.10.1
72+
# via jsonschema
73+
kerchunk==0.2.7
74+
# via xstac (./pyproject.toml)
75+
locket==1.0.0
76+
# via partd
77+
multidict==6.1.0
78+
# via
79+
# aiohttp
80+
# yarl
81+
nodeenv==1.9.1
82+
# via pre-commit
83+
numcodecs==0.13.1
84+
# via
85+
# kerchunk
86+
# zarr
87+
numpy==2.2.3
88+
# via
89+
# xstac (./pyproject.toml)
90+
# cftime
91+
# dask
92+
# h5py
93+
# kerchunk
94+
# numcodecs
95+
# pandas
96+
# shapely
97+
# xarray
98+
# zarr
99+
packaging==24.2
100+
# via
101+
# dask
102+
# h5netcdf
103+
# pooch
104+
# pytest
105+
# xarray
106+
pandas==2.2.3
107+
# via
108+
# xstac (./pyproject.toml)
109+
# xarray
110+
partd==1.4.2
111+
# via dask
112+
platformdirs==4.3.6
113+
# via
114+
# pooch
115+
# virtualenv
116+
pluggy==1.5.0
117+
# via pytest
118+
pooch==1.8.2
119+
# via xstac (./pyproject.toml)
120+
pre-commit==4.1.0
121+
# via xstac (./pyproject.toml)
122+
propcache==0.3.0
123+
# via
124+
# aiohttp
125+
# yarl
126+
pydantic==2.10.6
127+
# via xstac (./pyproject.toml)
128+
pydantic-core==2.27.2
129+
# via pydantic
130+
pyproj==3.7.1
131+
# via xstac (./pyproject.toml)
132+
pystac==1.12.1
133+
# via xstac (./pyproject.toml)
134+
pytest==8.3.4
135+
# via xstac (./pyproject.toml)
136+
python-dateutil==2.9.0.post0
137+
# via
138+
# pandas
139+
# pystac
140+
pytz==2025.1
141+
# via pandas
142+
pyyaml==6.0.2
143+
# via
144+
# dask
145+
# pre-commit
146+
referencing==0.36.2
147+
# via
148+
# jsonschema
149+
# jsonschema-specifications
150+
requests==2.32.3
151+
# via pooch
152+
rpds-py==0.23.0
153+
# via
154+
# jsonschema
155+
# referencing
156+
shapely==2.0.7
157+
# via xstac (./pyproject.toml)
158+
six==1.17.0
159+
# via python-dateutil
160+
tomli==2.2.1
161+
# via pytest
162+
toolz==1.0.0
163+
# via
164+
# dask
165+
# partd
166+
typing-extensions==4.12.2
167+
# via
168+
# multidict
169+
# pydantic
170+
# pydantic-core
171+
# referencing
172+
tzdata==2025.1
173+
# via pandas
174+
ujson==5.10.0
175+
# via kerchunk
176+
urllib3==2.3.0
177+
# via requests
178+
virtualenv==20.29.2
179+
# via pre-commit
180+
xarray==2025.1.2
181+
# via
182+
# xstac (./pyproject.toml)
183+
# cf-xarray
184+
yarl==1.18.3
185+
# via aiohttp
186+
zarr==2.18.3
187+
# via
188+
# xstac (./pyproject.toml)
189+
# kerchunk
190+
zipp==3.21.0
191+
# via importlib-metadata

tests/test_xstac.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,28 @@ def test_from_pystac_object(ds_without_spatial_dims):
165165

166166

167167
@pytest.mark.parametrize(
168-
"ds",
168+
("ds", "expected"),
169169
[
170-
xr.Dataset(coords={"epsg": xr.DataArray(32633, name="epsg")}),
171-
xr.Dataset(coords={"proj:epsg": xr.DataArray(32633, name="proj:epsg")}),
172-
xr.Dataset(attrs={"crs": "epsg:32633"}),
170+
(xr.Dataset(coords={"epsg": xr.DataArray(32633, name="epsg")}), 32633),
171+
(
172+
xr.Dataset(coords={"proj:epsg": xr.DataArray(32633, name="proj:epsg")}),
173+
32633,
174+
),
175+
(xr.Dataset(attrs={"crs": "epsg:32633"}), 32633),
173176
],
174177
)
175-
def test_maybe_infer_reference_system(ds):
178+
def test_maybe_infer_reference_system(ds, expected):
176179
result = maybe_infer_reference_system(ds, reference_system=None)
177-
expected = pyproj.crs.CRS.from_epsg(32633).to_json_dict()
180+
expected = pyproj.crs.CRS.from_epsg(expected).to_json_dict()
181+
assert result == expected
182+
183+
184+
def test_maybe_infer_reference_system_from_cf_coordinates(ds):
185+
for n, variable in ds.variables.items():
186+
if "grid_mapping_name" in variable.attrs:
187+
ds[n].attrs.pop("grid_mapping_name")
188+
result = maybe_infer_reference_system(ds, reference_system=None)
189+
expected = pyproj.crs.CRS.from_epsg(4326).to_json_dict()
178190
assert result == expected
179191

180192

xstac/_xstac.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def maybe_infer_reference_system(ds, reference_system) -> dict:
204204
* proj:epsg from the coords
205205
* crs from the attrs
206206
* variables with grid_mapping_name in their attrs.
207+
* CF convention detection through cf_xarray
207208
208209
"""
209210
if reference_system is None:
@@ -220,17 +221,21 @@ def maybe_infer_reference_system(ds, reference_system) -> dict:
220221
ds.attrs["crs"]
221222
).to_json_dict()
222223
else:
223-
# try to infer it
224+
# try to infer it from variables
224225
names = [
225226
k for k, v in ds.variables.items() if "grid_mapping_name" in v.attrs
226227
]
227-
if not names:
228-
raise ValueError("Couldn't find a reference system")
229-
elif len(names) > 1:
228+
if len(names) > 1:
230229
raise ValueError("Too many reference systems: %s", names)
231-
(name,) = names
232-
crs = CRS.from_cf(ds[name].attrs)
233-
reference_system = crs.to_json_dict()
230+
elif len(names) == 1:
231+
(name,) = names
232+
crs = CRS.from_cf(ds[name].attrs)
233+
reference_system = crs.to_json_dict()
234+
elif {"latitude", "longitude"} <= ds.cf.coordinates.keys():
235+
reference_system = pyproj.CRS.from_epsg(4326).to_json_dict()
236+
237+
if not reference_system:
238+
raise ValueError("Couldn't find a reference system")
234239

235240
elif reference_system is False:
236241
reference_system = None

0 commit comments

Comments
 (0)