Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed

### Fixed

- `DataCube.resample_spatial()` now supports parameterized `resolution` and `projection` arguments. ([#897](https://github.com/Open-EO/openeo-python-client/issues/897))

## [0.49.0] - 2026-04-01

Expand Down
18 changes: 12 additions & 6 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pystac.extensions.eo
import pystac.extensions.item_assets

from openeo.api.process import Parameter
from openeo.internal.jupyter import render_component
from openeo.util import Rfc3339, deep_get
from openeo.utils.normalize import normalize_resample_resolution, unique
Expand Down Expand Up @@ -494,10 +495,14 @@ def drop_dimension(self, name: str = None) -> CubeMetadata:

def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
projection: Union[int, str, None] = None,
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
projection: Union[int, str, Parameter, None] = None,
) -> CubeMetadata:
resolution = normalize_resample_resolution(resolution)
if isinstance(resolution, Parameter):
normalized_resolution = None, None
else:
normalized_resolution = normalize_resample_resolution(resolution)

if self._dimensions is None:
# Best-effort fallback to work with
dimensions = [
Expand All @@ -512,13 +517,14 @@ def resample_spatial(
spatial_indices = [i for i, d in enumerate(dimensions) if isinstance(d, SpatialDimension)]
if len(spatial_indices) != 2:
raise MetadataException(f"Expected two spatial dimensions but found {spatial_indices=}")
assert len(resolution) == 2
for i, r in zip(spatial_indices, resolution):

assert len(normalized_resolution) == 2
for i, r in zip(spatial_indices, normalized_resolution):
dim: SpatialDimension = dimensions[i]
dimensions[i] = SpatialDimension(
name=dim.name,
extent=dim.extent,
crs=projection or dim.crs,
crs=None if isinstance(projection, Parameter) else projection or dim.crs,
step=r if r != 0.0 else dim.step,
)

Expand Down
4 changes: 2 additions & 2 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,8 +809,8 @@ def band(self, band: Union[str, int]) -> DataCube:
@openeo_process
def resample_spatial(
self,
resolution: Union[float, Tuple[float, float], List[float]] = 0.0,
projection: Union[int, str, None] = None,
resolution: Union[float, Tuple[float, float], List[float], Parameter] = 0.0,
projection: Union[int, str, Parameter, None] = None,
method: str = "near",
align: str = "upper-left",
) -> DataCube:
Expand Down
85 changes: 85 additions & 0 deletions tests/rest/datacube/test_datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,91 @@ def test_resample_spatial_no_metadata(s2cube_without_metadata):
]


def test_resample_spatial_parameter_resolution(s2cube):
"""A Parameter object passed as resolution must not crash and must appear in the process graph."""
param = Parameter.number("res", description="The spatial resolution.")
cube = s2cube.resample_spatial(resolution=param, projection=32631)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": {"from_parameter": "res"},
"projection": 32631,
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=32631, step=None),
SpatialDimension(name="y", extent=None, crs=32631, step=None),
]


def test_resample_spatial_parameter_resolution_no_projection(s2cube):
"""A Parameter resolution with no concrete projection leaves step and crs unchanged."""
param = Parameter.number("res", description="The spatial resolution.")
cube = s2cube.resample_spatial(resolution=param)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": {"from_parameter": "res"},
"projection": None,
"method": "near",
"align": "upper-left",
},
}
}


def test_resample_spatial_parameter_projection(s2cube):
"""A Parameter object passed as projection must appear in the process graph; metadata crs should be None."""
proj_param = Parameter.integer("proj", description="The target projection.")
cube = s2cube.resample_spatial(resolution=10, projection=proj_param)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": 10,
"projection": {"from_parameter": "proj"},
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=None, step=10),
SpatialDimension(name="y", extent=None, crs=None, step=10),
]


def test_resample_spatial_parameter_resolution_and_projection(s2cube):
"""When both resolution and projection are Parameters, step and crs should both be None."""
res_param = Parameter.number("res", description="The spatial resolution.")
proj_param = Parameter.integer("proj", description="The target projection.")
cube = s2cube.resample_spatial(resolution=res_param, projection=proj_param)
assert get_download_graph(cube, drop_load_collection=True, drop_save_result=True) == {
"resamplespatial1": {
"process_id": "resample_spatial",
"arguments": {
"data": {"from_node": "loadcollection1"},
"resolution": {"from_parameter": "res"},
"projection": {"from_parameter": "proj"},
"method": "near",
"align": "upper-left",
},
}
}
assert cube.metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=None, crs=None, step=None),
SpatialDimension(name="y", extent=None, crs=None, step=None),
]


def test_resample_cube_spatial(s2cube):
cube1 = s2cube.resample_spatial(resolution=[2.0, 3.0], projection=4578)
cube2 = s2cube.resample_spatial(resolution=10, projection=32631)
Expand Down
59 changes: 59 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pystac
import pytest

from openeo.api.process import Parameter
from openeo.metadata import (
_PYSTAC_1_9_EXTENSION_INTERFACE,
Band,
Expand Down Expand Up @@ -1279,6 +1280,64 @@ def test_metadata_resample_spatial(cube_metadata, kwargs, expected_x, expected_y
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_resolution_only(cube_metadata):
"""When resolution is a Parameter, step should remain unchanged and crs should stay as-is."""
param = Parameter.number("res", description="The spatial resolution.")
metadata = cube_metadata.resample_spatial(resolution=param)
assert isinstance(metadata, CubeMetadata)
# step must be set to None after parametrized resample_spatial.
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=4326, step=None),
SpatialDimension(name="y", extent=[49, 52], crs=4326, step=None),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_resolution_with_projection(cube_metadata):
"""When resolution is a Parameter but projection is concrete, crs should be updated."""
param = Parameter.number("res", description="The spatial resolution.")
metadata = cube_metadata.resample_spatial(resolution=param, projection=32631)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=32631, step=None),
SpatialDimension(name="y", extent=[49, 52], crs=32631, step=None),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_projection_only(cube_metadata):
"""When projection is a Parameter, crs should be set to None."""
param = Parameter.integer("proj", description="The target projection.")
metadata = cube_metadata.resample_spatial(resolution=10, projection=param)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=None, step=10),
SpatialDimension(name="y", extent=[49, 52], crs=None, step=10),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_spatial_parameter_resolution_and_projection(cube_metadata):
"""When both resolution and projection are Parameters, step and crs should both be None."""
res_param = Parameter.number("res", description="The spatial resolution.")
proj_param = Parameter.integer("proj", description="The target projection.")
metadata = cube_metadata.resample_spatial(resolution=res_param, projection=proj_param)
assert isinstance(metadata, CubeMetadata)
assert metadata.spatial_dimensions == [
SpatialDimension(name="x", extent=[2, 7], crs=None, step=None),
SpatialDimension(name="y", extent=[49, 52], crs=None, step=None),
]
assert metadata.temporal_dimension == cube_metadata.temporal_dimension
assert metadata.band_dimension == cube_metadata.band_dimension


@pytest.mark.parametrize("cube_metadata", [CUBE_METADATA_XYTB, CUBE_METADATA_TBXY])
def test_metadata_resample_cube_spatial(cube_metadata):
metadata1 = cube_metadata.resample_spatial(resolution=(11, 22), projection=32631)
Expand Down
Loading