diff --git a/openeo/metadata.py b/openeo/metadata.py index 733f4f8fd..ecd582fc8 100644 --- a/openeo/metadata.py +++ b/openeo/metadata.py @@ -655,12 +655,17 @@ def is_band_asset(asset: pystac.Asset) -> bool: else: raise ValueError(stac_object) - # At least assume there are spatial dimensions - # TODO: are there conditions in which we even should not assume the presence of spatial dimensions? - dimensions = [ - SpatialDimension(name="x", extent=[None, None]), - SpatialDimension(name="y", extent=[None, None]), - ] + dimensions = [] + + spatial_dimensions = _StacMetadataParser().get_spatial_dimensions(stac_object) + # Unless spatial dimensions are explicitly absent from the STAC metadata: + # assume we at least have spatial dimensions ("x" and "y", per openEO API recommendation). + if spatial_dimensions is None: + spatial_dimensions = [ + SpatialDimension(name="x", extent=[None, None]), + SpatialDimension(name="y", extent=[None, None]), + ] + dimensions.extend(spatial_dimensions) # TODO: conditionally include band dimension when there was actual indication of band metadata? band_dimension = BandDimension(name="bands", bands=bands) @@ -780,3 +785,17 @@ def get_temporal_dimension(self, stac_obj: pystac.STACObject) -> Union[TemporalD if len(temporal_dims) == 1: name, extent = temporal_dims[0] return TemporalDimension(name=name, extent=extent) + + def get_spatial_dimensions(self, stac_obj: pystac.STACObject) -> Union[List[SpatialDimension], None]: + if _PYSTAC_1_9_EXTENSION_INTERFACE: + if stac_obj.ext.has("cube") and hasattr(stac_obj.ext, "cube"): + return [ + SpatialDimension( + name=n, + extent=d.extent or [None, None], + crs=d.reference_system or SpatialDimension.DEFAULT_CRS, + step=d.step, + ) + for (n, d) in stac_obj.ext.cube.dimensions.items() + if d.dim_type == pystac.extensions.datacube.DimensionType.SPATIAL + ] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index d3d798488..28a333877 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -937,6 +937,93 @@ def test_metadata_from_stac_temporal_dimension(tmp_path, stac_dict, expected): assert not metadata.has_temporal_dimension() +@pytest.mark.skipif( + not _PYSTAC_1_9_EXTENSION_INTERFACE, + reason="No backport of implementation/test below PySTAC 1.9 extension interface", +) +@pytest.mark.parametrize( + ["stac_dict", "expected"], + [ + ( + # Item without cube:dimensions metadata -> assume spatial dimensions x and y + StacDummyBuilder.item(), + {"x": [None, None], "y": [None, None]}, + ), + ( + StacDummyBuilder.item( + cube_dimensions={ + "t": {"type": "temporal", "extent": ["2024-04-04", "2024-06-06"]}, + "x": {"type": "spatial", "axis": "x", "extent": [-10, 20]}, + "y": {"type": "spatial", "axis": "y", "extent": [30, 50]}, + } + ), + {"x": [-10, 20], "y": [30, 50]}, + ), + ( + # Custom dimension names + StacDummyBuilder.item( + cube_dimensions={ + "TTT": {"type": "temporal", "extent": ["2024-04-04", "2024-06-06"]}, + "XXX": {"type": "spatial", "axis": "x", "extent": [-10, 20]}, + "YYY": {"type": "spatial", "axis": "y", "extent": [30, 50]}, + } + ), + {"XXX": [-10, 20], "YYY": [30, 50]}, + ), + ( + # Explicitly absent spatial dimensions + StacDummyBuilder.item( + cube_dimensions={ + "t": {"type": "temporal", "extent": ["2024-04-04", "2024-06-06"]}, + } + ), + {}, + ), + ( + # No cube:dimensions metadata -> assume spatial dimensions x and y + StacDummyBuilder.collection(), + {"x": [None, None], "y": [None, None]}, + ), + ( + StacDummyBuilder.collection( + cube_dimensions={ + "t": {"type": "temporal", "extent": ["2024-04-04", "2024-06-06"]}, + "x": {"type": "spatial", "axis": "x", "extent": [-10, 20]}, + "y": {"type": "spatial", "axis": "y", "extent": [30, 50]}, + } + ), + {"x": [-10, 20], "y": [30, 50]}, + ), + ( + # Explicitly absent spatial dimensions + StacDummyBuilder.collection( + cube_dimensions={ + "t": {"type": "temporal", "extent": ["2024-04-04", "2024-06-06"]}, + } + ), + {}, + ), + ( + StacDummyBuilder.catalog(), + {"x": [None, None], "y": [None, None]}, + ), + ( + # Note: a catalog is not supposed to have datacube extension enabled, but we should not choke on that + StacDummyBuilder.catalog(stac_extensions=[StacDummyBuilder._EXT_DATACUBE]), + {"x": [None, None], "y": [None, None]}, + ), + ], +) +def test_metadata_from_stac_spatial_dimensions(tmp_path, stac_dict, expected): + path = tmp_path / "stac.json" + # TODO #738 real request mocking of STAC resources compatible with pystac? + path.write_text(json.dumps(stac_dict)) + metadata = metadata_from_stac(str(path)) + dims = metadata.spatial_dimensions + assert all(isinstance(d, SpatialDimension) for d in dims) + assert {d.name: d.extent for d in dims} == expected + + @pytest.mark.parametrize( ["kwargs", "expected_x", "expected_y"], [