Skip to content

Leverage pystac for deeper spatial dimension detection #739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
31 changes: 25 additions & 6 deletions openeo/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
]
87 changes: 87 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if no dimension metadata is present, I would assume a default cube with also temporal and bands dimension

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes indeed, that is the topic of #743

this PR is just about improving the spatial extent detection if there is STAC metadata about that

),
(
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"],
[
Expand Down