Skip to content

Commit 52433cc

Browse files
committed
feat: added deps, tests, fix tox/gdal/pdal setup with gh actions
1 parent 69d1ff3 commit 52433cc

File tree

9 files changed

+90
-48
lines changed

9 files changed

+90
-48
lines changed

.github/workflows/tests.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,21 @@ jobs:
3232
environment-name: test-env
3333
create-args: >-
3434
python=${{ matrix.python-version }}
35+
gdal>=3.3
36+
pdal
3537
pip
38+
python-pdal
39+
tox
40+
tox-current-env
41+
tox-gh
3642
37-
- name: install dependencies
38-
run: pip install tox tox-gh-actions
43+
- name: install current package with dev/doc/test extras
44+
run: pip install ".[dev,doc,test]"
3945

4046
- name: test with tox
41-
run: tox
47+
run: tox --current-env
4248
env:
49+
TOX_GH_MAJOR_MINOR: ${{ matrix.python-version }}
4350
CONDA_EXE: mamba
4451

4552
- name: upload coverage reports to Codecov

docs/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ dependencies:
66
- gdal=3.10.2
77
- myst-parser=4.0.1
88
- sphinx=8.2.3
9+
- pdal=2.8.4
910
- pip=25.0.1
1011
- pydata-sphinx-theme=0.16.1
1112
- setuptools=75.8.2

pyproject.toml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ classifiers = [
1919
"Programming Language :: Python :: 3.12",
2020
"Programming Language :: Python :: 3.13"
2121
]
22+
dependencies = [
23+
"geopandas>=1.0.0",
24+
"numpy>=2.0.0",
25+
"osmnx>=2.0.0",
26+
"pooch",
27+
"pyregeon",
28+
"pystac-client",
29+
"rasterio",
30+
"rasterstats",
31+
"shapely>=2.0.0",
32+
"tqdm"
33+
]
2234

2335
[project.license]
2436
text = "GPL-3.0"
@@ -40,6 +52,9 @@ doc = [
4052
"sphinx",
4153
"sphinxemoji"
4254
]
55+
pdal = [
56+
"pdal"
57+
]
4358
test = [
4459
"coverage[toml]",
4560
"pytest",
@@ -122,9 +137,9 @@ requires = [
122137

123138
[tool.tox.env.lint]
124139
commands = [
125-
"python -m build",
126-
"sphinx-build docs docs/_build",
127-
"twine check dist/*"
140+
["python", "-m", "build"],
141+
["sphinx-build", "docs", "docs/_build"],
142+
["twine", "check", "dist/*"]
128143
]
129144
extras = [
130145
"dev",
@@ -141,9 +156,6 @@ whitelist_externals = [
141156
commands = [
142157
["pytest", "-s", "--cov=swisstopopy", "--cov-report=xml", "tests"]
143158
]
144-
conda_deps = [
145-
"gdal>=3.3"
146-
]
147159
extras = [
148160
"test"
149161
]

swisstopopy/buildings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def get_bldg_gdf(
112112
region_gser.to_crs(ox.settings.default_crs).iloc[0],
113113
tags=OSMNX_TAGS,
114114
)
115-
.to_crs(stac.SWISSALTI3D_CRS)
115+
.to_crs(stac.CH_CRS)
116116
.drop("node")
117117
)
118118

@@ -157,7 +157,7 @@ def get_bldg_gdf(
157157
# and swissALTI3D products (again, EPSG:2056)
158158
tile_gdf = surface3d_gdf.sjoin(
159159
alti3d_gdf, how="inner", predicate="contains"
160-
).to_crs(stac.SWISSALTI3D_CRS)
160+
).to_crs(stac.CH_CRS)
161161

162162
# we could do a data frame apply approach returning a series of of building heights
163163
# that correspond to a single zonal statistic (e.g., "mean"). However, we use

swisstopopy/dem.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@
44
"""
55

66
import pooch
7-
from osgeo import gdal
87
from pyregeon import CRSType, RegionType
98
from pystac_client.item_search import DatetimeLike
9+
from rasterio import merge
1010
from tqdm import tqdm
1111

12-
from swisstopopy import stac, utils
12+
from swisstopopy import settings, stac, utils
1313

1414
__all__ = ["get_dem_raster"]
1515

16-
DST_OPTIONS = ["TILED:YES"]
17-
1816

1917
def get_dem_raster(
2018
region: RegionType,
@@ -24,7 +22,7 @@ def get_dem_raster(
2422
alti3d_datetime: DatetimeLike | None = None,
2523
alti3d_res: float = 2,
2624
pooch_retrieve_kwargs: utils.KwargsType = None,
27-
gdal_warp_kwargs: utils.KwargsType = None,
25+
rio_merge_kwargs: utils.KwargsType = None,
2826
) -> None:
2927
"""Get digital elevation model (DEM) raster.
3028
@@ -43,9 +41,10 @@ def get_dem_raster(
4341
If None, the latest data for each tile is used.
4442
alti3d_res : {0.5, 2}, default 2
4543
Resolution of the swissALTI3D data to get, can be 0.5 or 2 (meters).
46-
pooch_retrieve_kwargs, gdal_warp_kwargs : mapping, optional
44+
pooch_retrieve_kwargs, rio_merge_kwargs : mapping, optional
4745
Additional keyword arguments to respectively pass to `pooch.retrieve` and
48-
`gdal.Warp`.
46+
`rasterio.merge.merge`. If the latter is None, the default values from
47+
`settings.RIO_MERGE_DST_KWARGS` are used.
4948
"""
5049
# use the STAC API to get the DEM from swissALTI3D
5150
client = stac.SwissTopoClient(region=region, region_crs=region_crs)
@@ -65,16 +64,21 @@ def get_dem_raster(
6564
if pooch_retrieve_kwargs is None:
6665
pooch_retrieve_kwargs = {}
6766

68-
if gdal_warp_kwargs is None:
69-
_gdal_warp_kwargs = {}
67+
if rio_merge_kwargs is None:
68+
_rio_merge_kwargs = {}
7069
else:
71-
_gdal_warp_kwargs = gdal_warp_kwargs.copy()
72-
_gdal_warp_kwargs.update(creationOptions=DST_OPTIONS)
70+
_rio_merge_kwargs = rio_merge_kwargs.copy()
71+
_rio_merge_kwargs.update(dst_kwds=settings.RIO_MERGE_DST_KWARGS)
7372

7473
img_filepaths = []
7574
for url in tqdm(
7675
alti3d_gdf["assets.href"],
7776
):
7877
img_filepath = pooch.retrieve(url, known_hash=None, **pooch_retrieve_kwargs)
7978
img_filepaths.append(img_filepath)
80-
_ = gdal.Warp(dst_filepath, img_filepaths, format="GTiff", **_gdal_warp_kwargs)
79+
# merge the images into the final raster
80+
merge.merge(
81+
img_filepaths,
82+
dst_path=dst_filepath,
83+
**_rio_merge_kwargs,
84+
)

swisstopopy/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Settings."""
2+
3+
RIO_MERGE_DST_KWARGS = {"tiled": True, "blockxsize": 512, "blockysize": 512}

swisstopopy/stac.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ def __init__(
154154
# rather than inheriting from `RegionMixin`, we just use the
155155
# `_process_region_arg` static method
156156
self.region = (
157-
RegionMixin._process_region_arg(region, region_crs=region_crs)
158-
.to_crs(CLIENT_CRS)
157+
RegionMixin._process_region_arg(region, crs=region_crs)
158+
.to_crs(CLIENT_CRS)["geometry"]
159159
.iloc[0]
160160
)
161161
else:

swisstopopy/tree_canopy.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,15 @@
1010
import pdal
1111
import pooch
1212
import rasterio as rio
13-
from osgeo import gdal
1413
from pyregeon import CRSType, RegionType
1514
from pystac_client.item_search import DatetimeLike
15+
from rasterio import merge
1616
from tqdm import tqdm
1717

18-
from swisstopopy import stac, utils
18+
from swisstopopy import settings, stac, utils
1919

2020
__all__ = ["get_tree_canopy_raster"]
2121

22-
DST_OPTIONS = ["TILED:YES"]
23-
2422

2523
def rasterize_lidar(
2624
lidar_filepath: utils.PathType,
@@ -61,10 +59,10 @@ def get_tree_canopy_raster(
6159
dst_tree_val: int = 1,
6260
dst_nodata: int = 0,
6361
dst_dtype: npt.DTypeLike = "uint32",
64-
lidar_tree_values: Sequence[int] | None = None,
62+
lidar_tree_values: int | Sequence[int] | None = 3,
6563
rasterize_lidar_kwargs: utils.KwargsType = None,
6664
pooch_retrieve_kwargs: utils.KwargsType = None,
67-
gdal_warp_kwargs: utils.KwargsType = None,
65+
rio_merge_kwargs: utils.KwargsType = None,
6866
) -> None:
6967
"""Get tree canopy raster.
7068
@@ -96,9 +94,11 @@ def get_tree_canopy_raster(
9694
lidar_tree_values : int or sequence of int, default 3.
9795
LiDAR classification values to use for tree canopy. If None, defaults to
9896
the "Vegetation" class value of swissSURFACE3D, i.e., 3.
99-
rasterize_lidar_kwargs, pooch_retrieve_kwargs, gdal_warp_kwargs : mapping, optional
97+
rasterize_lidar_kwargs, pooch_retrieve_kwargs, rio_merge_kwargs : mapping, optional
10098
Additional keyword arguments to respectively pass to
101-
`swisstopopy.tree_canopy.rasterize_lidar`, `pooch.retrieve` and `gdal.Warp`.
99+
`swisstopopy.tree_canopy.rasterize_lidar`, `pooch.retrieve` and
100+
`rasterio.merge.merge`. If the latter is None, the default values from
101+
`settings.RIO_MERGE_DST_KWARGS` are used.
102102
"""
103103
# use the STAC API to get the tree canopy from swissSURFACE3D
104104
# TODO: dry with `dem.get_dem_raster`?
@@ -126,18 +126,19 @@ def get_tree_canopy_raster(
126126
output_type="count",
127127
data_type="uint32",
128128
nodata=dst_nodata,
129-
default_srs=stac.SWISSALTI3D_CRS,
129+
default_srs=stac.CH_CRS,
130130
)
131131
if pooch_retrieve_kwargs is None:
132132
pooch_retrieve_kwargs = {}
133133

134134
if isinstance(lidar_tree_values, int):
135135
lidar_tree_values = [lidar_tree_values]
136-
if gdal_warp_kwargs is None:
137-
_gdal_warp_kwargs = {}
136+
137+
if rio_merge_kwargs is None:
138+
_rio_merge_kwargs = {}
138139
else:
139-
_gdal_warp_kwargs = gdal_warp_kwargs.copy()
140-
_gdal_warp_kwargs.update(creationOptions=DST_OPTIONS)
140+
_rio_merge_kwargs = rio_merge_kwargs.copy()
141+
_rio_merge_kwargs.update(dst_kwds=settings.RIO_MERGE_DST_KWARGS)
141142

142143
img_filepaths = []
143144
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -165,6 +166,13 @@ def get_tree_canopy_raster(
165166
tmp_dir,
166167
f"{path.splitext(path.basename(img_filepath))[0]}-counts.tif",
167168
)
169+
_ = rasterize_lidar(
170+
las_filepath,
171+
counts_filepath,
172+
lidar_tree_values,
173+
**_rasterize_lidar_kwargs,
174+
)
175+
168176
try:
169177
_ = rasterize_lidar(
170178
las_filepath,
@@ -193,5 +201,9 @@ def get_tree_canopy_raster(
193201
# add path to list
194202
img_filepaths.append(img_filepath)
195203

196-
# creationOptions=dst_options
197-
_ = gdal.Warp(dst_filepath, img_filepaths, format="GTiff", **_gdal_warp_kwargs)
204+
# merge tiles into the final raster
205+
merge.merge(
206+
img_filepaths,
207+
dst_path=dst_filepath,
208+
**_rio_merge_kwargs,
209+
)

tests/test_swisstopopy.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ def test_region(self):
2121
# test without region (all collection items)
2222
client = swisstopopy.SwissTopoClient()
2323

24-
# test init all collections
25-
for collection_id in self.collection_ids:
26-
gdf = client.gdf_from_collection(collection_id)
27-
# test that we get a non empty geo-data frame
28-
self.assertIsInstance(gdf, gpd.GeoDataFrame)
29-
self.assertFalse(gdf.empty)
24+
# since this is slow, test init one collection only
25+
collection_id = self.collection_ids[0]
26+
gdf = client.gdf_from_collection(collection_id)
3027

31-
# now only test one collection (the last one)
32-
region_client = swisstopopy.SwissTopoClient(self.region)
28+
# test with region
29+
region_client = swisstopopy.SwissTopoClient(region=self.nominatim_query)
3330
# test that there are at most as many items as when not filtering spatially
3431
self.assertLessEqual(
3532
len(region_client.gdf_from_collection(collection_id).index), len(gdf.index)
3633
)
34+
# test init all collections
35+
for collection_id in self.collection_ids:
36+
gdf = region_client.gdf_from_collection(collection_id)
37+
# test that we get a non empty geo-data frame
38+
self.assertIsInstance(gdf, gpd.GeoDataFrame)
39+
self.assertFalse(gdf.empty)

0 commit comments

Comments
 (0)