diff --git a/api-clients/python/.openapi-generator/FILES b/api-clients/python/.openapi-generator/FILES index 693175b64c..4db5810afe 100644 --- a/api-clients/python/.openapi-generator/FILES +++ b/api-clients/python/.openapi-generator/FILES @@ -250,7 +250,12 @@ docs/SpatialGridDescriptorState.md docs/SpatialPartition2D.md docs/SpatialReferenceSpecification.md docs/SpatialReferencesApi.md +docs/SpatialResolution.md docs/StacApiRetries.md +docs/StacDataProviderDefinition.md +docs/StacProviderDataset.md +docs/StacProviderDatasetBand.md +docs/StacProviderS3Config.md docs/StacQueryBuffer.md docs/StaticColor.md docs/StaticNumber.md @@ -588,8 +593,13 @@ geoengine_api_client/models/spatial_grid_descriptor.py geoengine_api_client/models/spatial_grid_descriptor_state.py geoengine_api_client/models/spatial_partition2_d.py geoengine_api_client/models/spatial_reference_specification.py +geoengine_api_client/models/spatial_resolution.py geoengine_api_client/models/st_rectangle.py geoengine_api_client/models/stac_api_retries.py +geoengine_api_client/models/stac_data_provider_definition.py +geoengine_api_client/models/stac_provider_dataset.py +geoengine_api_client/models/stac_provider_dataset_band.py +geoengine_api_client/models/stac_provider_s3_config.py geoengine_api_client/models/stac_query_buffer.py geoengine_api_client/models/static_color.py geoengine_api_client/models/static_number.py @@ -921,8 +931,13 @@ test/test_spatial_grid_descriptor_state.py test/test_spatial_partition2_d.py test/test_spatial_reference_specification.py test/test_spatial_references_api.py +test/test_spatial_resolution.py test/test_st_rectangle.py test/test_stac_api_retries.py +test/test_stac_data_provider_definition.py +test/test_stac_provider_dataset.py +test/test_stac_provider_dataset_band.py +test/test_stac_provider_s3_config.py test/test_stac_query_buffer.py test/test_static_color.py test/test_static_number.py diff --git a/api-clients/python/README.md b/api-clients/python/README.md index 467e98a495..0f2438205d 100644 --- a/api-clients/python/README.md +++ b/api-clients/python/README.md @@ -424,7 +424,12 @@ Class | Method | HTTP request | Description - [SpatialGridDescriptorState](docs/SpatialGridDescriptorState.md) - [SpatialPartition2D](docs/SpatialPartition2D.md) - [SpatialReferenceSpecification](docs/SpatialReferenceSpecification.md) + - [SpatialResolution](docs/SpatialResolution.md) - [StacApiRetries](docs/StacApiRetries.md) + - [StacDataProviderDefinition](docs/StacDataProviderDefinition.md) + - [StacProviderDataset](docs/StacProviderDataset.md) + - [StacProviderDatasetBand](docs/StacProviderDatasetBand.md) + - [StacProviderS3Config](docs/StacProviderS3Config.md) - [StacQueryBuffer](docs/StacQueryBuffer.md) - [StaticColor](docs/StaticColor.md) - [StaticNumber](docs/StaticNumber.md) diff --git a/api-clients/python/geoengine_api_client/__init__.py b/api-clients/python/geoengine_api_client/__init__.py index fc61bb528b..a31d7f8ba3 100644 --- a/api-clients/python/geoengine_api_client/__init__.py +++ b/api-clients/python/geoengine_api_client/__init__.py @@ -278,7 +278,12 @@ "SpatialGridDescriptorState", "SpatialPartition2D", "SpatialReferenceSpecification", + "SpatialResolution", "StacApiRetries", + "StacDataProviderDefinition", + "StacProviderDataset", + "StacProviderDatasetBand", + "StacProviderS3Config", "StacQueryBuffer", "StaticColor", "StaticNumber", @@ -622,7 +627,12 @@ from geoengine_api_client.models.spatial_grid_descriptor_state import SpatialGridDescriptorState as SpatialGridDescriptorState from geoengine_api_client.models.spatial_partition2_d import SpatialPartition2D as SpatialPartition2D from geoengine_api_client.models.spatial_reference_specification import SpatialReferenceSpecification as SpatialReferenceSpecification +from geoengine_api_client.models.spatial_resolution import SpatialResolution as SpatialResolution from geoengine_api_client.models.stac_api_retries import StacApiRetries as StacApiRetries +from geoengine_api_client.models.stac_data_provider_definition import StacDataProviderDefinition as StacDataProviderDefinition +from geoengine_api_client.models.stac_provider_dataset import StacProviderDataset as StacProviderDataset +from geoengine_api_client.models.stac_provider_dataset_band import StacProviderDatasetBand as StacProviderDatasetBand +from geoengine_api_client.models.stac_provider_s3_config import StacProviderS3Config as StacProviderS3Config from geoengine_api_client.models.stac_query_buffer import StacQueryBuffer as StacQueryBuffer from geoengine_api_client.models.static_color import StaticColor as StaticColor from geoengine_api_client.models.static_number import StaticNumber as StaticNumber diff --git a/api-clients/python/geoengine_api_client/models/__init__.py b/api-clients/python/geoengine_api_client/models/__init__.py index fb9ab50816..0b5396f5e1 100644 --- a/api-clients/python/geoengine_api_client/models/__init__.py +++ b/api-clients/python/geoengine_api_client/models/__init__.py @@ -248,7 +248,12 @@ from geoengine_api_client.models.spatial_grid_descriptor_state import SpatialGridDescriptorState from geoengine_api_client.models.spatial_partition2_d import SpatialPartition2D from geoengine_api_client.models.spatial_reference_specification import SpatialReferenceSpecification +from geoengine_api_client.models.spatial_resolution import SpatialResolution from geoengine_api_client.models.stac_api_retries import StacApiRetries +from geoengine_api_client.models.stac_data_provider_definition import StacDataProviderDefinition +from geoengine_api_client.models.stac_provider_dataset import StacProviderDataset +from geoengine_api_client.models.stac_provider_dataset_band import StacProviderDatasetBand +from geoengine_api_client.models.stac_provider_s3_config import StacProviderS3Config from geoengine_api_client.models.stac_query_buffer import StacQueryBuffer from geoengine_api_client.models.static_color import StaticColor from geoengine_api_client.models.static_number import StaticNumber diff --git a/api-clients/python/geoengine_api_client/models/spatial_resolution.py b/api-clients/python/geoengine_api_client/models/spatial_resolution.py new file mode 100644 index 0000000000..21c2493726 --- /dev/null +++ b/api-clients/python/geoengine_api_client/models/spatial_resolution.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +class SpatialResolution(BaseModel): + """ + The spatial resolution in SRS units + """ # noqa: E501 + x: Union[StrictFloat, StrictInt] + y: Union[StrictFloat, StrictInt] + __properties: ClassVar[List[str]] = ["x", "y"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SpatialResolution from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SpatialResolution from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "x": obj.get("x"), + "y": obj.get("y") + }) + return _obj + + diff --git a/api-clients/python/geoengine_api_client/models/stac_data_provider_definition.py b/api-clients/python/geoengine_api_client/models/stac_data_provider_definition.py new file mode 100644 index 0000000000..ad416473b8 --- /dev/null +++ b/api-clients/python/geoengine_api_client/models/stac_data_provider_definition.py @@ -0,0 +1,139 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from uuid import UUID +from geoengine_api_client.models.stac_provider_dataset import StacProviderDataset +from geoengine_api_client.models.stac_provider_s3_config import StacProviderS3Config +from geoengine_api_client.models.time_dimension import TimeDimension +from typing import Optional, Set +from typing_extensions import Self + +class StacDataProviderDefinition(BaseModel): + """ + StacDataProviderDefinition + """ # noqa: E501 + api_url: StrictStr = Field(alias="apiUrl") + collection_name: StrictStr = Field(alias="collectionName") + datasets: List[StacProviderDataset] + description: StrictStr + id: UUID + name: StrictStr + priority: Optional[StrictInt] = None + s3_config: Optional[StacProviderS3Config] = Field(default=None, alias="s3Config") + time_dimension: TimeDimension = Field(alias="timeDimension") + type: StrictStr + __properties: ClassVar[List[str]] = ["apiUrl", "collectionName", "datasets", "description", "id", "name", "priority", "s3Config", "timeDimension", "type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['StacProviderDefinition']): + raise ValueError("must be one of enum values ('StacProviderDefinition')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StacDataProviderDefinition from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in datasets (list) + _items = [] + if self.datasets: + for _item_datasets in self.datasets: + if _item_datasets: + _items.append(_item_datasets.to_dict()) + _dict['datasets'] = _items + # override the default output from pydantic by calling `to_dict()` of s3_config + if self.s3_config: + _dict['s3Config'] = self.s3_config.to_dict() + # override the default output from pydantic by calling `to_dict()` of time_dimension + if self.time_dimension: + _dict['timeDimension'] = self.time_dimension.to_dict() + # set to None if priority (nullable) is None + # and model_fields_set contains the field + if self.priority is None and "priority" in self.model_fields_set: + _dict['priority'] = None + + # set to None if s3_config (nullable) is None + # and model_fields_set contains the field + if self.s3_config is None and "s3_config" in self.model_fields_set: + _dict['s3Config'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StacDataProviderDefinition from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "apiUrl": obj.get("apiUrl"), + "collectionName": obj.get("collectionName"), + "datasets": [StacProviderDataset.from_dict(_item) for _item in obj["datasets"]] if obj.get("datasets") is not None else None, + "description": obj.get("description"), + "id": obj.get("id"), + "name": obj.get("name"), + "priority": obj.get("priority"), + "s3Config": StacProviderS3Config.from_dict(obj["s3Config"]) if obj.get("s3Config") is not None else None, + "timeDimension": TimeDimension.from_dict(obj["timeDimension"]) if obj.get("timeDimension") is not None else None, + "type": obj.get("type") + }) + return _obj + + diff --git a/api-clients/python/geoengine_api_client/models/stac_provider_dataset.py b/api-clients/python/geoengine_api_client/models/stac_provider_dataset.py new file mode 100644 index 0000000000..83d5c18b20 --- /dev/null +++ b/api-clients/python/geoengine_api_client/models/stac_provider_dataset.py @@ -0,0 +1,116 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from geoengine_api_client.models.raster_data_type import RasterDataType +from geoengine_api_client.models.spatial_grid_descriptor import SpatialGridDescriptor +from geoengine_api_client.models.spatial_resolution import SpatialResolution +from geoengine_api_client.models.stac_provider_dataset_band import StacProviderDatasetBand +from typing import Optional, Set +from typing_extensions import Self + +class StacProviderDataset(BaseModel): + """ + StacProviderDataset + """ # noqa: E501 + bands: List[StacProviderDatasetBand] + data_type: RasterDataType = Field(alias="dataType") + description: StrictStr + name: StrictStr + projection: StrictStr + resolution: SpatialResolution + spatial_grid: SpatialGridDescriptor = Field(alias="spatialGrid") + __properties: ClassVar[List[str]] = ["bands", "dataType", "description", "name", "projection", "resolution", "spatialGrid"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StacProviderDataset from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in bands (list) + _items = [] + if self.bands: + for _item_bands in self.bands: + if _item_bands: + _items.append(_item_bands.to_dict()) + _dict['bands'] = _items + # override the default output from pydantic by calling `to_dict()` of resolution + if self.resolution: + _dict['resolution'] = self.resolution.to_dict() + # override the default output from pydantic by calling `to_dict()` of spatial_grid + if self.spatial_grid: + _dict['spatialGrid'] = self.spatial_grid.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StacProviderDataset from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "bands": [StacProviderDatasetBand.from_dict(_item) for _item in obj["bands"]] if obj.get("bands") is not None else None, + "dataType": obj.get("dataType"), + "description": obj.get("description"), + "name": obj.get("name"), + "projection": obj.get("projection"), + "resolution": SpatialResolution.from_dict(obj["resolution"]) if obj.get("resolution") is not None else None, + "spatialGrid": SpatialGridDescriptor.from_dict(obj["spatialGrid"]) if obj.get("spatialGrid") is not None else None + }) + return _obj + + diff --git a/api-clients/python/geoengine_api_client/models/stac_provider_dataset_band.py b/api-clients/python/geoengine_api_client/models/stac_provider_dataset_band.py new file mode 100644 index 0000000000..32630fc9bb --- /dev/null +++ b/api-clients/python/geoengine_api_client/models/stac_provider_dataset_band.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class StacProviderDatasetBand(BaseModel): + """ + StacProviderDatasetBand + """ # noqa: E501 + asset_title: StrictStr = Field(alias="assetTitle") + band_name: Optional[StrictStr] = Field(default=None, alias="bandName") + __properties: ClassVar[List[str]] = ["assetTitle", "bandName"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StacProviderDatasetBand from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if band_name (nullable) is None + # and model_fields_set contains the field + if self.band_name is None and "band_name" in self.model_fields_set: + _dict['bandName'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StacProviderDatasetBand from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "assetTitle": obj.get("assetTitle"), + "bandName": obj.get("bandName") + }) + return _obj + + diff --git a/api-clients/python/geoengine_api_client/models/stac_provider_s3_config.py b/api-clients/python/geoengine_api_client/models/stac_provider_s3_config.py new file mode 100644 index 0000000000..659f34d3e3 --- /dev/null +++ b/api-clients/python/geoengine_api_client/models/stac_provider_s3_config.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class StacProviderS3Config(BaseModel): + """ + StacProviderS3Config + """ # noqa: E501 + access_key: Optional[StrictStr] = Field(default=None, alias="accessKey") + endpoint: StrictStr + secret_key: Optional[StrictStr] = Field(default=None, alias="secretKey") + __properties: ClassVar[List[str]] = ["accessKey", "endpoint", "secretKey"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StacProviderS3Config from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if access_key (nullable) is None + # and model_fields_set contains the field + if self.access_key is None and "access_key" in self.model_fields_set: + _dict['accessKey'] = None + + # set to None if secret_key (nullable) is None + # and model_fields_set contains the field + if self.secret_key is None and "secret_key" in self.model_fields_set: + _dict['secretKey'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StacProviderS3Config from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "accessKey": obj.get("accessKey"), + "endpoint": obj.get("endpoint"), + "secretKey": obj.get("secretKey") + }) + return _obj + + diff --git a/api-clients/python/geoengine_api_client/models/typed_data_provider_definition.py b/api-clients/python/geoengine_api_client/models/typed_data_provider_definition.py index 49360ed686..df3302580e 100644 --- a/api-clients/python/geoengine_api_client/models/typed_data_provider_definition.py +++ b/api-clients/python/geoengine_api_client/models/typed_data_provider_definition.py @@ -28,12 +28,13 @@ from geoengine_api_client.models.net_cdf_cf_data_provider_definition import NetCdfCfDataProviderDefinition from geoengine_api_client.models.pangaea_data_provider_definition import PangaeaDataProviderDefinition from geoengine_api_client.models.sentinel_s2_l2_a_cogs_provider_definition import SentinelS2L2ACogsProviderDefinition +from geoengine_api_client.models.stac_data_provider_definition import StacDataProviderDefinition from geoengine_api_client.models.wildlive_data_connector_definition import WildliveDataConnectorDefinition from pydantic import StrictStr, Field from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self -TYPEDDATAPROVIDERDEFINITION_ONE_OF_SCHEMAS = ["ArunaDataProviderDefinition", "CopernicusDataspaceDataProviderDefinition", "DatasetLayerListingProviderDefinition", "EbvPortalDataProviderDefinition", "EdrDataProviderDefinition", "GbifDataProviderDefinition", "GfbioAbcdDataProviderDefinition", "GfbioCollectionsDataProviderDefinition", "NetCdfCfDataProviderDefinition", "PangaeaDataProviderDefinition", "SentinelS2L2ACogsProviderDefinition", "WildliveDataConnectorDefinition"] +TYPEDDATAPROVIDERDEFINITION_ONE_OF_SCHEMAS = ["ArunaDataProviderDefinition", "CopernicusDataspaceDataProviderDefinition", "DatasetLayerListingProviderDefinition", "EbvPortalDataProviderDefinition", "EdrDataProviderDefinition", "GbifDataProviderDefinition", "GfbioAbcdDataProviderDefinition", "GfbioCollectionsDataProviderDefinition", "NetCdfCfDataProviderDefinition", "PangaeaDataProviderDefinition", "SentinelS2L2ACogsProviderDefinition", "StacDataProviderDefinition", "WildliveDataConnectorDefinition"] class TypedDataProviderDefinition(BaseModel): """ @@ -61,10 +62,12 @@ class TypedDataProviderDefinition(BaseModel): oneof_schema_10_validator: Optional[PangaeaDataProviderDefinition] = None # data type: SentinelS2L2ACogsProviderDefinition oneof_schema_11_validator: Optional[SentinelS2L2ACogsProviderDefinition] = None + # data type: StacDataProviderDefinition + oneof_schema_12_validator: Optional[StacDataProviderDefinition] = None # data type: WildliveDataConnectorDefinition - oneof_schema_12_validator: Optional[WildliveDataConnectorDefinition] = None - actual_instance: Optional[Union[ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition]] = None - one_of_schemas: Set[str] = { "ArunaDataProviderDefinition", "CopernicusDataspaceDataProviderDefinition", "DatasetLayerListingProviderDefinition", "EbvPortalDataProviderDefinition", "EdrDataProviderDefinition", "GbifDataProviderDefinition", "GfbioAbcdDataProviderDefinition", "GfbioCollectionsDataProviderDefinition", "NetCdfCfDataProviderDefinition", "PangaeaDataProviderDefinition", "SentinelS2L2ACogsProviderDefinition", "WildliveDataConnectorDefinition" } + oneof_schema_13_validator: Optional[WildliveDataConnectorDefinition] = None + actual_instance: Optional[Union[ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition]] = None + one_of_schemas: Set[str] = { "ArunaDataProviderDefinition", "CopernicusDataspaceDataProviderDefinition", "DatasetLayerListingProviderDefinition", "EbvPortalDataProviderDefinition", "EdrDataProviderDefinition", "GbifDataProviderDefinition", "GfbioAbcdDataProviderDefinition", "GfbioCollectionsDataProviderDefinition", "NetCdfCfDataProviderDefinition", "PangaeaDataProviderDefinition", "SentinelS2L2ACogsProviderDefinition", "StacDataProviderDefinition", "WildliveDataConnectorDefinition" } model_config = ConfigDict( validate_assignment=True, @@ -145,6 +148,11 @@ def actual_instance_must_validate_oneof(cls, v): error_messages.append(f"Error! Input type `{type(v)}` is not `SentinelS2L2ACogsProviderDefinition`") else: match += 1 + # validate data type: StacDataProviderDefinition + if not isinstance(v, StacDataProviderDefinition): + error_messages.append(f"Error! Input type `{type(v)}` is not `StacDataProviderDefinition`") + else: + match += 1 # validate data type: WildliveDataConnectorDefinition if not isinstance(v, WildliveDataConnectorDefinition): error_messages.append(f"Error! Input type `{type(v)}` is not `WildliveDataConnectorDefinition`") @@ -152,10 +160,10 @@ def actual_instance_must_validate_oneof(cls, v): match += 1 if match > 1: # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when setting `actual_instance` in TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when setting `actual_instance` in TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when setting `actual_instance` in TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) else: return v @@ -230,6 +238,11 @@ def from_json(cls, json_str: str) -> Self: instance.actual_instance = SentinelS2L2ACogsProviderDefinition.from_json(json_str) return instance + # check if data type is `StacDataProviderDefinition` + if _data_type == "StacProviderDefinition": + instance.actual_instance = StacDataProviderDefinition.from_json(json_str) + return instance + # check if data type is `WildliveDataConnectorDefinition` if _data_type == "WildLIVE!": instance.actual_instance = WildliveDataConnectorDefinition.from_json(json_str) @@ -301,6 +314,12 @@ def from_json(cls, json_str: str) -> Self: match += 1 except (ValidationError, ValueError) as e: error_messages.append(str(e)) + # deserialize data into StacDataProviderDefinition + try: + instance.actual_instance = StacDataProviderDefinition.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) # deserialize data into WildliveDataConnectorDefinition try: instance.actual_instance = WildliveDataConnectorDefinition.from_json(json_str) @@ -310,10 +329,10 @@ def from_json(cls, json_str: str) -> Self: if match > 1: # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when deserializing the JSON string into TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when deserializing the JSON string into TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when deserializing the JSON string into TypedDataProviderDefinition with oneOf schemas: ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition. Details: " + ", ".join(error_messages)) else: return instance @@ -327,7 +346,7 @@ def to_json(self) -> str: else: return json.dumps(self.actual_instance) - def to_dict(self) -> Optional[Union[Dict[str, Any], ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, WildliveDataConnectorDefinition]]: + def to_dict(self) -> Optional[Union[Dict[str, Any], ArunaDataProviderDefinition, CopernicusDataspaceDataProviderDefinition, DatasetLayerListingProviderDefinition, EbvPortalDataProviderDefinition, EdrDataProviderDefinition, GbifDataProviderDefinition, GfbioAbcdDataProviderDefinition, GfbioCollectionsDataProviderDefinition, NetCdfCfDataProviderDefinition, PangaeaDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, StacDataProviderDefinition, WildliveDataConnectorDefinition]]: """Returns the dict representation of the actual instance""" if self.actual_instance is None: return None diff --git a/api-clients/python/test/test_spatial_resolution.py b/api-clients/python/test/test_spatial_resolution.py new file mode 100644 index 0000000000..1b7d05bf88 --- /dev/null +++ b/api-clients/python/test/test_spatial_resolution.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from geoengine_api_client.models.spatial_resolution import SpatialResolution + +class TestSpatialResolution(unittest.TestCase): + """SpatialResolution unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SpatialResolution: + """Test SpatialResolution + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SpatialResolution` + """ + model = SpatialResolution() + if include_optional: + return SpatialResolution( + x = 1.337, + y = 1.337 + ) + else: + return SpatialResolution( + x = 1.337, + y = 1.337, + ) + """ + + def testSpatialResolution(self): + """Test SpatialResolution""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/api-clients/python/test/test_stac_data_provider_definition.py b/api-clients/python/test/test_stac_data_provider_definition.py new file mode 100644 index 0000000000..5e4383296a --- /dev/null +++ b/api-clients/python/test/test_stac_data_provider_definition.py @@ -0,0 +1,131 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from geoengine_api_client.models.stac_data_provider_definition import StacDataProviderDefinition + +class TestStacDataProviderDefinition(unittest.TestCase): + """StacDataProviderDefinition unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StacDataProviderDefinition: + """Test StacDataProviderDefinition + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StacDataProviderDefinition` + """ + model = StacDataProviderDefinition() + if include_optional: + return StacDataProviderDefinition( + api_url = '', + collection_name = '', + datasets = [ + geoengine_api_client.models.stac_provider_dataset.StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ), ) + ], + description = '', + id = '', + name = '', + priority = 56, + s3_config = geoengine_api_client.models.stac_provider_s3_config.StacProviderS3Config( + access_key = '', + endpoint = '', + secret_key = '', ), + time_dimension = None, + type = 'StacProviderDefinition' + ) + else: + return StacDataProviderDefinition( + api_url = '', + collection_name = '', + datasets = [ + geoengine_api_client.models.stac_provider_dataset.StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ), ) + ], + description = '', + id = '', + name = '', + time_dimension = None, + type = 'StacProviderDefinition', + ) + """ + + def testStacDataProviderDefinition(self): + """Test StacDataProviderDefinition""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/api-clients/python/test/test_stac_provider_dataset.py b/api-clients/python/test/test_stac_provider_dataset.py new file mode 100644 index 0000000000..f913ee4bf4 --- /dev/null +++ b/api-clients/python/test/test_stac_provider_dataset.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from geoengine_api_client.models.stac_provider_dataset import StacProviderDataset + +class TestStacProviderDataset(unittest.TestCase): + """StacProviderDataset unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StacProviderDataset: + """Test StacProviderDataset + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StacProviderDataset` + """ + model = StacProviderDataset() + if include_optional: + return StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ) + ) + else: + return StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ), + ) + """ + + def testStacProviderDataset(self): + """Test StacProviderDataset""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/api-clients/python/test/test_stac_provider_dataset_band.py b/api-clients/python/test/test_stac_provider_dataset_band.py new file mode 100644 index 0000000000..1ba2089bfb --- /dev/null +++ b/api-clients/python/test/test_stac_provider_dataset_band.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from geoengine_api_client.models.stac_provider_dataset_band import StacProviderDatasetBand + +class TestStacProviderDatasetBand(unittest.TestCase): + """StacProviderDatasetBand unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StacProviderDatasetBand: + """Test StacProviderDatasetBand + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StacProviderDatasetBand` + """ + model = StacProviderDatasetBand() + if include_optional: + return StacProviderDatasetBand( + asset_title = '', + band_name = '' + ) + else: + return StacProviderDatasetBand( + asset_title = '', + ) + """ + + def testStacProviderDatasetBand(self): + """Test StacProviderDatasetBand""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/api-clients/python/test/test_stac_provider_s3_config.py b/api-clients/python/test/test_stac_provider_s3_config.py new file mode 100644 index 0000000000..9b39cf016a --- /dev/null +++ b/api-clients/python/test/test_stac_provider_s3_config.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Geo Engine API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + Contact: dev@geoengine.de + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from geoengine_api_client.models.stac_provider_s3_config import StacProviderS3Config + +class TestStacProviderS3Config(unittest.TestCase): + """StacProviderS3Config unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StacProviderS3Config: + """Test StacProviderS3Config + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StacProviderS3Config` + """ + model = StacProviderS3Config() + if include_optional: + return StacProviderS3Config( + access_key = '', + endpoint = '', + secret_key = '' + ) + else: + return StacProviderS3Config( + endpoint = '', + ) + """ + + def testStacProviderS3Config(self): + """Test StacProviderS3Config""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/api-clients/python/test/test_typed_data_provider_definition.py b/api-clients/python/test/test_typed_data_provider_definition.py index 77edcb7c8e..0673d0312c 100644 --- a/api-clients/python/test/test_typed_data_provider_definition.py +++ b/api-clients/python/test/test_typed_data_provider_definition.py @@ -107,6 +107,43 @@ def make_instance(self, include_optional) -> TypedDataProviderDefinition: exponential_backoff_factor = 1.337, initial_delay_ms = 0, number_of_retries = 0, ), + collection_name = '', + datasets = [ + geoengine_api_client.models.stac_provider_dataset.StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ), ) + ], + s3_config = geoengine_api_client.models.stac_provider_s3_config.StacProviderS3Config( + access_key = '', + endpoint = '', + secret_key = '', ), + time_dimension = None, expiry_date = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), refresh_token = '', user = '' @@ -163,6 +200,39 @@ def make_instance(self, include_optional) -> TypedDataProviderDefinition: collection_api_auth_token = '', collection_api_url = '', pangaea_url = '', + collection_name = '', + datasets = [ + geoengine_api_client.models.stac_provider_dataset.StacProviderDataset( + bands = [ + geoengine_api_client.models.stac_provider_dataset_band.StacProviderDatasetBand( + asset_title = '', + band_name = '', ) + ], + data_type = 'U8', + description = '', + name = '', + projection = '', + resolution = geoengine_api_client.models.spatial_resolution.SpatialResolution( + x = 1.337, + y = 1.337, ), + spatial_grid = geoengine_api_client.models.spatial_grid_descriptor.SpatialGridDescriptor( + descriptor = 'source', + spatial_grid = geoengine_api_client.models.spatial_grid_definition.SpatialGridDefinition( + geo_transform = geoengine_api_client.models.geo_transform.GeoTransform( + origin_coordinate = geoengine_api_client.models.coordinate2_d.Coordinate2D( + x = 1.337, + y = 1.337, ), + x_pixel_size = 1.337, + y_pixel_size = 1.337, ), + grid_bounds = geoengine_api_client.models.grid_bounding_box2_d.GridBoundingBox2D( + bottom_right_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), + top_left_idx = geoengine_api_client.models.grid_idx2_d.GridIdx2D( + x_idx = 56, + y_idx = 56, ), ), ), ), ) + ], + time_dimension = None, ) """ diff --git a/api-clients/rust/.openapi-generator/FILES b/api-clients/rust/.openapi-generator/FILES index 7f0fa7f91d..862065c339 100644 --- a/api-clients/rust/.openapi-generator/FILES +++ b/api-clients/rust/.openapi-generator/FILES @@ -248,8 +248,13 @@ docs/SpatialGridDescriptorState.md docs/SpatialPartition2D.md docs/SpatialReferenceSpecification.md docs/SpatialReferencesApi.md +docs/SpatialResolution.md docs/StRectangle.md docs/StacApiRetries.md +docs/StacDataProviderDefinition.md +docs/StacProviderDataset.md +docs/StacProviderDatasetBand.md +docs/StacProviderS3Config.md docs/StacQueryBuffer.md docs/StaticColor.md docs/StaticNumber.md @@ -585,8 +590,13 @@ src/models/spatial_grid_descriptor.rs src/models/spatial_grid_descriptor_state.rs src/models/spatial_partition2_d.rs src/models/spatial_reference_specification.rs +src/models/spatial_resolution.rs src/models/st_rectangle.rs src/models/stac_api_retries.rs +src/models/stac_data_provider_definition.rs +src/models/stac_provider_dataset.rs +src/models/stac_provider_dataset_band.rs +src/models/stac_provider_s3_config.rs src/models/stac_query_buffer.rs src/models/static_color.rs src/models/static_number.rs diff --git a/api-clients/rust/README.md b/api-clients/rust/README.md index da0195c541..d2bf1d7eca 100644 --- a/api-clients/rust/README.md +++ b/api-clients/rust/README.md @@ -356,8 +356,13 @@ Class | Method | HTTP request | Description - [SpatialGridDescriptorState](docs/SpatialGridDescriptorState.md) - [SpatialPartition2D](docs/SpatialPartition2D.md) - [SpatialReferenceSpecification](docs/SpatialReferenceSpecification.md) + - [SpatialResolution](docs/SpatialResolution.md) - [StRectangle](docs/StRectangle.md) - [StacApiRetries](docs/StacApiRetries.md) + - [StacDataProviderDefinition](docs/StacDataProviderDefinition.md) + - [StacProviderDataset](docs/StacProviderDataset.md) + - [StacProviderDatasetBand](docs/StacProviderDatasetBand.md) + - [StacProviderS3Config](docs/StacProviderS3Config.md) - [StacQueryBuffer](docs/StacQueryBuffer.md) - [StaticColor](docs/StaticColor.md) - [StaticNumber](docs/StaticNumber.md) diff --git a/api-clients/rust/docs/Regular1.md b/api-clients/rust/docs/Regular1.md new file mode 100644 index 0000000000..1fb4d79187 --- /dev/null +++ b/api-clients/rust/docs/Regular1.md @@ -0,0 +1,13 @@ +# Regular1 + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**origin** | **i64** | | +**step** | [**models::TimeStep**](TimeStep.md) | | +**r#type** | **Type** | (enum: regular) | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/SpatialResolution.md b/api-clients/rust/docs/SpatialResolution.md new file mode 100644 index 0000000000..a1d301b232 --- /dev/null +++ b/api-clients/rust/docs/SpatialResolution.md @@ -0,0 +1,12 @@ +# SpatialResolution + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**x** | **f64** | | +**y** | **f64** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacDataProviderDefinition.md b/api-clients/rust/docs/StacDataProviderDefinition.md new file mode 100644 index 0000000000..cbac40e657 --- /dev/null +++ b/api-clients/rust/docs/StacDataProviderDefinition.md @@ -0,0 +1,20 @@ +# StacDataProviderDefinition + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**api_url** | **String** | | +**collection_name** | **String** | | +**datasets** | [**Vec**](StacProviderDataset.md) | | +**description** | **String** | | +**id** | **uuid::Uuid** | | +**name** | **String** | | +**priority** | Option<**i32**> | | [optional] +**s3_config** | Option<[**models::StacProviderS3Config**](StacProviderS3Config.md)> | | [optional] +**time_dimension** | [**models::TimeDimension**](TimeDimension.md) | | +**r#type** | **Type** | (enum: StacProviderDefinition) | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacProviderDataset.md b/api-clients/rust/docs/StacProviderDataset.md new file mode 100644 index 0000000000..1446897025 --- /dev/null +++ b/api-clients/rust/docs/StacProviderDataset.md @@ -0,0 +1,17 @@ +# StacProviderDataset + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**bands** | [**Vec**](StacProviderDatasetBand.md) | | +**data_type** | [**models::RasterDataType**](RasterDataType.md) | | +**description** | **String** | | +**name** | **String** | | +**projection** | **String** | | +**resolution** | [**models::SpatialResolution**](SpatialResolution.md) | | +**spatial_grid** | [**models::SpatialGridDescriptor**](SpatialGridDescriptor.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacProviderDatasetBand.md b/api-clients/rust/docs/StacProviderDatasetBand.md new file mode 100644 index 0000000000..79f9fe69e5 --- /dev/null +++ b/api-clients/rust/docs/StacProviderDatasetBand.md @@ -0,0 +1,12 @@ +# StacProviderDatasetBand + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**asset_title** | **String** | | +**band_name** | Option<**String**> | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacProviderDatasetResolution.md b/api-clients/rust/docs/StacProviderDatasetResolution.md new file mode 100644 index 0000000000..6822800d34 --- /dev/null +++ b/api-clients/rust/docs/StacProviderDatasetResolution.md @@ -0,0 +1,12 @@ +# StacProviderDatasetResolution + +## Enum Variants + +| Name | Description | +|---- | -----| +| SpatialResolution | | +| f64 | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacProviderS3Config.md b/api-clients/rust/docs/StacProviderS3Config.md new file mode 100644 index 0000000000..ba64c5becc --- /dev/null +++ b/api-clients/rust/docs/StacProviderS3Config.md @@ -0,0 +1,13 @@ +# StacProviderS3Config + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**access_key** | Option<**String**> | | [optional] +**endpoint** | **String** | | +**secret_key** | Option<**String**> | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacTimeDimension.md b/api-clients/rust/docs/StacTimeDimension.md new file mode 100644 index 0000000000..3a49ce16d1 --- /dev/null +++ b/api-clients/rust/docs/StacTimeDimension.md @@ -0,0 +1,12 @@ +# StacTimeDimension + +## Enum Variants + +| Name | Description | +|---- | -----| +| Irregular | | +| Regular | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/docs/StacTimeStep.md b/api-clients/rust/docs/StacTimeStep.md new file mode 100644 index 0000000000..15f0e3f3f8 --- /dev/null +++ b/api-clients/rust/docs/StacTimeStep.md @@ -0,0 +1,12 @@ +# StacTimeStep + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**granularity** | [**models::TimeGranularity**](TimeGranularity.md) | | +**value** | **i32** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/api-clients/rust/rustfmt.toml b/api-clients/rust/rustfmt.toml new file mode 100644 index 0000000000..c7ad93bafe --- /dev/null +++ b/api-clients/rust/rustfmt.toml @@ -0,0 +1 @@ +disable_all_formatting = true diff --git a/api-clients/rust/src/models/mod.rs b/api-clients/rust/src/models/mod.rs index cf8635efd3..529739017a 100644 --- a/api-clients/rust/src/models/mod.rs +++ b/api-clients/rust/src/models/mod.rs @@ -466,10 +466,20 @@ pub mod spatial_partition2_d; pub use self::spatial_partition2_d::SpatialPartition2D; pub mod spatial_reference_specification; pub use self::spatial_reference_specification::SpatialReferenceSpecification; +pub mod spatial_resolution; +pub use self::spatial_resolution::SpatialResolution; pub mod st_rectangle; pub use self::st_rectangle::StRectangle; pub mod stac_api_retries; pub use self::stac_api_retries::StacApiRetries; +pub mod stac_data_provider_definition; +pub use self::stac_data_provider_definition::StacDataProviderDefinition; +pub mod stac_provider_dataset; +pub use self::stac_provider_dataset::StacProviderDataset; +pub mod stac_provider_dataset_band; +pub use self::stac_provider_dataset_band::StacProviderDatasetBand; +pub mod stac_provider_s3_config; +pub use self::stac_provider_s3_config::StacProviderS3Config; pub mod stac_query_buffer; pub use self::stac_query_buffer::StacQueryBuffer; pub mod static_color; diff --git a/api-clients/rust/src/models/regular_1.rs b/api-clients/rust/src/models/regular_1.rs new file mode 100644 index 0000000000..5d20b509a4 --- /dev/null +++ b/api-clients/rust/src/models/regular_1.rs @@ -0,0 +1,43 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Regular1 { + #[serde(rename = "origin")] + pub origin: i64, + #[serde(rename = "step")] + pub step: Box, + #[serde(rename = "type")] + pub r#type: Type, +} + +impl Regular1 { + pub fn new(origin: i64, step: models::TimeStep, r#type: Type) -> Regular1 { + Regular1 { + origin, + step: Box::new(step), + r#type, + } + } +} +/// +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Type { + #[serde(rename = "regular")] + Regular, +} + +impl Default for Type { + fn default() -> Type { + Self::Regular + } +} diff --git a/api-clients/rust/src/models/spatial_resolution.rs b/api-clients/rust/src/models/spatial_resolution.rs new file mode 100644 index 0000000000..fa71990dd6 --- /dev/null +++ b/api-clients/rust/src/models/spatial_resolution.rs @@ -0,0 +1,31 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// SpatialResolution : The spatial resolution in SRS units +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SpatialResolution { + #[serde(rename = "x")] + pub x: f64, + #[serde(rename = "y")] + pub y: f64, +} + +impl SpatialResolution { + /// The spatial resolution in SRS units + pub fn new(x: f64, y: f64) -> SpatialResolution { + SpatialResolution { + x, + y, + } + } +} + diff --git a/api-clients/rust/src/models/stac_data_provider_definition.rs b/api-clients/rust/src/models/stac_data_provider_definition.rs new file mode 100644 index 0000000000..c1081b2984 --- /dev/null +++ b/api-clients/rust/src/models/stac_data_provider_definition.rs @@ -0,0 +1,65 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StacDataProviderDefinition { + #[serde(rename = "apiUrl")] + pub api_url: String, + #[serde(rename = "collectionName")] + pub collection_name: String, + #[serde(rename = "datasets")] + pub datasets: Vec, + #[serde(rename = "description")] + pub description: String, + #[serde(rename = "id")] + pub id: uuid::Uuid, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "priority", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub priority: Option>, + #[serde(rename = "s3Config", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub s3_config: Option>>, + #[serde(rename = "timeDimension")] + pub time_dimension: Box, + #[serde(rename = "type")] + pub r#type: Type, +} + +impl StacDataProviderDefinition { + pub fn new(api_url: String, collection_name: String, datasets: Vec, description: String, id: uuid::Uuid, name: String, time_dimension: models::TimeDimension, r#type: Type) -> StacDataProviderDefinition { + StacDataProviderDefinition { + api_url, + collection_name, + datasets, + description, + id, + name, + priority: None, + s3_config: None, + time_dimension: Box::new(time_dimension), + r#type, + } + } +} +/// +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Type { + #[serde(rename = "StacProviderDefinition")] + StacProviderDefinition, +} + +impl Default for Type { + fn default() -> Type { + Self::StacProviderDefinition + } +} + diff --git a/api-clients/rust/src/models/stac_provider_dataset.rs b/api-clients/rust/src/models/stac_provider_dataset.rs new file mode 100644 index 0000000000..0f6dbc0288 --- /dev/null +++ b/api-clients/rust/src/models/stac_provider_dataset.rs @@ -0,0 +1,44 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StacProviderDataset { + #[serde(rename = "bands")] + pub bands: Vec, + #[serde(rename = "dataType")] + pub data_type: models::RasterDataType, + #[serde(rename = "description")] + pub description: String, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "projection")] + pub projection: String, + #[serde(rename = "resolution")] + pub resolution: Box, + #[serde(rename = "spatialGrid")] + pub spatial_grid: Box, +} + +impl StacProviderDataset { + pub fn new(bands: Vec, data_type: models::RasterDataType, description: String, name: String, projection: String, resolution: models::SpatialResolution, spatial_grid: models::SpatialGridDescriptor) -> StacProviderDataset { + StacProviderDataset { + bands, + data_type, + description, + name, + projection, + resolution: Box::new(resolution), + spatial_grid: Box::new(spatial_grid), + } + } +} + diff --git a/api-clients/rust/src/models/stac_provider_dataset_band.rs b/api-clients/rust/src/models/stac_provider_dataset_band.rs new file mode 100644 index 0000000000..308b85c227 --- /dev/null +++ b/api-clients/rust/src/models/stac_provider_dataset_band.rs @@ -0,0 +1,29 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StacProviderDatasetBand { + #[serde(rename = "assetTitle")] + pub asset_title: String, + #[serde(rename = "bandName", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub band_name: Option>, +} + +impl StacProviderDatasetBand { + pub fn new(asset_title: String) -> StacProviderDatasetBand { + StacProviderDatasetBand { + asset_title, + band_name: None, + } + } +} + diff --git a/api-clients/rust/src/models/stac_provider_dataset_resolution.rs b/api-clients/rust/src/models/stac_provider_dataset_resolution.rs new file mode 100644 index 0000000000..885bde7c01 --- /dev/null +++ b/api-clients/rust/src/models/stac_provider_dataset_resolution.rs @@ -0,0 +1,25 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StacProviderDatasetResolution { + Number(f64), + SpatialResolution(Box), +} + +impl Default for StacProviderDatasetResolution { + fn default() -> Self { + Self::Number(Default::default()) + } +} + diff --git a/api-clients/rust/src/models/stac_provider_s3_config.rs b/api-clients/rust/src/models/stac_provider_s3_config.rs new file mode 100644 index 0000000000..67f30ff8f1 --- /dev/null +++ b/api-clients/rust/src/models/stac_provider_s3_config.rs @@ -0,0 +1,32 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StacProviderS3Config { + #[serde(rename = "accessKey", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub access_key: Option>, + #[serde(rename = "endpoint")] + pub endpoint: String, + #[serde(rename = "secretKey", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub secret_key: Option>, +} + +impl StacProviderS3Config { + pub fn new(endpoint: String) -> StacProviderS3Config { + StacProviderS3Config { + access_key: None, + endpoint, + secret_key: None, + } + } +} + diff --git a/api-clients/rust/src/models/stac_time_dimension.rs b/api-clients/rust/src/models/stac_time_dimension.rs new file mode 100644 index 0000000000..f08e3313eb --- /dev/null +++ b/api-clients/rust/src/models/stac_time_dimension.rs @@ -0,0 +1,38 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StacTimeDimension { + Regular(Box), + Irregular(Box), +} + +impl Default for StacTimeDimension { + fn default() -> Self { + Self::Regular(Default::default()) + } +} +/// +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Type { + #[serde(rename = "regular")] + Regular, + #[serde(rename = "irregular")] + Irregular, +} + +impl Default for Type { + fn default() -> Type { + Self::Regular + } +} diff --git a/api-clients/rust/src/models/stac_time_step.rs b/api-clients/rust/src/models/stac_time_step.rs new file mode 100644 index 0000000000..118037a178 --- /dev/null +++ b/api-clients/rust/src/models/stac_time_step.rs @@ -0,0 +1,25 @@ +/* + * Geo Engine API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StacTimeStep { + #[serde(rename = "granularity")] + pub granularity: models::TimeGranularity, + #[serde(rename = "value")] + pub value: i32, +} + +impl StacTimeStep { + pub fn new(granularity: models::TimeGranularity, value: i32) -> StacTimeStep { + StacTimeStep { granularity, value } + } +} diff --git a/api-clients/rust/src/models/typed_data_provider_definition.rs b/api-clients/rust/src/models/typed_data_provider_definition.rs index 1fe4f4bcb5..6a713692c2 100644 --- a/api-clients/rust/src/models/typed_data_provider_definition.rs +++ b/api-clients/rust/src/models/typed_data_provider_definition.rs @@ -35,6 +35,8 @@ pub enum TypedDataProviderDefinition { Pangaea(Box), #[serde(rename="SentinelS2L2ACogs")] SentinelS2L2ACogs(Box), + #[serde(rename="StacProviderDefinition")] + StacProviderDefinition(Box), #[serde(rename="WildLIVE!")] WildLiveExclamation(Box), } diff --git a/api-clients/typescript/README.md b/api-clients/typescript/README.md index 2d4936559e..b2ad7a88d7 100644 --- a/api-clients/typescript/README.md +++ b/api-clients/typescript/README.md @@ -387,7 +387,12 @@ All URIs are relative to *https://geoengine.io/api* - [SpatialGridDescriptorState](docs/SpatialGridDescriptorState.md) - [SpatialPartition2D](docs/SpatialPartition2D.md) - [SpatialReferenceSpecification](docs/SpatialReferenceSpecification.md) +- [SpatialResolution](docs/SpatialResolution.md) - [StacApiRetries](docs/StacApiRetries.md) +- [StacDataProviderDefinition](docs/StacDataProviderDefinition.md) +- [StacProviderDataset](docs/StacProviderDataset.md) +- [StacProviderDatasetBand](docs/StacProviderDatasetBand.md) +- [StacProviderS3Config](docs/StacProviderS3Config.md) - [StacQueryBuffer](docs/StacQueryBuffer.md) - [StaticColor](docs/StaticColor.md) - [StaticNumber](docs/StaticNumber.md) diff --git a/api-clients/typescript/dist/esm/models/SpatialResolution.d.ts b/api-clients/typescript/dist/esm/models/SpatialResolution.d.ts new file mode 100644 index 0000000000..3d7616a7ea --- /dev/null +++ b/api-clients/typescript/dist/esm/models/SpatialResolution.d.ts @@ -0,0 +1,37 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * The spatial resolution in SRS units + * @export + * @interface SpatialResolution + */ +export interface SpatialResolution { + /** + * + * @type {number} + * @memberof SpatialResolution + */ + x: number; + /** + * + * @type {number} + * @memberof SpatialResolution + */ + y: number; +} +/** + * Check if a given object implements the SpatialResolution interface. + */ +export declare function instanceOfSpatialResolution(value: object): value is SpatialResolution; +export declare function SpatialResolutionFromJSON(json: any): SpatialResolution; +export declare function SpatialResolutionFromJSONTyped(json: any, ignoreDiscriminator: boolean): SpatialResolution; +export declare function SpatialResolutionToJSON(json: any): SpatialResolution; +export declare function SpatialResolutionToJSONTyped(value?: SpatialResolution | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/esm/models/SpatialResolution.js b/api-clients/typescript/dist/esm/models/SpatialResolution.js new file mode 100644 index 0000000000..54d3218417 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/SpatialResolution.js @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * Check if a given object implements the SpatialResolution interface. + */ +export function instanceOfSpatialResolution(value) { + if (!('x' in value) || value['x'] === undefined) + return false; + if (!('y' in value) || value['y'] === undefined) + return false; + return true; +} +export function SpatialResolutionFromJSON(json) { + return SpatialResolutionFromJSONTyped(json, false); +} +export function SpatialResolutionFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'x': json['x'], + 'y': json['y'], + }; +} +export function SpatialResolutionToJSON(json) { + return SpatialResolutionToJSONTyped(json, false); +} +export function SpatialResolutionToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'x': value['x'], + 'y': value['y'], + }; +} diff --git a/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.d.ts b/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.d.ts new file mode 100644 index 0000000000..d11daa09fc --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.d.ts @@ -0,0 +1,95 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import type { StacProviderS3Config } from './StacProviderS3Config'; +import type { StacProviderDataset } from './StacProviderDataset'; +import type { TimeDimension } from './TimeDimension'; +/** + * + * @export + * @interface StacDataProviderDefinition + */ +export interface StacDataProviderDefinition { + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + apiUrl: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + collectionName: string; + /** + * + * @type {Array} + * @memberof StacDataProviderDefinition + */ + datasets: Array; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + description: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + id: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + name: string; + /** + * + * @type {number} + * @memberof StacDataProviderDefinition + */ + priority?: number | null; + /** + * + * @type {StacProviderS3Config} + * @memberof StacDataProviderDefinition + */ + s3Config?: StacProviderS3Config | null; + /** + * + * @type {TimeDimension} + * @memberof StacDataProviderDefinition + */ + timeDimension: TimeDimension; + /** + * + * @type {StacDataProviderDefinitionTypeEnum} + * @memberof StacDataProviderDefinition + */ + type: StacDataProviderDefinitionTypeEnum; +} +/** + * @export + */ +export declare const StacDataProviderDefinitionTypeEnum: { + readonly StacProviderDefinition: "StacProviderDefinition"; +}; +export type StacDataProviderDefinitionTypeEnum = typeof StacDataProviderDefinitionTypeEnum[keyof typeof StacDataProviderDefinitionTypeEnum]; +/** + * Check if a given object implements the StacDataProviderDefinition interface. + */ +export declare function instanceOfStacDataProviderDefinition(value: object): value is StacDataProviderDefinition; +export declare function StacDataProviderDefinitionFromJSON(json: any): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionToJSON(json: any): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionToJSONTyped(value?: StacDataProviderDefinition | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.js b/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.js new file mode 100644 index 0000000000..8a3c5bd036 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacDataProviderDefinition.js @@ -0,0 +1,83 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { StacProviderS3ConfigFromJSON, StacProviderS3ConfigToJSON, } from './StacProviderS3Config'; +import { StacProviderDatasetFromJSON, StacProviderDatasetToJSON, } from './StacProviderDataset'; +import { TimeDimensionFromJSON, TimeDimensionToJSON, } from './TimeDimension'; +/** + * @export + */ +export const StacDataProviderDefinitionTypeEnum = { + StacProviderDefinition: 'StacProviderDefinition' +}; +/** + * Check if a given object implements the StacDataProviderDefinition interface. + */ +export function instanceOfStacDataProviderDefinition(value) { + if (!('apiUrl' in value) || value['apiUrl'] === undefined) + return false; + if (!('collectionName' in value) || value['collectionName'] === undefined) + return false; + if (!('datasets' in value) || value['datasets'] === undefined) + return false; + if (!('description' in value) || value['description'] === undefined) + return false; + if (!('id' in value) || value['id'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('timeDimension' in value) || value['timeDimension'] === undefined) + return false; + if (!('type' in value) || value['type'] === undefined) + return false; + return true; +} +export function StacDataProviderDefinitionFromJSON(json) { + return StacDataProviderDefinitionFromJSONTyped(json, false); +} +export function StacDataProviderDefinitionFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'apiUrl': json['apiUrl'], + 'collectionName': json['collectionName'], + 'datasets': (json['datasets'].map(StacProviderDatasetFromJSON)), + 'description': json['description'], + 'id': json['id'], + 'name': json['name'], + 'priority': json['priority'] == null ? undefined : json['priority'], + 's3Config': json['s3Config'] == null ? undefined : StacProviderS3ConfigFromJSON(json['s3Config']), + 'timeDimension': TimeDimensionFromJSON(json['timeDimension']), + 'type': json['type'], + }; +} +export function StacDataProviderDefinitionToJSON(json) { + return StacDataProviderDefinitionToJSONTyped(json, false); +} +export function StacDataProviderDefinitionToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'apiUrl': value['apiUrl'], + 'collectionName': value['collectionName'], + 'datasets': (value['datasets'].map(StacProviderDatasetToJSON)), + 'description': value['description'], + 'id': value['id'], + 'name': value['name'], + 'priority': value['priority'], + 's3Config': StacProviderS3ConfigToJSON(value['s3Config']), + 'timeDimension': TimeDimensionToJSON(value['timeDimension']), + 'type': value['type'], + }; +} diff --git a/api-clients/typescript/dist/esm/models/StacProviderDataset.d.ts b/api-clients/typescript/dist/esm/models/StacProviderDataset.d.ts new file mode 100644 index 0000000000..8364f84933 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderDataset.d.ts @@ -0,0 +1,71 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import type { SpatialResolution } from './SpatialResolution'; +import type { StacProviderDatasetBand } from './StacProviderDatasetBand'; +import type { SpatialGridDescriptor } from './SpatialGridDescriptor'; +import type { RasterDataType } from './RasterDataType'; +/** + * + * @export + * @interface StacProviderDataset + */ +export interface StacProviderDataset { + /** + * + * @type {Array} + * @memberof StacProviderDataset + */ + bands: Array; + /** + * + * @type {RasterDataType} + * @memberof StacProviderDataset + */ + dataType: RasterDataType; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + description: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + name: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + projection: string; + /** + * + * @type {SpatialResolution} + * @memberof StacProviderDataset + */ + resolution: SpatialResolution; + /** + * + * @type {SpatialGridDescriptor} + * @memberof StacProviderDataset + */ + spatialGrid: SpatialGridDescriptor; +} +/** + * Check if a given object implements the StacProviderDataset interface. + */ +export declare function instanceOfStacProviderDataset(value: object): value is StacProviderDataset; +export declare function StacProviderDatasetFromJSON(json: any): StacProviderDataset; +export declare function StacProviderDatasetFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDataset; +export declare function StacProviderDatasetToJSON(json: any): StacProviderDataset; +export declare function StacProviderDatasetToJSONTyped(value?: StacProviderDataset | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/esm/models/StacProviderDataset.js b/api-clients/typescript/dist/esm/models/StacProviderDataset.js new file mode 100644 index 0000000000..c0bcb16ab4 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderDataset.js @@ -0,0 +1,70 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { SpatialResolutionFromJSON, SpatialResolutionToJSON, } from './SpatialResolution'; +import { StacProviderDatasetBandFromJSON, StacProviderDatasetBandToJSON, } from './StacProviderDatasetBand'; +import { SpatialGridDescriptorFromJSON, SpatialGridDescriptorToJSON, } from './SpatialGridDescriptor'; +import { RasterDataTypeFromJSON, RasterDataTypeToJSON, } from './RasterDataType'; +/** + * Check if a given object implements the StacProviderDataset interface. + */ +export function instanceOfStacProviderDataset(value) { + if (!('bands' in value) || value['bands'] === undefined) + return false; + if (!('dataType' in value) || value['dataType'] === undefined) + return false; + if (!('description' in value) || value['description'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('projection' in value) || value['projection'] === undefined) + return false; + if (!('resolution' in value) || value['resolution'] === undefined) + return false; + if (!('spatialGrid' in value) || value['spatialGrid'] === undefined) + return false; + return true; +} +export function StacProviderDatasetFromJSON(json) { + return StacProviderDatasetFromJSONTyped(json, false); +} +export function StacProviderDatasetFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'bands': (json['bands'].map(StacProviderDatasetBandFromJSON)), + 'dataType': RasterDataTypeFromJSON(json['dataType']), + 'description': json['description'], + 'name': json['name'], + 'projection': json['projection'], + 'resolution': SpatialResolutionFromJSON(json['resolution']), + 'spatialGrid': SpatialGridDescriptorFromJSON(json['spatialGrid']), + }; +} +export function StacProviderDatasetToJSON(json) { + return StacProviderDatasetToJSONTyped(json, false); +} +export function StacProviderDatasetToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'bands': (value['bands'].map(StacProviderDatasetBandToJSON)), + 'dataType': RasterDataTypeToJSON(value['dataType']), + 'description': value['description'], + 'name': value['name'], + 'projection': value['projection'], + 'resolution': SpatialResolutionToJSON(value['resolution']), + 'spatialGrid': SpatialGridDescriptorToJSON(value['spatialGrid']), + }; +} diff --git a/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.d.ts b/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.d.ts new file mode 100644 index 0000000000..b1199183c5 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.d.ts @@ -0,0 +1,37 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * + * @export + * @interface StacProviderDatasetBand + */ +export interface StacProviderDatasetBand { + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + assetTitle: string; + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + bandName?: string | null; +} +/** + * Check if a given object implements the StacProviderDatasetBand interface. + */ +export declare function instanceOfStacProviderDatasetBand(value: object): value is StacProviderDatasetBand; +export declare function StacProviderDatasetBandFromJSON(json: any): StacProviderDatasetBand; +export declare function StacProviderDatasetBandFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDatasetBand; +export declare function StacProviderDatasetBandToJSON(json: any): StacProviderDatasetBand; +export declare function StacProviderDatasetBandToJSONTyped(value?: StacProviderDatasetBand | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.js b/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.js new file mode 100644 index 0000000000..5dc9d4a4f0 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderDatasetBand.js @@ -0,0 +1,44 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * Check if a given object implements the StacProviderDatasetBand interface. + */ +export function instanceOfStacProviderDatasetBand(value) { + if (!('assetTitle' in value) || value['assetTitle'] === undefined) + return false; + return true; +} +export function StacProviderDatasetBandFromJSON(json) { + return StacProviderDatasetBandFromJSONTyped(json, false); +} +export function StacProviderDatasetBandFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'assetTitle': json['assetTitle'], + 'bandName': json['bandName'] == null ? undefined : json['bandName'], + }; +} +export function StacProviderDatasetBandToJSON(json) { + return StacProviderDatasetBandToJSONTyped(json, false); +} +export function StacProviderDatasetBandToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'assetTitle': value['assetTitle'], + 'bandName': value['bandName'], + }; +} diff --git a/api-clients/typescript/dist/esm/models/StacProviderS3Config.d.ts b/api-clients/typescript/dist/esm/models/StacProviderS3Config.d.ts new file mode 100644 index 0000000000..e414b98b86 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderS3Config.d.ts @@ -0,0 +1,43 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * + * @export + * @interface StacProviderS3Config + */ +export interface StacProviderS3Config { + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + accessKey?: string | null; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + endpoint: string; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + secretKey?: string | null; +} +/** + * Check if a given object implements the StacProviderS3Config interface. + */ +export declare function instanceOfStacProviderS3Config(value: object): value is StacProviderS3Config; +export declare function StacProviderS3ConfigFromJSON(json: any): StacProviderS3Config; +export declare function StacProviderS3ConfigFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderS3Config; +export declare function StacProviderS3ConfigToJSON(json: any): StacProviderS3Config; +export declare function StacProviderS3ConfigToJSONTyped(value?: StacProviderS3Config | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/esm/models/StacProviderS3Config.js b/api-clients/typescript/dist/esm/models/StacProviderS3Config.js new file mode 100644 index 0000000000..495ec37208 --- /dev/null +++ b/api-clients/typescript/dist/esm/models/StacProviderS3Config.js @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * Check if a given object implements the StacProviderS3Config interface. + */ +export function instanceOfStacProviderS3Config(value) { + if (!('endpoint' in value) || value['endpoint'] === undefined) + return false; + return true; +} +export function StacProviderS3ConfigFromJSON(json) { + return StacProviderS3ConfigFromJSONTyped(json, false); +} +export function StacProviderS3ConfigFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'accessKey': json['accessKey'] == null ? undefined : json['accessKey'], + 'endpoint': json['endpoint'], + 'secretKey': json['secretKey'] == null ? undefined : json['secretKey'], + }; +} +export function StacProviderS3ConfigToJSON(json) { + return StacProviderS3ConfigToJSONTyped(json, false); +} +export function StacProviderS3ConfigToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'accessKey': value['accessKey'], + 'endpoint': value['endpoint'], + 'secretKey': value['secretKey'], + }; +} diff --git a/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.d.ts b/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.d.ts index 913ca27419..726e5544e9 100644 --- a/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.d.ts +++ b/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.d.ts @@ -19,6 +19,7 @@ import type { GfbioCollectionsDataProviderDefinition } from './GfbioCollectionsD import type { NetCdfCfDataProviderDefinition } from './NetCdfCfDataProviderDefinition'; import type { PangaeaDataProviderDefinition } from './PangaeaDataProviderDefinition'; import type { SentinelS2L2ACogsProviderDefinition } from './SentinelS2L2ACogsProviderDefinition'; +import type { StacDataProviderDefinition } from './StacDataProviderDefinition'; import type { WildliveDataConnectorDefinition } from './WildliveDataConnectorDefinition'; /** * @type TypedDataProviderDefinition @@ -48,6 +49,8 @@ export type TypedDataProviderDefinition = { } & PangaeaDataProviderDefinition | { type: 'SentinelS2L2ACogs'; } & SentinelS2L2ACogsProviderDefinition | { + type: 'StacProviderDefinition'; +} & StacDataProviderDefinition | { type: 'WildLIVE!'; } & WildliveDataConnectorDefinition; export declare function TypedDataProviderDefinitionFromJSON(json: any): TypedDataProviderDefinition; diff --git a/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.js b/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.js index b21891025b..2d231bf038 100644 --- a/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.js +++ b/api-clients/typescript/dist/esm/models/TypedDataProviderDefinition.js @@ -21,6 +21,7 @@ import { GfbioCollectionsDataProviderDefinitionFromJSONTyped, GfbioCollectionsDa import { NetCdfCfDataProviderDefinitionFromJSONTyped, NetCdfCfDataProviderDefinitionToJSON, } from './NetCdfCfDataProviderDefinition'; import { PangaeaDataProviderDefinitionFromJSONTyped, PangaeaDataProviderDefinitionToJSON, } from './PangaeaDataProviderDefinition'; import { SentinelS2L2ACogsProviderDefinitionFromJSONTyped, SentinelS2L2ACogsProviderDefinitionToJSON, } from './SentinelS2L2ACogsProviderDefinition'; +import { StacDataProviderDefinitionFromJSONTyped, StacDataProviderDefinitionToJSON, } from './StacDataProviderDefinition'; import { WildliveDataConnectorDefinitionFromJSONTyped, WildliveDataConnectorDefinitionToJSON, } from './WildliveDataConnectorDefinition'; export function TypedDataProviderDefinitionFromJSON(json) { return TypedDataProviderDefinitionFromJSONTyped(json, false); @@ -52,6 +53,8 @@ export function TypedDataProviderDefinitionFromJSONTyped(json, ignoreDiscriminat return Object.assign({}, PangaeaDataProviderDefinitionFromJSONTyped(json, true), { type: 'Pangaea' }); case 'SentinelS2L2ACogs': return Object.assign({}, SentinelS2L2ACogsProviderDefinitionFromJSONTyped(json, true), { type: 'SentinelS2L2ACogs' }); + case 'StacProviderDefinition': + return Object.assign({}, StacDataProviderDefinitionFromJSONTyped(json, true), { type: 'StacProviderDefinition' }); case 'WildLIVE!': return Object.assign({}, WildliveDataConnectorDefinitionFromJSONTyped(json, true), { type: 'WildLIVE!' }); default: @@ -88,6 +91,8 @@ export function TypedDataProviderDefinitionToJSONTyped(value, ignoreDiscriminato return Object.assign({}, PangaeaDataProviderDefinitionToJSON(value), { type: 'Pangaea' }); case 'SentinelS2L2ACogs': return Object.assign({}, SentinelS2L2ACogsProviderDefinitionToJSON(value), { type: 'SentinelS2L2ACogs' }); + case 'StacProviderDefinition': + return Object.assign({}, StacDataProviderDefinitionToJSON(value), { type: 'StacProviderDefinition' }); case 'WildLIVE!': return Object.assign({}, WildliveDataConnectorDefinitionToJSON(value), { type: 'WildLIVE!' }); default: diff --git a/api-clients/typescript/dist/esm/models/index.d.ts b/api-clients/typescript/dist/esm/models/index.d.ts index 42d17814fd..7cbb54abe8 100644 --- a/api-clients/typescript/dist/esm/models/index.d.ts +++ b/api-clients/typescript/dist/esm/models/index.d.ts @@ -233,7 +233,12 @@ export * from './SpatialGridDescriptor'; export * from './SpatialGridDescriptorState'; export * from './SpatialPartition2D'; export * from './SpatialReferenceSpecification'; +export * from './SpatialResolution'; export * from './StacApiRetries'; +export * from './StacDataProviderDefinition'; +export * from './StacProviderDataset'; +export * from './StacProviderDatasetBand'; +export * from './StacProviderS3Config'; export * from './StacQueryBuffer'; export * from './StaticColor'; export * from './StaticNumber'; diff --git a/api-clients/typescript/dist/esm/models/index.js b/api-clients/typescript/dist/esm/models/index.js index b89e84a15f..ce2f2e856f 100644 --- a/api-clients/typescript/dist/esm/models/index.js +++ b/api-clients/typescript/dist/esm/models/index.js @@ -235,7 +235,12 @@ export * from './SpatialGridDescriptor'; export * from './SpatialGridDescriptorState'; export * from './SpatialPartition2D'; export * from './SpatialReferenceSpecification'; +export * from './SpatialResolution'; export * from './StacApiRetries'; +export * from './StacDataProviderDefinition'; +export * from './StacProviderDataset'; +export * from './StacProviderDatasetBand'; +export * from './StacProviderS3Config'; export * from './StacQueryBuffer'; export * from './StaticColor'; export * from './StaticNumber'; diff --git a/api-clients/typescript/dist/models/SpatialResolution.d.ts b/api-clients/typescript/dist/models/SpatialResolution.d.ts new file mode 100644 index 0000000000..3d7616a7ea --- /dev/null +++ b/api-clients/typescript/dist/models/SpatialResolution.d.ts @@ -0,0 +1,37 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * The spatial resolution in SRS units + * @export + * @interface SpatialResolution + */ +export interface SpatialResolution { + /** + * + * @type {number} + * @memberof SpatialResolution + */ + x: number; + /** + * + * @type {number} + * @memberof SpatialResolution + */ + y: number; +} +/** + * Check if a given object implements the SpatialResolution interface. + */ +export declare function instanceOfSpatialResolution(value: object): value is SpatialResolution; +export declare function SpatialResolutionFromJSON(json: any): SpatialResolution; +export declare function SpatialResolutionFromJSONTyped(json: any, ignoreDiscriminator: boolean): SpatialResolution; +export declare function SpatialResolutionToJSON(json: any): SpatialResolution; +export declare function SpatialResolutionToJSONTyped(value?: SpatialResolution | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/models/SpatialResolution.js b/api-clients/typescript/dist/models/SpatialResolution.js new file mode 100644 index 0000000000..587b3c5b4f --- /dev/null +++ b/api-clients/typescript/dist/models/SpatialResolution.js @@ -0,0 +1,53 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.instanceOfSpatialResolution = instanceOfSpatialResolution; +exports.SpatialResolutionFromJSON = SpatialResolutionFromJSON; +exports.SpatialResolutionFromJSONTyped = SpatialResolutionFromJSONTyped; +exports.SpatialResolutionToJSON = SpatialResolutionToJSON; +exports.SpatialResolutionToJSONTyped = SpatialResolutionToJSONTyped; +/** + * Check if a given object implements the SpatialResolution interface. + */ +function instanceOfSpatialResolution(value) { + if (!('x' in value) || value['x'] === undefined) + return false; + if (!('y' in value) || value['y'] === undefined) + return false; + return true; +} +function SpatialResolutionFromJSON(json) { + return SpatialResolutionFromJSONTyped(json, false); +} +function SpatialResolutionFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'x': json['x'], + 'y': json['y'], + }; +} +function SpatialResolutionToJSON(json) { + return SpatialResolutionToJSONTyped(json, false); +} +function SpatialResolutionToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'x': value['x'], + 'y': value['y'], + }; +} diff --git a/api-clients/typescript/dist/models/StacDataProviderDefinition.d.ts b/api-clients/typescript/dist/models/StacDataProviderDefinition.d.ts new file mode 100644 index 0000000000..d11daa09fc --- /dev/null +++ b/api-clients/typescript/dist/models/StacDataProviderDefinition.d.ts @@ -0,0 +1,95 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import type { StacProviderS3Config } from './StacProviderS3Config'; +import type { StacProviderDataset } from './StacProviderDataset'; +import type { TimeDimension } from './TimeDimension'; +/** + * + * @export + * @interface StacDataProviderDefinition + */ +export interface StacDataProviderDefinition { + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + apiUrl: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + collectionName: string; + /** + * + * @type {Array} + * @memberof StacDataProviderDefinition + */ + datasets: Array; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + description: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + id: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + name: string; + /** + * + * @type {number} + * @memberof StacDataProviderDefinition + */ + priority?: number | null; + /** + * + * @type {StacProviderS3Config} + * @memberof StacDataProviderDefinition + */ + s3Config?: StacProviderS3Config | null; + /** + * + * @type {TimeDimension} + * @memberof StacDataProviderDefinition + */ + timeDimension: TimeDimension; + /** + * + * @type {StacDataProviderDefinitionTypeEnum} + * @memberof StacDataProviderDefinition + */ + type: StacDataProviderDefinitionTypeEnum; +} +/** + * @export + */ +export declare const StacDataProviderDefinitionTypeEnum: { + readonly StacProviderDefinition: "StacProviderDefinition"; +}; +export type StacDataProviderDefinitionTypeEnum = typeof StacDataProviderDefinitionTypeEnum[keyof typeof StacDataProviderDefinitionTypeEnum]; +/** + * Check if a given object implements the StacDataProviderDefinition interface. + */ +export declare function instanceOfStacDataProviderDefinition(value: object): value is StacDataProviderDefinition; +export declare function StacDataProviderDefinitionFromJSON(json: any): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionToJSON(json: any): StacDataProviderDefinition; +export declare function StacDataProviderDefinitionToJSONTyped(value?: StacDataProviderDefinition | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/models/StacDataProviderDefinition.js b/api-clients/typescript/dist/models/StacDataProviderDefinition.js new file mode 100644 index 0000000000..ebcfe3e7c6 --- /dev/null +++ b/api-clients/typescript/dist/models/StacDataProviderDefinition.js @@ -0,0 +1,91 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StacDataProviderDefinitionTypeEnum = void 0; +exports.instanceOfStacDataProviderDefinition = instanceOfStacDataProviderDefinition; +exports.StacDataProviderDefinitionFromJSON = StacDataProviderDefinitionFromJSON; +exports.StacDataProviderDefinitionFromJSONTyped = StacDataProviderDefinitionFromJSONTyped; +exports.StacDataProviderDefinitionToJSON = StacDataProviderDefinitionToJSON; +exports.StacDataProviderDefinitionToJSONTyped = StacDataProviderDefinitionToJSONTyped; +const StacProviderS3Config_1 = require("./StacProviderS3Config"); +const StacProviderDataset_1 = require("./StacProviderDataset"); +const TimeDimension_1 = require("./TimeDimension"); +/** + * @export + */ +exports.StacDataProviderDefinitionTypeEnum = { + StacProviderDefinition: 'StacProviderDefinition' +}; +/** + * Check if a given object implements the StacDataProviderDefinition interface. + */ +function instanceOfStacDataProviderDefinition(value) { + if (!('apiUrl' in value) || value['apiUrl'] === undefined) + return false; + if (!('collectionName' in value) || value['collectionName'] === undefined) + return false; + if (!('datasets' in value) || value['datasets'] === undefined) + return false; + if (!('description' in value) || value['description'] === undefined) + return false; + if (!('id' in value) || value['id'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('timeDimension' in value) || value['timeDimension'] === undefined) + return false; + if (!('type' in value) || value['type'] === undefined) + return false; + return true; +} +function StacDataProviderDefinitionFromJSON(json) { + return StacDataProviderDefinitionFromJSONTyped(json, false); +} +function StacDataProviderDefinitionFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'apiUrl': json['apiUrl'], + 'collectionName': json['collectionName'], + 'datasets': (json['datasets'].map(StacProviderDataset_1.StacProviderDatasetFromJSON)), + 'description': json['description'], + 'id': json['id'], + 'name': json['name'], + 'priority': json['priority'] == null ? undefined : json['priority'], + 's3Config': json['s3Config'] == null ? undefined : (0, StacProviderS3Config_1.StacProviderS3ConfigFromJSON)(json['s3Config']), + 'timeDimension': (0, TimeDimension_1.TimeDimensionFromJSON)(json['timeDimension']), + 'type': json['type'], + }; +} +function StacDataProviderDefinitionToJSON(json) { + return StacDataProviderDefinitionToJSONTyped(json, false); +} +function StacDataProviderDefinitionToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'apiUrl': value['apiUrl'], + 'collectionName': value['collectionName'], + 'datasets': (value['datasets'].map(StacProviderDataset_1.StacProviderDatasetToJSON)), + 'description': value['description'], + 'id': value['id'], + 'name': value['name'], + 'priority': value['priority'], + 's3Config': (0, StacProviderS3Config_1.StacProviderS3ConfigToJSON)(value['s3Config']), + 'timeDimension': (0, TimeDimension_1.TimeDimensionToJSON)(value['timeDimension']), + 'type': value['type'], + }; +} diff --git a/api-clients/typescript/dist/models/StacProviderDataset.d.ts b/api-clients/typescript/dist/models/StacProviderDataset.d.ts new file mode 100644 index 0000000000..8364f84933 --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderDataset.d.ts @@ -0,0 +1,71 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import type { SpatialResolution } from './SpatialResolution'; +import type { StacProviderDatasetBand } from './StacProviderDatasetBand'; +import type { SpatialGridDescriptor } from './SpatialGridDescriptor'; +import type { RasterDataType } from './RasterDataType'; +/** + * + * @export + * @interface StacProviderDataset + */ +export interface StacProviderDataset { + /** + * + * @type {Array} + * @memberof StacProviderDataset + */ + bands: Array; + /** + * + * @type {RasterDataType} + * @memberof StacProviderDataset + */ + dataType: RasterDataType; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + description: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + name: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + projection: string; + /** + * + * @type {SpatialResolution} + * @memberof StacProviderDataset + */ + resolution: SpatialResolution; + /** + * + * @type {SpatialGridDescriptor} + * @memberof StacProviderDataset + */ + spatialGrid: SpatialGridDescriptor; +} +/** + * Check if a given object implements the StacProviderDataset interface. + */ +export declare function instanceOfStacProviderDataset(value: object): value is StacProviderDataset; +export declare function StacProviderDatasetFromJSON(json: any): StacProviderDataset; +export declare function StacProviderDatasetFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDataset; +export declare function StacProviderDatasetToJSON(json: any): StacProviderDataset; +export declare function StacProviderDatasetToJSONTyped(value?: StacProviderDataset | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/models/StacProviderDataset.js b/api-clients/typescript/dist/models/StacProviderDataset.js new file mode 100644 index 0000000000..6ba9ee89c2 --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderDataset.js @@ -0,0 +1,77 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.instanceOfStacProviderDataset = instanceOfStacProviderDataset; +exports.StacProviderDatasetFromJSON = StacProviderDatasetFromJSON; +exports.StacProviderDatasetFromJSONTyped = StacProviderDatasetFromJSONTyped; +exports.StacProviderDatasetToJSON = StacProviderDatasetToJSON; +exports.StacProviderDatasetToJSONTyped = StacProviderDatasetToJSONTyped; +const SpatialResolution_1 = require("./SpatialResolution"); +const StacProviderDatasetBand_1 = require("./StacProviderDatasetBand"); +const SpatialGridDescriptor_1 = require("./SpatialGridDescriptor"); +const RasterDataType_1 = require("./RasterDataType"); +/** + * Check if a given object implements the StacProviderDataset interface. + */ +function instanceOfStacProviderDataset(value) { + if (!('bands' in value) || value['bands'] === undefined) + return false; + if (!('dataType' in value) || value['dataType'] === undefined) + return false; + if (!('description' in value) || value['description'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('projection' in value) || value['projection'] === undefined) + return false; + if (!('resolution' in value) || value['resolution'] === undefined) + return false; + if (!('spatialGrid' in value) || value['spatialGrid'] === undefined) + return false; + return true; +} +function StacProviderDatasetFromJSON(json) { + return StacProviderDatasetFromJSONTyped(json, false); +} +function StacProviderDatasetFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'bands': (json['bands'].map(StacProviderDatasetBand_1.StacProviderDatasetBandFromJSON)), + 'dataType': (0, RasterDataType_1.RasterDataTypeFromJSON)(json['dataType']), + 'description': json['description'], + 'name': json['name'], + 'projection': json['projection'], + 'resolution': (0, SpatialResolution_1.SpatialResolutionFromJSON)(json['resolution']), + 'spatialGrid': (0, SpatialGridDescriptor_1.SpatialGridDescriptorFromJSON)(json['spatialGrid']), + }; +} +function StacProviderDatasetToJSON(json) { + return StacProviderDatasetToJSONTyped(json, false); +} +function StacProviderDatasetToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'bands': (value['bands'].map(StacProviderDatasetBand_1.StacProviderDatasetBandToJSON)), + 'dataType': (0, RasterDataType_1.RasterDataTypeToJSON)(value['dataType']), + 'description': value['description'], + 'name': value['name'], + 'projection': value['projection'], + 'resolution': (0, SpatialResolution_1.SpatialResolutionToJSON)(value['resolution']), + 'spatialGrid': (0, SpatialGridDescriptor_1.SpatialGridDescriptorToJSON)(value['spatialGrid']), + }; +} diff --git a/api-clients/typescript/dist/models/StacProviderDatasetBand.d.ts b/api-clients/typescript/dist/models/StacProviderDatasetBand.d.ts new file mode 100644 index 0000000000..b1199183c5 --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderDatasetBand.d.ts @@ -0,0 +1,37 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * + * @export + * @interface StacProviderDatasetBand + */ +export interface StacProviderDatasetBand { + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + assetTitle: string; + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + bandName?: string | null; +} +/** + * Check if a given object implements the StacProviderDatasetBand interface. + */ +export declare function instanceOfStacProviderDatasetBand(value: object): value is StacProviderDatasetBand; +export declare function StacProviderDatasetBandFromJSON(json: any): StacProviderDatasetBand; +export declare function StacProviderDatasetBandFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDatasetBand; +export declare function StacProviderDatasetBandToJSON(json: any): StacProviderDatasetBand; +export declare function StacProviderDatasetBandToJSONTyped(value?: StacProviderDatasetBand | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/models/StacProviderDatasetBand.js b/api-clients/typescript/dist/models/StacProviderDatasetBand.js new file mode 100644 index 0000000000..d16f200b95 --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderDatasetBand.js @@ -0,0 +1,51 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.instanceOfStacProviderDatasetBand = instanceOfStacProviderDatasetBand; +exports.StacProviderDatasetBandFromJSON = StacProviderDatasetBandFromJSON; +exports.StacProviderDatasetBandFromJSONTyped = StacProviderDatasetBandFromJSONTyped; +exports.StacProviderDatasetBandToJSON = StacProviderDatasetBandToJSON; +exports.StacProviderDatasetBandToJSONTyped = StacProviderDatasetBandToJSONTyped; +/** + * Check if a given object implements the StacProviderDatasetBand interface. + */ +function instanceOfStacProviderDatasetBand(value) { + if (!('assetTitle' in value) || value['assetTitle'] === undefined) + return false; + return true; +} +function StacProviderDatasetBandFromJSON(json) { + return StacProviderDatasetBandFromJSONTyped(json, false); +} +function StacProviderDatasetBandFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'assetTitle': json['assetTitle'], + 'bandName': json['bandName'] == null ? undefined : json['bandName'], + }; +} +function StacProviderDatasetBandToJSON(json) { + return StacProviderDatasetBandToJSONTyped(json, false); +} +function StacProviderDatasetBandToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'assetTitle': value['assetTitle'], + 'bandName': value['bandName'], + }; +} diff --git a/api-clients/typescript/dist/models/StacProviderS3Config.d.ts b/api-clients/typescript/dist/models/StacProviderS3Config.d.ts new file mode 100644 index 0000000000..e414b98b86 --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderS3Config.d.ts @@ -0,0 +1,43 @@ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/** + * + * @export + * @interface StacProviderS3Config + */ +export interface StacProviderS3Config { + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + accessKey?: string | null; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + endpoint: string; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + secretKey?: string | null; +} +/** + * Check if a given object implements the StacProviderS3Config interface. + */ +export declare function instanceOfStacProviderS3Config(value: object): value is StacProviderS3Config; +export declare function StacProviderS3ConfigFromJSON(json: any): StacProviderS3Config; +export declare function StacProviderS3ConfigFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderS3Config; +export declare function StacProviderS3ConfigToJSON(json: any): StacProviderS3Config; +export declare function StacProviderS3ConfigToJSONTyped(value?: StacProviderS3Config | null, ignoreDiscriminator?: boolean): any; diff --git a/api-clients/typescript/dist/models/StacProviderS3Config.js b/api-clients/typescript/dist/models/StacProviderS3Config.js new file mode 100644 index 0000000000..b15c7b2abc --- /dev/null +++ b/api-clients/typescript/dist/models/StacProviderS3Config.js @@ -0,0 +1,53 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.instanceOfStacProviderS3Config = instanceOfStacProviderS3Config; +exports.StacProviderS3ConfigFromJSON = StacProviderS3ConfigFromJSON; +exports.StacProviderS3ConfigFromJSONTyped = StacProviderS3ConfigFromJSONTyped; +exports.StacProviderS3ConfigToJSON = StacProviderS3ConfigToJSON; +exports.StacProviderS3ConfigToJSONTyped = StacProviderS3ConfigToJSONTyped; +/** + * Check if a given object implements the StacProviderS3Config interface. + */ +function instanceOfStacProviderS3Config(value) { + if (!('endpoint' in value) || value['endpoint'] === undefined) + return false; + return true; +} +function StacProviderS3ConfigFromJSON(json) { + return StacProviderS3ConfigFromJSONTyped(json, false); +} +function StacProviderS3ConfigFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'accessKey': json['accessKey'] == null ? undefined : json['accessKey'], + 'endpoint': json['endpoint'], + 'secretKey': json['secretKey'] == null ? undefined : json['secretKey'], + }; +} +function StacProviderS3ConfigToJSON(json) { + return StacProviderS3ConfigToJSONTyped(json, false); +} +function StacProviderS3ConfigToJSONTyped(value, ignoreDiscriminator = false) { + if (value == null) { + return value; + } + return { + 'accessKey': value['accessKey'], + 'endpoint': value['endpoint'], + 'secretKey': value['secretKey'], + }; +} diff --git a/api-clients/typescript/dist/models/TypedDataProviderDefinition.d.ts b/api-clients/typescript/dist/models/TypedDataProviderDefinition.d.ts index 913ca27419..726e5544e9 100644 --- a/api-clients/typescript/dist/models/TypedDataProviderDefinition.d.ts +++ b/api-clients/typescript/dist/models/TypedDataProviderDefinition.d.ts @@ -19,6 +19,7 @@ import type { GfbioCollectionsDataProviderDefinition } from './GfbioCollectionsD import type { NetCdfCfDataProviderDefinition } from './NetCdfCfDataProviderDefinition'; import type { PangaeaDataProviderDefinition } from './PangaeaDataProviderDefinition'; import type { SentinelS2L2ACogsProviderDefinition } from './SentinelS2L2ACogsProviderDefinition'; +import type { StacDataProviderDefinition } from './StacDataProviderDefinition'; import type { WildliveDataConnectorDefinition } from './WildliveDataConnectorDefinition'; /** * @type TypedDataProviderDefinition @@ -48,6 +49,8 @@ export type TypedDataProviderDefinition = { } & PangaeaDataProviderDefinition | { type: 'SentinelS2L2ACogs'; } & SentinelS2L2ACogsProviderDefinition | { + type: 'StacProviderDefinition'; +} & StacDataProviderDefinition | { type: 'WildLIVE!'; } & WildliveDataConnectorDefinition; export declare function TypedDataProviderDefinitionFromJSON(json: any): TypedDataProviderDefinition; diff --git a/api-clients/typescript/dist/models/TypedDataProviderDefinition.js b/api-clients/typescript/dist/models/TypedDataProviderDefinition.js index 8efd6c0b92..2405097c53 100644 --- a/api-clients/typescript/dist/models/TypedDataProviderDefinition.js +++ b/api-clients/typescript/dist/models/TypedDataProviderDefinition.js @@ -27,6 +27,7 @@ const GfbioCollectionsDataProviderDefinition_1 = require("./GfbioCollectionsData const NetCdfCfDataProviderDefinition_1 = require("./NetCdfCfDataProviderDefinition"); const PangaeaDataProviderDefinition_1 = require("./PangaeaDataProviderDefinition"); const SentinelS2L2ACogsProviderDefinition_1 = require("./SentinelS2L2ACogsProviderDefinition"); +const StacDataProviderDefinition_1 = require("./StacDataProviderDefinition"); const WildliveDataConnectorDefinition_1 = require("./WildliveDataConnectorDefinition"); function TypedDataProviderDefinitionFromJSON(json) { return TypedDataProviderDefinitionFromJSONTyped(json, false); @@ -58,6 +59,8 @@ function TypedDataProviderDefinitionFromJSONTyped(json, ignoreDiscriminator) { return Object.assign({}, (0, PangaeaDataProviderDefinition_1.PangaeaDataProviderDefinitionFromJSONTyped)(json, true), { type: 'Pangaea' }); case 'SentinelS2L2ACogs': return Object.assign({}, (0, SentinelS2L2ACogsProviderDefinition_1.SentinelS2L2ACogsProviderDefinitionFromJSONTyped)(json, true), { type: 'SentinelS2L2ACogs' }); + case 'StacProviderDefinition': + return Object.assign({}, (0, StacDataProviderDefinition_1.StacDataProviderDefinitionFromJSONTyped)(json, true), { type: 'StacProviderDefinition' }); case 'WildLIVE!': return Object.assign({}, (0, WildliveDataConnectorDefinition_1.WildliveDataConnectorDefinitionFromJSONTyped)(json, true), { type: 'WildLIVE!' }); default: @@ -94,6 +97,8 @@ function TypedDataProviderDefinitionToJSONTyped(value, ignoreDiscriminator = fal return Object.assign({}, (0, PangaeaDataProviderDefinition_1.PangaeaDataProviderDefinitionToJSON)(value), { type: 'Pangaea' }); case 'SentinelS2L2ACogs': return Object.assign({}, (0, SentinelS2L2ACogsProviderDefinition_1.SentinelS2L2ACogsProviderDefinitionToJSON)(value), { type: 'SentinelS2L2ACogs' }); + case 'StacProviderDefinition': + return Object.assign({}, (0, StacDataProviderDefinition_1.StacDataProviderDefinitionToJSON)(value), { type: 'StacProviderDefinition' }); case 'WildLIVE!': return Object.assign({}, (0, WildliveDataConnectorDefinition_1.WildliveDataConnectorDefinitionToJSON)(value), { type: 'WildLIVE!' }); default: diff --git a/api-clients/typescript/dist/models/index.d.ts b/api-clients/typescript/dist/models/index.d.ts index 42d17814fd..7cbb54abe8 100644 --- a/api-clients/typescript/dist/models/index.d.ts +++ b/api-clients/typescript/dist/models/index.d.ts @@ -233,7 +233,12 @@ export * from './SpatialGridDescriptor'; export * from './SpatialGridDescriptorState'; export * from './SpatialPartition2D'; export * from './SpatialReferenceSpecification'; +export * from './SpatialResolution'; export * from './StacApiRetries'; +export * from './StacDataProviderDefinition'; +export * from './StacProviderDataset'; +export * from './StacProviderDatasetBand'; +export * from './StacProviderS3Config'; export * from './StacQueryBuffer'; export * from './StaticColor'; export * from './StaticNumber'; diff --git a/api-clients/typescript/dist/models/index.js b/api-clients/typescript/dist/models/index.js index 47ac862d3c..7f60ee7f86 100644 --- a/api-clients/typescript/dist/models/index.js +++ b/api-clients/typescript/dist/models/index.js @@ -251,7 +251,12 @@ __exportStar(require("./SpatialGridDescriptor"), exports); __exportStar(require("./SpatialGridDescriptorState"), exports); __exportStar(require("./SpatialPartition2D"), exports); __exportStar(require("./SpatialReferenceSpecification"), exports); +__exportStar(require("./SpatialResolution"), exports); __exportStar(require("./StacApiRetries"), exports); +__exportStar(require("./StacDataProviderDefinition"), exports); +__exportStar(require("./StacProviderDataset"), exports); +__exportStar(require("./StacProviderDatasetBand"), exports); +__exportStar(require("./StacProviderS3Config"), exports); __exportStar(require("./StacQueryBuffer"), exports); __exportStar(require("./StaticColor"), exports); __exportStar(require("./StaticNumber"), exports); diff --git a/api-clients/typescript/docs/Regular1.md b/api-clients/typescript/docs/Regular1.md new file mode 100644 index 0000000000..dba59f0b44 --- /dev/null +++ b/api-clients/typescript/docs/Regular1.md @@ -0,0 +1,34 @@ + +# Regular1 + + +## Properties + +Name | Type +------------ | ------------- +`type` | string + +## Example + +```typescript +import type { Regular1 } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "type": null, +} satisfies Regular1 + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as Regular1 +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/SpatialResolution.md b/api-clients/typescript/docs/SpatialResolution.md new file mode 100644 index 0000000000..528fe82121 --- /dev/null +++ b/api-clients/typescript/docs/SpatialResolution.md @@ -0,0 +1,37 @@ + +# SpatialResolution + +The spatial resolution in SRS units + +## Properties + +Name | Type +------------ | ------------- +`x` | number +`y` | number + +## Example + +```typescript +import type { SpatialResolution } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "x": null, + "y": null, +} satisfies SpatialResolution + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as SpatialResolution +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacDataProviderDefinition.md b/api-clients/typescript/docs/StacDataProviderDefinition.md new file mode 100644 index 0000000000..71b3041b2f --- /dev/null +++ b/api-clients/typescript/docs/StacDataProviderDefinition.md @@ -0,0 +1,52 @@ + +# StacDataProviderDefinition + + +## Properties + +Name | Type +------------ | ------------- +`apiUrl` | string +`collectionName` | string +`datasets` | [Array<StacProviderDataset>](StacProviderDataset.md) +`description` | string +`id` | string +`name` | string +`priority` | number +`s3Config` | [StacProviderS3Config](StacProviderS3Config.md) +`timeDimension` | [TimeDimension](TimeDimension.md) +`type` | string + +## Example + +```typescript +import type { StacDataProviderDefinition } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "apiUrl": null, + "collectionName": null, + "datasets": null, + "description": null, + "id": null, + "name": null, + "priority": null, + "s3Config": null, + "timeDimension": null, + "type": null, +} satisfies StacDataProviderDefinition + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacDataProviderDefinition +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacProviderDataset.md b/api-clients/typescript/docs/StacProviderDataset.md new file mode 100644 index 0000000000..622d8db93e --- /dev/null +++ b/api-clients/typescript/docs/StacProviderDataset.md @@ -0,0 +1,46 @@ + +# StacProviderDataset + + +## Properties + +Name | Type +------------ | ------------- +`bands` | [Array<StacProviderDatasetBand>](StacProviderDatasetBand.md) +`dataType` | [RasterDataType](RasterDataType.md) +`description` | string +`name` | string +`projection` | string +`resolution` | [SpatialResolution](SpatialResolution.md) +`spatialGrid` | [SpatialGridDescriptor](SpatialGridDescriptor.md) + +## Example + +```typescript +import type { StacProviderDataset } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "bands": null, + "dataType": null, + "description": null, + "name": null, + "projection": null, + "resolution": null, + "spatialGrid": null, +} satisfies StacProviderDataset + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacProviderDataset +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacProviderDatasetBand.md b/api-clients/typescript/docs/StacProviderDatasetBand.md new file mode 100644 index 0000000000..bb5b3a7c32 --- /dev/null +++ b/api-clients/typescript/docs/StacProviderDatasetBand.md @@ -0,0 +1,36 @@ + +# StacProviderDatasetBand + + +## Properties + +Name | Type +------------ | ------------- +`assetTitle` | string +`bandName` | string + +## Example + +```typescript +import type { StacProviderDatasetBand } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "assetTitle": null, + "bandName": null, +} satisfies StacProviderDatasetBand + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacProviderDatasetBand +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacProviderDatasetResolution.md b/api-clients/typescript/docs/StacProviderDatasetResolution.md new file mode 100644 index 0000000000..71e461c299 --- /dev/null +++ b/api-clients/typescript/docs/StacProviderDatasetResolution.md @@ -0,0 +1,36 @@ + +# StacProviderDatasetResolution + + +## Properties + +Name | Type +------------ | ------------- +`x` | number +`y` | number + +## Example + +```typescript +import type { StacProviderDatasetResolution } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "x": null, + "y": null, +} satisfies StacProviderDatasetResolution + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacProviderDatasetResolution +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacProviderS3Config.md b/api-clients/typescript/docs/StacProviderS3Config.md new file mode 100644 index 0000000000..15e9761894 --- /dev/null +++ b/api-clients/typescript/docs/StacProviderS3Config.md @@ -0,0 +1,38 @@ + +# StacProviderS3Config + + +## Properties + +Name | Type +------------ | ------------- +`accessKey` | string +`endpoint` | string +`secretKey` | string + +## Example + +```typescript +import type { StacProviderS3Config } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "accessKey": null, + "endpoint": null, + "secretKey": null, +} satisfies StacProviderS3Config + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacProviderS3Config +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacTimeDimension.md b/api-clients/typescript/docs/StacTimeDimension.md new file mode 100644 index 0000000000..642d7d1223 --- /dev/null +++ b/api-clients/typescript/docs/StacTimeDimension.md @@ -0,0 +1,38 @@ + +# StacTimeDimension + + +## Properties + +Name | Type +------------ | ------------- +`origin` | number +`step` | [StacTimeStep](StacTimeStep.md) +`type` | string + +## Example + +```typescript +import type { StacTimeDimension } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "origin": null, + "step": null, + "type": null, +} satisfies StacTimeDimension + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacTimeDimension +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/StacTimeStep.md b/api-clients/typescript/docs/StacTimeStep.md new file mode 100644 index 0000000000..f2c467d28e --- /dev/null +++ b/api-clients/typescript/docs/StacTimeStep.md @@ -0,0 +1,36 @@ + +# StacTimeStep + + +## Properties + +Name | Type +------------ | ------------- +`granularity` | [TimeGranularity](TimeGranularity.md) +`value` | number + +## Example + +```typescript +import type { StacTimeStep } from '@geoengine/api-client' + +// TODO: Update the object below with actual values +const example = { + "granularity": null, + "value": null, +} satisfies StacTimeStep + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as StacTimeStep +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/api-clients/typescript/docs/TypedDataProviderDefinition.md b/api-clients/typescript/docs/TypedDataProviderDefinition.md index 9ad9bf6adc..e3983d96b0 100644 --- a/api-clients/typescript/docs/TypedDataProviderDefinition.md +++ b/api-clients/typescript/docs/TypedDataProviderDefinition.md @@ -38,6 +38,10 @@ Name | Type `gdalRetries` | number `queryBuffer` | [StacQueryBuffer](StacQueryBuffer.md) `stacApiRetries` | [StacApiRetries](StacApiRetries.md) +`collectionName` | string +`datasets` | [Array<StacProviderDataset>](StacProviderDataset.md) +`s3Config` | [StacProviderS3Config](StacProviderS3Config.md) +`timeDimension` | [TimeDimension](TimeDimension.md) `expiryDate` | Date `refreshToken` | string `user` | string @@ -81,6 +85,10 @@ const example = { "gdalRetries": null, "queryBuffer": null, "stacApiRetries": null, + "collectionName": null, + "datasets": null, + "s3Config": null, + "timeDimension": null, "expiryDate": null, "refreshToken": null, "user": null, diff --git a/api-clients/typescript/src/models/SpatialResolution.ts b/api-clients/typescript/src/models/SpatialResolution.ts new file mode 100644 index 0000000000..5f26b6d515 --- /dev/null +++ b/api-clients/typescript/src/models/SpatialResolution.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * The spatial resolution in SRS units + * @export + * @interface SpatialResolution + */ +export interface SpatialResolution { + /** + * + * @type {number} + * @memberof SpatialResolution + */ + x: number; + /** + * + * @type {number} + * @memberof SpatialResolution + */ + y: number; +} + +/** + * Check if a given object implements the SpatialResolution interface. + */ +export function instanceOfSpatialResolution(value: object): value is SpatialResolution { + if (!('x' in value) || value['x'] === undefined) return false; + if (!('y' in value) || value['y'] === undefined) return false; + return true; +} + +export function SpatialResolutionFromJSON(json: any): SpatialResolution { + return SpatialResolutionFromJSONTyped(json, false); +} + +export function SpatialResolutionFromJSONTyped(json: any, ignoreDiscriminator: boolean): SpatialResolution { + if (json == null) { + return json; + } + return { + + 'x': json['x'], + 'y': json['y'], + }; +} + +export function SpatialResolutionToJSON(json: any): SpatialResolution { + return SpatialResolutionToJSONTyped(json, false); +} + +export function SpatialResolutionToJSONTyped(value?: SpatialResolution | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'x': value['x'], + 'y': value['y'], + }; +} + diff --git a/api-clients/typescript/src/models/StacDataProviderDefinition.ts b/api-clients/typescript/src/models/StacDataProviderDefinition.ts new file mode 100644 index 0000000000..a4f342b9c2 --- /dev/null +++ b/api-clients/typescript/src/models/StacDataProviderDefinition.ts @@ -0,0 +1,176 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { StacProviderS3Config } from './StacProviderS3Config'; +import { + StacProviderS3ConfigFromJSON, + StacProviderS3ConfigFromJSONTyped, + StacProviderS3ConfigToJSON, + StacProviderS3ConfigToJSONTyped, +} from './StacProviderS3Config'; +import type { StacProviderDataset } from './StacProviderDataset'; +import { + StacProviderDatasetFromJSON, + StacProviderDatasetFromJSONTyped, + StacProviderDatasetToJSON, + StacProviderDatasetToJSONTyped, +} from './StacProviderDataset'; +import type { TimeDimension } from './TimeDimension'; +import { + TimeDimensionFromJSON, + TimeDimensionFromJSONTyped, + TimeDimensionToJSON, + TimeDimensionToJSONTyped, +} from './TimeDimension'; + +/** + * + * @export + * @interface StacDataProviderDefinition + */ +export interface StacDataProviderDefinition { + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + apiUrl: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + collectionName: string; + /** + * + * @type {Array} + * @memberof StacDataProviderDefinition + */ + datasets: Array; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + description: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + id: string; + /** + * + * @type {string} + * @memberof StacDataProviderDefinition + */ + name: string; + /** + * + * @type {number} + * @memberof StacDataProviderDefinition + */ + priority?: number | null; + /** + * + * @type {StacProviderS3Config} + * @memberof StacDataProviderDefinition + */ + s3Config?: StacProviderS3Config | null; + /** + * + * @type {TimeDimension} + * @memberof StacDataProviderDefinition + */ + timeDimension: TimeDimension; + /** + * + * @type {StacDataProviderDefinitionTypeEnum} + * @memberof StacDataProviderDefinition + */ + type: StacDataProviderDefinitionTypeEnum; +} + + +/** + * @export + */ +export const StacDataProviderDefinitionTypeEnum = { + StacProviderDefinition: 'StacProviderDefinition' +} as const; +export type StacDataProviderDefinitionTypeEnum = typeof StacDataProviderDefinitionTypeEnum[keyof typeof StacDataProviderDefinitionTypeEnum]; + + +/** + * Check if a given object implements the StacDataProviderDefinition interface. + */ +export function instanceOfStacDataProviderDefinition(value: object): value is StacDataProviderDefinition { + if (!('apiUrl' in value) || value['apiUrl'] === undefined) return false; + if (!('collectionName' in value) || value['collectionName'] === undefined) return false; + if (!('datasets' in value) || value['datasets'] === undefined) return false; + if (!('description' in value) || value['description'] === undefined) return false; + if (!('id' in value) || value['id'] === undefined) return false; + if (!('name' in value) || value['name'] === undefined) return false; + if (!('timeDimension' in value) || value['timeDimension'] === undefined) return false; + if (!('type' in value) || value['type'] === undefined) return false; + return true; +} + +export function StacDataProviderDefinitionFromJSON(json: any): StacDataProviderDefinition { + return StacDataProviderDefinitionFromJSONTyped(json, false); +} + +export function StacDataProviderDefinitionFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacDataProviderDefinition { + if (json == null) { + return json; + } + return { + + 'apiUrl': json['apiUrl'], + 'collectionName': json['collectionName'], + 'datasets': ((json['datasets'] as Array).map(StacProviderDatasetFromJSON)), + 'description': json['description'], + 'id': json['id'], + 'name': json['name'], + 'priority': json['priority'] == null ? undefined : json['priority'], + 's3Config': json['s3Config'] == null ? undefined : StacProviderS3ConfigFromJSON(json['s3Config']), + 'timeDimension': TimeDimensionFromJSON(json['timeDimension']), + 'type': json['type'], + }; +} + +export function StacDataProviderDefinitionToJSON(json: any): StacDataProviderDefinition { + return StacDataProviderDefinitionToJSONTyped(json, false); +} + +export function StacDataProviderDefinitionToJSONTyped(value?: StacDataProviderDefinition | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'apiUrl': value['apiUrl'], + 'collectionName': value['collectionName'], + 'datasets': ((value['datasets'] as Array).map(StacProviderDatasetToJSON)), + 'description': value['description'], + 'id': value['id'], + 'name': value['name'], + 'priority': value['priority'], + 's3Config': StacProviderS3ConfigToJSON(value['s3Config']), + 'timeDimension': TimeDimensionToJSON(value['timeDimension']), + 'type': value['type'], + }; +} + diff --git a/api-clients/typescript/src/models/StacProviderDataset.ts b/api-clients/typescript/src/models/StacProviderDataset.ts new file mode 100644 index 0000000000..daac69bcf8 --- /dev/null +++ b/api-clients/typescript/src/models/StacProviderDataset.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { SpatialResolution } from './SpatialResolution'; +import { + SpatialResolutionFromJSON, + SpatialResolutionFromJSONTyped, + SpatialResolutionToJSON, + SpatialResolutionToJSONTyped, +} from './SpatialResolution'; +import type { StacProviderDatasetBand } from './StacProviderDatasetBand'; +import { + StacProviderDatasetBandFromJSON, + StacProviderDatasetBandFromJSONTyped, + StacProviderDatasetBandToJSON, + StacProviderDatasetBandToJSONTyped, +} from './StacProviderDatasetBand'; +import type { SpatialGridDescriptor } from './SpatialGridDescriptor'; +import { + SpatialGridDescriptorFromJSON, + SpatialGridDescriptorFromJSONTyped, + SpatialGridDescriptorToJSON, + SpatialGridDescriptorToJSONTyped, +} from './SpatialGridDescriptor'; +import type { RasterDataType } from './RasterDataType'; +import { + RasterDataTypeFromJSON, + RasterDataTypeFromJSONTyped, + RasterDataTypeToJSON, + RasterDataTypeToJSONTyped, +} from './RasterDataType'; + +/** + * + * @export + * @interface StacProviderDataset + */ +export interface StacProviderDataset { + /** + * + * @type {Array} + * @memberof StacProviderDataset + */ + bands: Array; + /** + * + * @type {RasterDataType} + * @memberof StacProviderDataset + */ + dataType: RasterDataType; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + description: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + name: string; + /** + * + * @type {string} + * @memberof StacProviderDataset + */ + projection: string; + /** + * + * @type {SpatialResolution} + * @memberof StacProviderDataset + */ + resolution: SpatialResolution; + /** + * + * @type {SpatialGridDescriptor} + * @memberof StacProviderDataset + */ + spatialGrid: SpatialGridDescriptor; +} + + + +/** + * Check if a given object implements the StacProviderDataset interface. + */ +export function instanceOfStacProviderDataset(value: object): value is StacProviderDataset { + if (!('bands' in value) || value['bands'] === undefined) return false; + if (!('dataType' in value) || value['dataType'] === undefined) return false; + if (!('description' in value) || value['description'] === undefined) return false; + if (!('name' in value) || value['name'] === undefined) return false; + if (!('projection' in value) || value['projection'] === undefined) return false; + if (!('resolution' in value) || value['resolution'] === undefined) return false; + if (!('spatialGrid' in value) || value['spatialGrid'] === undefined) return false; + return true; +} + +export function StacProviderDatasetFromJSON(json: any): StacProviderDataset { + return StacProviderDatasetFromJSONTyped(json, false); +} + +export function StacProviderDatasetFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDataset { + if (json == null) { + return json; + } + return { + + 'bands': ((json['bands'] as Array).map(StacProviderDatasetBandFromJSON)), + 'dataType': RasterDataTypeFromJSON(json['dataType']), + 'description': json['description'], + 'name': json['name'], + 'projection': json['projection'], + 'resolution': SpatialResolutionFromJSON(json['resolution']), + 'spatialGrid': SpatialGridDescriptorFromJSON(json['spatialGrid']), + }; +} + +export function StacProviderDatasetToJSON(json: any): StacProviderDataset { + return StacProviderDatasetToJSONTyped(json, false); +} + +export function StacProviderDatasetToJSONTyped(value?: StacProviderDataset | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'bands': ((value['bands'] as Array).map(StacProviderDatasetBandToJSON)), + 'dataType': RasterDataTypeToJSON(value['dataType']), + 'description': value['description'], + 'name': value['name'], + 'projection': value['projection'], + 'resolution': SpatialResolutionToJSON(value['resolution']), + 'spatialGrid': SpatialGridDescriptorToJSON(value['spatialGrid']), + }; +} + diff --git a/api-clients/typescript/src/models/StacProviderDatasetBand.ts b/api-clients/typescript/src/models/StacProviderDatasetBand.ts new file mode 100644 index 0000000000..4327ac5b56 --- /dev/null +++ b/api-clients/typescript/src/models/StacProviderDatasetBand.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface StacProviderDatasetBand + */ +export interface StacProviderDatasetBand { + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + assetTitle: string; + /** + * + * @type {string} + * @memberof StacProviderDatasetBand + */ + bandName?: string | null; +} + +/** + * Check if a given object implements the StacProviderDatasetBand interface. + */ +export function instanceOfStacProviderDatasetBand(value: object): value is StacProviderDatasetBand { + if (!('assetTitle' in value) || value['assetTitle'] === undefined) return false; + return true; +} + +export function StacProviderDatasetBandFromJSON(json: any): StacProviderDatasetBand { + return StacProviderDatasetBandFromJSONTyped(json, false); +} + +export function StacProviderDatasetBandFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderDatasetBand { + if (json == null) { + return json; + } + return { + + 'assetTitle': json['assetTitle'], + 'bandName': json['bandName'] == null ? undefined : json['bandName'], + }; +} + +export function StacProviderDatasetBandToJSON(json: any): StacProviderDatasetBand { + return StacProviderDatasetBandToJSONTyped(json, false); +} + +export function StacProviderDatasetBandToJSONTyped(value?: StacProviderDatasetBand | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'assetTitle': value['assetTitle'], + 'bandName': value['bandName'], + }; +} + diff --git a/api-clients/typescript/src/models/StacProviderS3Config.ts b/api-clients/typescript/src/models/StacProviderS3Config.ts new file mode 100644 index 0000000000..93db046cec --- /dev/null +++ b/api-clients/typescript/src/models/StacProviderS3Config.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Geo Engine API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * Contact: dev@geoengine.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface StacProviderS3Config + */ +export interface StacProviderS3Config { + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + accessKey?: string | null; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + endpoint: string; + /** + * + * @type {string} + * @memberof StacProviderS3Config + */ + secretKey?: string | null; +} + +/** + * Check if a given object implements the StacProviderS3Config interface. + */ +export function instanceOfStacProviderS3Config(value: object): value is StacProviderS3Config { + if (!('endpoint' in value) || value['endpoint'] === undefined) return false; + return true; +} + +export function StacProviderS3ConfigFromJSON(json: any): StacProviderS3Config { + return StacProviderS3ConfigFromJSONTyped(json, false); +} + +export function StacProviderS3ConfigFromJSONTyped(json: any, ignoreDiscriminator: boolean): StacProviderS3Config { + if (json == null) { + return json; + } + return { + + 'accessKey': json['accessKey'] == null ? undefined : json['accessKey'], + 'endpoint': json['endpoint'], + 'secretKey': json['secretKey'] == null ? undefined : json['secretKey'], + }; +} + +export function StacProviderS3ConfigToJSON(json: any): StacProviderS3Config { + return StacProviderS3ConfigToJSONTyped(json, false); +} + +export function StacProviderS3ConfigToJSONTyped(value?: StacProviderS3Config | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'accessKey': value['accessKey'], + 'endpoint': value['endpoint'], + 'secretKey': value['secretKey'], + }; +} + diff --git a/api-clients/typescript/src/models/TypedDataProviderDefinition.ts b/api-clients/typescript/src/models/TypedDataProviderDefinition.ts index 424e266536..4d4ca070c4 100644 --- a/api-clients/typescript/src/models/TypedDataProviderDefinition.ts +++ b/api-clients/typescript/src/models/TypedDataProviderDefinition.ts @@ -88,6 +88,13 @@ import { SentinelS2L2ACogsProviderDefinitionFromJSONTyped, SentinelS2L2ACogsProviderDefinitionToJSON, } from './SentinelS2L2ACogsProviderDefinition'; +import type { StacDataProviderDefinition } from './StacDataProviderDefinition'; +import { + instanceOfStacDataProviderDefinition, + StacDataProviderDefinitionFromJSON, + StacDataProviderDefinitionFromJSONTyped, + StacDataProviderDefinitionToJSON, +} from './StacDataProviderDefinition'; import type { WildliveDataConnectorDefinition } from './WildliveDataConnectorDefinition'; import { instanceOfWildliveDataConnectorDefinition, @@ -101,7 +108,7 @@ import { * * @export */ -export type TypedDataProviderDefinition = { type: 'Aruna' } & ArunaDataProviderDefinition | { type: 'CopernicusDataspace' } & CopernicusDataspaceDataProviderDefinition | { type: 'DatasetLayerListing' } & DatasetLayerListingProviderDefinition | { type: 'EbvPortal' } & EbvPortalDataProviderDefinition | { type: 'Edr' } & EdrDataProviderDefinition | { type: 'Gbif' } & GbifDataProviderDefinition | { type: 'GfbioAbcd' } & GfbioAbcdDataProviderDefinition | { type: 'GfbioCollections' } & GfbioCollectionsDataProviderDefinition | { type: 'NetCdfCf' } & NetCdfCfDataProviderDefinition | { type: 'Pangaea' } & PangaeaDataProviderDefinition | { type: 'SentinelS2L2ACogs' } & SentinelS2L2ACogsProviderDefinition | { type: 'WildLIVE!' } & WildliveDataConnectorDefinition; +export type TypedDataProviderDefinition = { type: 'Aruna' } & ArunaDataProviderDefinition | { type: 'CopernicusDataspace' } & CopernicusDataspaceDataProviderDefinition | { type: 'DatasetLayerListing' } & DatasetLayerListingProviderDefinition | { type: 'EbvPortal' } & EbvPortalDataProviderDefinition | { type: 'Edr' } & EdrDataProviderDefinition | { type: 'Gbif' } & GbifDataProviderDefinition | { type: 'GfbioAbcd' } & GfbioAbcdDataProviderDefinition | { type: 'GfbioCollections' } & GfbioCollectionsDataProviderDefinition | { type: 'NetCdfCf' } & NetCdfCfDataProviderDefinition | { type: 'Pangaea' } & PangaeaDataProviderDefinition | { type: 'SentinelS2L2ACogs' } & SentinelS2L2ACogsProviderDefinition | { type: 'StacProviderDefinition' } & StacDataProviderDefinition | { type: 'WildLIVE!' } & WildliveDataConnectorDefinition; export function TypedDataProviderDefinitionFromJSON(json: any): TypedDataProviderDefinition { return TypedDataProviderDefinitionFromJSONTyped(json, false); @@ -134,6 +141,8 @@ export function TypedDataProviderDefinitionFromJSONTyped(json: any, ignoreDiscri return Object.assign({}, PangaeaDataProviderDefinitionFromJSONTyped(json, true), { type: 'Pangaea' } as const); case 'SentinelS2L2ACogs': return Object.assign({}, SentinelS2L2ACogsProviderDefinitionFromJSONTyped(json, true), { type: 'SentinelS2L2ACogs' } as const); + case 'StacProviderDefinition': + return Object.assign({}, StacDataProviderDefinitionFromJSONTyped(json, true), { type: 'StacProviderDefinition' } as const); case 'WildLIVE!': return Object.assign({}, WildliveDataConnectorDefinitionFromJSONTyped(json, true), { type: 'WildLIVE!' } as const); default: @@ -172,6 +181,8 @@ export function TypedDataProviderDefinitionToJSONTyped(value?: TypedDataProvider return Object.assign({}, PangaeaDataProviderDefinitionToJSON(value), { type: 'Pangaea' } as const); case 'SentinelS2L2ACogs': return Object.assign({}, SentinelS2L2ACogsProviderDefinitionToJSON(value), { type: 'SentinelS2L2ACogs' } as const); + case 'StacProviderDefinition': + return Object.assign({}, StacDataProviderDefinitionToJSON(value), { type: 'StacProviderDefinition' } as const); case 'WildLIVE!': return Object.assign({}, WildliveDataConnectorDefinitionToJSON(value), { type: 'WildLIVE!' } as const); default: diff --git a/api-clients/typescript/src/models/index.ts b/api-clients/typescript/src/models/index.ts index b89e84a15f..ce2f2e856f 100644 --- a/api-clients/typescript/src/models/index.ts +++ b/api-clients/typescript/src/models/index.ts @@ -235,7 +235,12 @@ export * from './SpatialGridDescriptor'; export * from './SpatialGridDescriptorState'; export * from './SpatialPartition2D'; export * from './SpatialReferenceSpecification'; +export * from './SpatialResolution'; export * from './StacApiRetries'; +export * from './StacDataProviderDefinition'; +export * from './StacProviderDataset'; +export * from './StacProviderDatasetBand'; +export * from './StacProviderS3Config'; export * from './StacQueryBuffer'; export * from './StaticColor'; export * from './StaticNumber'; diff --git a/geoengine/Cargo.lock b/geoengine/Cargo.lock index dc9e0f585d..9ea201bd76 100644 --- a/geoengine/Cargo.lock +++ b/geoengine/Cargo.lock @@ -2610,6 +2610,21 @@ dependencies = [ "serde", ] +[[package]] +name = "geoengine-api-client" +version = "0.9.2" +dependencies = [ + "reqwest 0.13.3", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "tokio", + "tokio-util", + "url", + "uuid", +] + [[package]] name = "geoengine-datatypes" version = "0.9.2" @@ -2771,6 +2786,7 @@ dependencies = [ "futures-util", "gdal", "geo 0.32.0", + "geoengine-api-client", "geoengine-datatypes", "geoengine-expression", "geoengine-macros", @@ -2799,7 +2815,7 @@ dependencies = [ "rand 0.10.0", "rayon", "regex", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_urlencoded", @@ -4543,7 +4559,7 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.5", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", @@ -4708,7 +4724,7 @@ dependencies = [ "bytes", "http 1.4.0", "opentelemetry", - "reqwest", + "reqwest 0.12.28", ] [[package]] @@ -4723,7 +4739,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost 0.14.3", - "reqwest", + "reqwest 0.12.28", "thiserror 2.0.18", "tokio", "tonic 0.14.5", @@ -5946,11 +5962,51 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -6457,6 +6513,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -8098,6 +8165,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" diff --git a/geoengine/operators/src/adapters/stream_statistics_adapter.rs b/geoengine/operators/src/adapters/stream_statistics_adapter.rs index d06cfba62a..1ca1b9abd8 100644 --- a/geoengine/operators/src/adapters/stream_statistics_adapter.rs +++ b/geoengine/operators/src/adapters/stream_statistics_adapter.rs @@ -76,7 +76,7 @@ where match v { Some(_) => { *this.element_count += 1; - tracing::debug!( + tracing::trace!( event = %"poll_next", poll_next_count = *this.poll_next_count, element_count = *this.element_count, @@ -90,7 +90,7 @@ where ); } None => { - tracing::debug!( + tracing::trace!( event = %"poll_next", poll_next_count = *this.poll_next_count, element_count = *this.element_count, diff --git a/geoengine/operators/src/source/multi_band_gdal_source/mod.rs b/geoengine/operators/src/source/multi_band_gdal_source/mod.rs index 8d287aee65..e76a947e8c 100644 --- a/geoengine/operators/src/source/multi_band_gdal_source/mod.rs +++ b/geoengine/operators/src/source/multi_band_gdal_source/mod.rs @@ -50,6 +50,7 @@ use snafu::{ResultExt, ensure}; use std::ffi::CString; use std::marker::PhantomData; use std::path::Path; +use std::time::Instant; use tracing::{debug, trace}; mod error; @@ -298,9 +299,17 @@ impl GdalRasterLoader { .as_ref() .map(|o| o.iter().map(String::as_str).collect::>()); + let config_options = if let Some(config_options) = &dataset_params.gdal_config_options { + // ensure that GDAL only uses a single thread because otherweise the thread local configs may not be used by all gdal threads + let mut options = config_options.clone(); + options.push(("GDAL_NUM_THREADS".to_string(), "1".to_string())); + Some(options) + } else { + None + }; + // reverts the thread local configs on drop - let _thread_local_configs = dataset_params - .gdal_config_options + let _thread_local_configs = config_options .as_ref() .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options)); @@ -455,6 +464,7 @@ where } }; + let loading_info_start = Instant::now(); let loading_info = self .meta_data .loading_info(raster_query_rectangle_to_loading_info_query_rectangle( @@ -463,6 +473,8 @@ where true, )) .await?; + let loading_info_ms = loading_info_start.elapsed().as_millis(); + debug!(loading_info_ms, "loading_info timing"); let time_steps = loading_info.time_steps().to_vec(); let bands = query.attributes().clone().as_vec(); @@ -882,12 +894,15 @@ where { let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y()); + let start = std::time::Instant::now(); let buffer = rasterband.read_as::( read_window.gdal_window_start(), // pixelspace origin read_window.gdal_window_size(), // pixelspace size gdal_out_shape, // requested raster size None, // sampling mode )?; + debug!("read raster band in {:?} s", start.elapsed().as_secs_f64()); + let (_, buffer_data) = buffer.into_shape_and_vec(); let data_grid = Grid::new(out_shape.clone(), buffer_data)?; diff --git a/geoengine/services/Cargo.toml b/geoengine/services/Cargo.toml index 2f6af18f64..df079c6586 100644 --- a/geoengine/services/Cargo.toml +++ b/geoengine/services/Cargo.toml @@ -56,6 +56,7 @@ geoengine-datatypes = { path = "../datatypes" } geoengine-expression = { path = "../expression" } geoengine-macros = { path = "../macros" } geoengine-operators = { path = "../operators" } +geoengine-api-client = { path = "../../api-clients/rust" } geojson = { workspace = true } itertools = { workspace = true } tracing = { workspace = true } diff --git a/geoengine/services/examples/benchmark-stac-harvest-vs-provider.rs b/geoengine/services/examples/benchmark-stac-harvest-vs-provider.rs new file mode 100644 index 0000000000..d3641e8a36 --- /dev/null +++ b/geoengine/services/examples/benchmark-stac-harvest-vs-provider.rs @@ -0,0 +1,1160 @@ +#![allow(clippy::print_stdout, clippy::print_stderr)] + +use anyhow::{Context, Result, anyhow, bail}; +use geoengine_api_client::apis; +use geoengine_api_client::apis::configuration::Configuration; +use geoengine_api_client::models; +use geoengine_datatypes::operations::image::{Colorizer, RasterColorizer, RgbaColor}; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::env; +use std::fs::{self, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant}; +use tokio::time::sleep; +use uuid::Uuid; + +const BBOX: &str = "399960.0000000000000000,5590200.0000000000000000,509760.0000000000000000,5700000.0000000000000000"; +const CRS: &str = "EPSG:32632"; +const TIME: &str = "2026-01-03T00:00:00.000Z"; +const IMAGE_WIDTH: i32 = 1830; +const IMAGE_HEIGHT: i32 = 1830; +const IMPORT_BBOX: &str = "7.560547850100084 50.45526533913283 9.140457957690465 51.45116832125808"; +const DEFAULT_SERVER_RUST_LOG: &str = "debug"; +#[derive(Debug, Clone)] +struct Config { + runs: u32, + host: String, + port: u16, + stac_url: String, + stac_s3_endpoint: String, + stac_s3_access_key: String, + stac_s3_secret_key: String, + out_dir: PathBuf, + import_limit: u32, + import_bbox: String, + import_time_start: String, + import_time_end: String, +} + +#[derive(Debug, Clone)] +struct BenchRecord { + run: u32, + scenario: &'static str, + import_ms: u128, + workflow_registration_ms: u128, + loading_info_ms: u128, + wms_ms: u128, + http_status: u16, +} + +struct ServerHandle { + child: Child, +} + +impl ServerHandle { + async fn start(root_dir: &Path, log_file: &Path, base_url: &str) -> Result { + let stdout = File::create(log_file) + .with_context(|| format!("creating log file {}", log_file.display()))?; + let stderr = stdout + .try_clone() + .context("cloning server log file handle")?; + let rust_log = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_SERVER_RUST_LOG.to_owned()); + + let child = Command::new("cargo") + .arg("run") + .arg("--release") + .arg("--bin") + .arg("geoengine-server") + .env("RUST_LOG", rust_log) + .current_dir(root_dir) + .stdout(Stdio::from(stdout)) + .stderr(Stdio::from(stderr)) + .spawn() + .context("starting geoengine-server")?; + + let mut handle = Self { child }; + handle + .wait_until_ready(base_url, log_file) + .await + .context("waiting for geoengine-server readiness")?; + + Ok(handle) + } + + async fn wait_until_ready(&mut self, base_url: &str, log_file: &Path) -> Result<()> { + let client = reqwest::Client::new(); + let health_url = format!("{base_url}/swagger-ui/"); + + loop { + if let Some(exit) = self + .child + .try_wait() + .context("checking geoengine-server process status")? + { + bail!( + "geoengine-server exited early with status {exit}. See {}", + log_file.display() + ); + } + + match client.get(&health_url).send().await { + Ok(response) if response.status().is_success() => return Ok(()), + _ => sleep(Duration::from_millis(500)).await, + } + } + } + + fn stop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +impl Drop for ServerHandle { + fn drop(&mut self) { + self.stop(); + } +} + +fn ensure_release_cli_binary(root_dir: &Path) -> Result<()> { + let cli_binary = release_binary(root_dir, "geoengine-cli"); + + if cli_binary.exists() { + return Ok(()); + } + + let status = Command::new("cargo") + .arg("run") + .arg("--release") + .arg("--bin") + .arg("geoengine-cli") + .arg("--") + .arg("--help") + .current_dir(root_dir) + .status() + .context("building geoengine-cli release binary")?; + + if !status.success() { + bail!("building geoengine-cli release binary failed with status {status}"); + } + + Ok(()) +} + +fn release_binary(root_dir: &Path, name: &str) -> PathBuf { + root_dir.join("target").join("release").join(name) +} + +#[tokio::main] +async fn main() -> Result<()> { + let config = Config::from_env()?; + let root_dir = workspace_root(); + + let log_dir = config.out_dir.join("logs"); + let png_dir = config.out_dir.join("png"); + fs::create_dir_all(&log_dir).with_context(|| format!("creating {}", log_dir.display()))?; + fs::create_dir_all(&png_dir).with_context(|| format!("creating {}", png_dir.display()))?; + + let csv_file = config.out_dir.join("results.csv"); + let mut csv_writer = + File::create(&csv_file).with_context(|| format!("creating {}", csv_file.display()))?; + writeln!( + csv_writer, + "run,scenario,import_ms,workflow_registration_ms,wms_ms,loading_info_ms,http_status" + )?; + + let gdal_access_file = config.out_dir.join("gdal_file_accesses.csv"); + let mut gdal_access_writer = File::create(&gdal_access_file) + .with_context(|| format!("creating {}", gdal_access_file.display()))?; + writeln!(gdal_access_writer, "run,scenario,file_path,access_count")?; + + println!("Running benchmark with RUNS={}", config.runs); + println!("Output directory: {}", config.out_dir.display()); + + let mut records = Vec::new(); + + for run in 1..=config.runs { + println!("\n=== Run {run}/{}: harvesting scenario ===", config.runs); + let harvest = run_harvest_benchmark(run, &config, &root_dir, &log_dir, &png_dir).await?; + write_record(&mut csv_writer, &harvest)?; + write_gdal_access_records( + &mut gdal_access_writer, + run, + "harvest", + &log_dir.join(format!("server_{run}_harvest.log")), + )?; + records.push(harvest); + + println!( + "\n=== Run {run}/{}: ad-hoc provider scenario ===", + config.runs + ); + let provider = run_provider_benchmark(run, &config, &root_dir, &log_dir, &png_dir).await?; + write_record(&mut csv_writer, &provider)?; + write_gdal_access_records( + &mut gdal_access_writer, + run, + "provider", + &log_dir.join(format!("server_{run}_provider.log")), + )?; + records.push(provider); + + println!("\n=== Run {run}/{}: NDVI harvest scenario ===", config.runs); + let ndvi_harvest = + run_ndvi_harvest_benchmark(run, &config, &root_dir, &log_dir, &png_dir).await?; + write_record(&mut csv_writer, &ndvi_harvest)?; + write_gdal_access_records( + &mut gdal_access_writer, + run, + "ndvi-harvest", + &log_dir.join(format!("server_{run}_ndvi_harvest.log")), + )?; + records.push(ndvi_harvest); + + println!( + "\n=== Run {run}/{}: NDVI provider scenario ===", + config.runs + ); + let ndvi_provider = + run_ndvi_provider_benchmark(run, &config, &root_dir, &log_dir, &png_dir).await?; + write_record(&mut csv_writer, &ndvi_provider)?; + write_gdal_access_records( + &mut gdal_access_writer, + run, + "ndvi-provider", + &log_dir.join(format!("server_{run}_ndvi_provider.log")), + )?; + records.push(ndvi_provider); + } + + csv_writer.flush()?; + gdal_access_writer.flush()?; + + println!("\nBenchmark results: {}", csv_file.display()); + println!("GDAL file access report: {}", gdal_access_file.display()); + + let comparison_file = config.out_dir.join("gdal_file_comparison.csv"); + generate_gdal_file_comparison(&gdal_access_file, &comparison_file)?; + println!( + "GDAL file comparison (pivot): {}", + comparison_file.display() + ); + + print_summary(&records); + + Ok(()) +} + +async fn run_harvest_benchmark( + run: u32, + config: &Config, + root_dir: &Path, + log_dir: &Path, + png_dir: &Path, +) -> Result { + let base_url = config.base_url(); + let log_file = log_dir.join(format!("server_{run}_harvest.log")); + let _server = ServerHandle::start(root_dir, &log_file, &base_url).await?; + + let mut api = api_configuration(&base_url); + + let token = anonymous_token(&api).await?; + api.bearer_access_token = Some(token.to_string()); + + // Keep one-time CLI release compilation out of measured import time. + ensure_release_cli_binary(root_dir)?; + + let import_start = Instant::now(); + run_stac_import(config, root_dir)?; + let import_ms = import_start.elapsed().as_millis(); + + let workflow_registration_start = Instant::now(); + let workflow_id = register_harvest_workflow(&api).await?; + let workflow_registration_ms = workflow_registration_start.elapsed().as_millis(); + + let style = build_wms_style(8, 1051.0, 16015.0)?; + + let wms_start = Instant::now(); + let response = apis::ogcwms_api::wms_handler( + &api, + &workflow_id.to_string(), + models::WmsRequest::GetMap, + Some(BBOX), + None, + Some(CRS), + None, + Some("application/json"), + Some("image/png"), + Some(IMAGE_HEIGHT), + None, + None, + Some(&workflow_id.to_string()), + None, + Some(models::WmsService::Wms), + None, + None, + Some(&style), + Some(TIME), + Some(true), + Some("1.3.0"), + Some(IMAGE_WIDTH), + ) + .await + .map_err(map_api_error("harvest WMS request"))?; + + let status = response.status(); + let png_bytes = response + .bytes() + .await + .context("reading harvest WMS response body")?; + require_http_200(status, "harvest WMS")?; + fs::write(png_dir.join(format!("harvest_run_{run}.png")), png_bytes) + .context("writing harvest PNG")?; + let wms_ms = wms_start.elapsed().as_millis(); + let loading_info_ms = wait_for_loading_info_ms(&log_file).await?; + + Ok(BenchRecord { + run, + scenario: "harvest", + import_ms, + workflow_registration_ms, + loading_info_ms, + wms_ms, + http_status: status.as_u16(), + }) +} + +async fn run_provider_benchmark( + run: u32, + config: &Config, + root_dir: &Path, + log_dir: &Path, + png_dir: &Path, +) -> Result { + let base_url = config.base_url(); + let log_file = log_dir.join(format!("server_{run}_provider.log")); + let _server = ServerHandle::start(root_dir, &log_file, &base_url).await?; + + let mut api = api_configuration(&base_url); + + let token = anonymous_token(&api).await?; + api.bearer_access_token = Some(token.to_string()); + + let workflow_registration_start = Instant::now(); + + let provider_definition = build_provider_definition(config, root_dir)?; + let provider_id = register_stac_provider_fallback(&api, provider_definition).await?; + + let workflow = + apis::layers_api::layer_to_workflow_id_handler(&api, &provider_id.to_string(), "dataset/2") + .await + .map_err(map_api_error("provider layer_to_workflow_id"))?; + let workflow_id = workflow.id; + + trigger_workflow_metadata_fallback(&api, workflow_id).await?; + + let workflow_registration_ms = workflow_registration_start.elapsed().as_millis(); + + let style = build_wms_style(9, 1051.0, 16015.0)?; + + let wms_start = Instant::now(); + let response = apis::ogcwms_api::wms_handler( + &api, + &workflow_id.to_string(), + models::WmsRequest::GetMap, + Some(BBOX), + None, + Some(CRS), + None, + Some("application/json"), + Some("image/png"), + Some(IMAGE_HEIGHT), + None, + None, + Some(&workflow_id.to_string()), + None, + Some(models::WmsService::Wms), + None, + None, + Some(&style), + Some(TIME), + Some(true), + Some("1.3.0"), + Some(IMAGE_WIDTH), + ) + .await + .map_err(map_api_error("provider WMS request"))?; + + let status = response.status(); + let png_bytes = response + .bytes() + .await + .context("reading provider WMS response body")?; + require_http_200(status, "provider WMS")?; + fs::write(png_dir.join(format!("provider_run_{run}.png")), png_bytes) + .context("writing provider PNG")?; + let wms_ms = wms_start.elapsed().as_millis(); + let loading_info_ms = wait_for_loading_info_ms(&log_file).await?; + + Ok(BenchRecord { + run, + scenario: "provider", + import_ms: 0, + workflow_registration_ms, + loading_info_ms, + wms_ms, + http_status: status.as_u16(), + }) +} + +async fn wait_for_loading_info_ms(log_file: &Path) -> Result { + for _ in 0..20 { + if let Some(ms) = extract_latest_loading_info_ms(log_file)? { + return Ok(ms); + } + + sleep(Duration::from_millis(100)).await; + } + + eprintln!( + "warning: could not find loading_info_ms marker in {}; using 0", + log_file.display() + ); + Ok(0) +} + +fn extract_latest_loading_info_ms(log_file: &Path) -> Result> { + let log_text = + fs::read_to_string(log_file).with_context(|| format!("reading {}", log_file.display()))?; + + for line in log_text.lines().rev() { + let clean_line = strip_ansi_escape_sequences(line); + + let rest = if let Some((_, rest)) = clean_line.split_once("loading_info_ms=") { + rest + } else if let Some((_, rest)) = clean_line.split_once("loading_info_ms:") { + rest + } else if let Some((_, rest)) = clean_line.split_once("loading_info_ms") { + rest + } else { + continue; + }; + + let digits: String = rest + .chars() + .skip_while(|c| !c.is_ascii_digit()) + .take_while(char::is_ascii_digit) + .collect(); + + if digits.is_empty() { + continue; + } + + let ms = digits.parse::().with_context(|| { + format!( + "parsing loading_info_ms marker '{digits}' in {}", + log_file.display() + ) + })?; + + return Ok(Some(ms)); + } + + Ok(None) +} + +fn strip_ansi_escape_sequences(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let bytes = input.as_bytes(); + let mut i = 0; + + while i < bytes.len() { + if bytes[i] == 0x1B && i + 1 < bytes.len() && bytes[i + 1] == b'[' { + i += 2; + while i < bytes.len() { + let b = bytes[i]; + if (0x40..=0x7E).contains(&b) { + i += 1; + break; + } + i += 1; + } + continue; + } + + out.push(bytes[i] as char); + i += 1; + } + + out +} + +async fn trigger_workflow_metadata_fallback(api: &Configuration, workflow_id: Uuid) -> Result<()> { + let response = api + .client + .get(format!( + "{}/workflow/{}/metadata", + api.base_path, workflow_id + )) + .bearer_auth( + api.bearer_access_token + .as_ref() + .ok_or_else(|| anyhow!("missing bearer token in API configuration"))?, + ) + .send() + .await + .context("querying workflow metadata")?; + + let status = response.status(); + let body = response + .text() + .await + .context("reading workflow metadata response")?; + + if !status.is_success() { + bail!("provider metadata failed: status={status} body={body}"); + } + + let _: Value = serde_json::from_str(&body).context("decoding workflow metadata JSON")?; + + Ok(()) +} + +fn api_configuration(base_url: &str) -> Configuration { + Configuration { + base_path: base_url.to_string(), + ..Configuration::default() + } +} + +async fn anonymous_token(api: &Configuration) -> Result { + let session = apis::session_api::anonymous_handler(api) + .await + .map_err(map_api_error("anonymous session"))?; + Ok(session.id) +} + +async fn register_harvest_workflow(api: &Configuration) -> Result { + let params = models::GdalSourceParameters::new("sentinel-2-l2a_EPSG32632_U16_60".to_owned()); + let source = models::MultiBandGdalSource::new( + params, + models::multi_band_gdal_source::Type::MultiBandGdalSource, + ); + let raster_operator = models::RasterOperator::MultiBandGdalSource(Box::new(source)); + let typed_raster = models::TypedRasterOperator::new( + raster_operator, + models::typed_raster_operator::Type::Raster, + ); + let typed_operator = models::TypedOperator::TypedRasterOperator(Box::new(typed_raster)); + let workflow = models::Workflow::TypedOperator(Box::new(typed_operator)); + + let id = apis::workflows_api::register_workflow_handler(api, workflow) + .await + .map_err(map_api_error("register harvest workflow"))?; + + Ok(id.id) +} + +fn build_wms_style(band: u32, min: f64, max: f64) -> Result { + let colorizer = Colorizer::linear_gradient( + vec![ + (min, RgbaColor::new(0, 0, 0, 255)).try_into()?, + (max, RgbaColor::new(255, 255, 255, 255)).try_into()?, + ], + RgbaColor::transparent(), + RgbaColor::new(246, 250, 254, 255), + RgbaColor::new(247, 251, 255, 255), + )?; + + let raster_colorizer = RasterColorizer::SingleBand { + band, + band_colorizer: colorizer, + }; + + Ok(format!( + "custom:{}", + serde_json::to_string(&raster_colorizer).context("serializing raster colorizer")? + )) +} + +fn build_ndvi_workflow_operator( + root_dir: &Path, + data_scl_20: Value, + data_nir_red_10: Value, +) -> Result { + let blueprint_path = root_dir.join("test_data/api_calls/nsiscloud/workflow.json"); + let blueprint_text = fs::read_to_string(&blueprint_path) + .with_context(|| format!("reading {}", blueprint_path.display()))?; + let mut workflow: Value = + serde_json::from_str(&blueprint_text).context("parsing NDVI workflow blueprint JSON")?; + let has_operator_wrapper = workflow.get("operator").is_some(); + + let op = if has_operator_wrapper { + workflow + .get_mut("operator") + .ok_or_else(|| anyhow!("NDVI workflow blueprint is missing 'operator' field"))? + } else { + &mut workflow + }; + + op["sources"]["raster"]["sources"]["raster"]["sources"]["rasters"][0]["sources"]["raster"]["sources"] + ["raster"]["sources"]["raster"]["params"]["data"] = data_scl_20; + op["sources"]["raster"]["sources"]["raster"]["sources"]["rasters"][1]["sources"]["raster"]["params"] + ["data"] = data_nir_red_10; + + if has_operator_wrapper { + Ok(workflow) + } else { + Ok(serde_json::json!({"type": "Raster", "operator": workflow})) + } +} + +async fn register_workflow_raw(api: &Configuration, typed_operator: Value) -> Result { + let response = api + .client + .post(format!("{}/workflow", api.base_path)) + .bearer_auth( + api.bearer_access_token + .as_ref() + .ok_or_else(|| anyhow!("missing bearer token in API configuration"))?, + ) + .json(&typed_operator) + .send() + .await + .context("registering workflow")?; + + let status = response.status(); + let body = response + .text() + .await + .context("reading workflow registration response")?; + + if !status.is_success() { + bail!("workflow registration failed: status={status} body={body}"); + } + + let id_response: models::IdResponse = + serde_json::from_str(&body).context("decoding workflow registration response")?; + + Ok(id_response.id) +} + +async fn run_ndvi_harvest_benchmark( + run: u32, + config: &Config, + root_dir: &Path, + log_dir: &Path, + png_dir: &Path, +) -> Result { + let base_url = config.base_url(); + let log_file = log_dir.join(format!("server_{run}_ndvi_harvest.log")); + let _server = ServerHandle::start(root_dir, &log_file, &base_url).await?; + + let mut api = api_configuration(&base_url); + let token = anonymous_token(&api).await?; + api.bearer_access_token = Some(token.to_string()); + + ensure_release_cli_binary(root_dir)?; + + let import_start = Instant::now(); + run_stac_import(config, root_dir)?; + let import_ms = import_start.elapsed().as_millis(); + + let workflow_registration_start = Instant::now(); + let data_scl_20 = Value::String("sentinel-2-l2a_EPSG32632_U8_20".to_owned()); + let data_nir_red_10 = Value::String("sentinel-2-l2a_EPSG32632_U16_10".to_owned()); + let typed_operator = build_ndvi_workflow_operator(root_dir, data_scl_20, data_nir_red_10)?; + let workflow_id = register_workflow_raw(&api, typed_operator).await?; + trigger_workflow_metadata_fallback(&api, workflow_id).await?; + let workflow_registration_ms = workflow_registration_start.elapsed().as_millis(); + + let style = build_wms_style(0, -0.1, 0.8)?; + + let wms_start = Instant::now(); + let response = apis::ogcwms_api::wms_handler( + &api, + &workflow_id.to_string(), + models::WmsRequest::GetMap, + Some(BBOX), + None, + Some(CRS), + None, + Some("application/json"), + Some("image/png"), + Some(IMAGE_HEIGHT), + None, + None, + Some(&workflow_id.to_string()), + None, + Some(models::WmsService::Wms), + None, + None, + Some(&style), + Some(TIME), + Some(true), + Some("1.3.0"), + Some(IMAGE_WIDTH), + ) + .await + .map_err(map_api_error("ndvi harvest WMS request"))?; + + let status = response.status(); + let png_bytes = response + .bytes() + .await + .context("reading ndvi harvest WMS response body")?; + require_http_200(status, "ndvi harvest WMS")?; + fs::write( + png_dir.join(format!("ndvi_harvest_run_{run}.png")), + png_bytes, + ) + .context("writing ndvi harvest PNG")?; + let wms_ms = wms_start.elapsed().as_millis(); + let loading_info_ms = wait_for_loading_info_ms(&log_file).await?; + + Ok(BenchRecord { + run, + scenario: "ndvi-harvest", + import_ms, + workflow_registration_ms, + loading_info_ms, + wms_ms, + http_status: status.as_u16(), + }) +} + +async fn run_ndvi_provider_benchmark( + run: u32, + config: &Config, + root_dir: &Path, + log_dir: &Path, + png_dir: &Path, +) -> Result { + let base_url = config.base_url(); + let log_file = log_dir.join(format!("server_{run}_ndvi_provider.log")); + let _server = ServerHandle::start(root_dir, &log_file, &base_url).await?; + + let mut api = api_configuration(&base_url); + let token = anonymous_token(&api).await?; + api.bearer_access_token = Some(token.to_string()); + + let workflow_registration_start = Instant::now(); + + let provider_definition = build_provider_definition(config, root_dir)?; + let scl_dataset_idx = provider_dataset_index(&provider_definition, "U8", 20.0)?; + let nir_red_dataset_idx = provider_dataset_index(&provider_definition, "U16", 10.0)?; + + let provider_id = register_stac_provider_fallback(&api, provider_definition).await?; + + let data_scl_20 = Value::String(format!("_:{provider_id}:`dataset/{scl_dataset_idx}`")); + let data_nir_red_10 = Value::String(format!("_:{provider_id}:`dataset/{nir_red_dataset_idx}`")); + let typed_operator = build_ndvi_workflow_operator(root_dir, data_scl_20, data_nir_red_10)?; + let workflow_id = register_workflow_raw(&api, typed_operator).await?; + trigger_workflow_metadata_fallback(&api, workflow_id).await?; + + let workflow_registration_ms = workflow_registration_start.elapsed().as_millis(); + + let style = build_wms_style(0, -0.1, 0.8)?; + + let wms_start = Instant::now(); + let response = apis::ogcwms_api::wms_handler( + &api, + &workflow_id.to_string(), + models::WmsRequest::GetMap, + Some(BBOX), + None, + Some(CRS), + None, + Some("application/json"), + Some("image/png"), + Some(IMAGE_HEIGHT), + None, + None, + Some(&workflow_id.to_string()), + None, + Some(models::WmsService::Wms), + None, + None, + Some(&style), + Some(TIME), + Some(true), + Some("1.3.0"), + Some(IMAGE_WIDTH), + ) + .await + .map_err(map_api_error("ndvi provider WMS request"))?; + + let status = response.status(); + let png_bytes = response + .bytes() + .await + .context("reading ndvi provider WMS response body")?; + require_http_200(status, "ndvi provider WMS")?; + fs::write( + png_dir.join(format!("ndvi_provider_run_{run}.png")), + png_bytes, + ) + .context("writing ndvi provider PNG")?; + let wms_ms = wms_start.elapsed().as_millis(); + let loading_info_ms = wait_for_loading_info_ms(&log_file).await?; + + Ok(BenchRecord { + run, + scenario: "ndvi-provider", + import_ms: 0, + workflow_registration_ms, + loading_info_ms, + wms_ms, + http_status: status.as_u16(), + }) +} + +fn run_stac_import(config: &Config, root_dir: &Path) -> Result<()> { + let cli_binary = release_binary(root_dir, "geoengine-cli"); + + let status = Command::new(&cli_binary) + .arg("stac-import") + .arg("--limit") + .arg(config.import_limit.to_string()) + .arg("--bbox") + .arg(&config.import_bbox) + .arg("--time-start") + .arg(&config.import_time_start) + .arg("--time-end") + .arg(&config.import_time_end) + .arg("--verbose") + .arg("--stac-url") + .arg(&config.stac_url) + .arg("--s3-endpoint") + .arg(&config.stac_s3_endpoint) + .arg("--s3-access-key") + .arg(&config.stac_s3_access_key) + .arg("--s3-secret-key") + .arg(&config.stac_s3_secret_key) + .current_dir(root_dir) + .status() + .with_context(|| format!("running {} stac-import", cli_binary.display()))?; + + if !status.success() { + bail!("stac-import failed with status {status}"); + } + + Ok(()) +} + +fn build_provider_definition(config: &Config, root_dir: &Path) -> Result { + let provider_path = root_dir.join("test_data/provider_defs_api/stac_sentinel2.json"); + let provider_text = fs::read_to_string(&provider_path) + .with_context(|| format!("reading {}", provider_path.display()))?; + let mut provider_definition: Value = + serde_json::from_str(&provider_text).context("parsing provider definition JSON")?; + + provider_definition["id"] = Value::String(Uuid::new_v4().to_string()); + provider_definition["s3Config"]["endpoint"] = Value::String(config.stac_s3_endpoint.clone()); + provider_definition["s3Config"]["accessKey"] = Value::String(config.stac_s3_access_key.clone()); + provider_definition["s3Config"]["secretKey"] = Value::String(config.stac_s3_secret_key.clone()); + + Ok(provider_definition) +} + +fn provider_dataset_index( + provider_definition: &Value, + data_type: &str, + resolution: f64, +) -> Result { + let datasets = provider_definition["datasets"] + .as_array() + .ok_or_else(|| anyhow!("provider definition datasets must be an array"))?; + + datasets + .iter() + .position(|dataset| { + dataset["dataType"].as_str() == Some(data_type) + && dataset["resolution"].as_f64() == Some(resolution) + }) + .ok_or_else(|| { + anyhow!("could not find dataset index for dataType={data_type} resolution={resolution}") + }) +} + +async fn register_stac_provider_fallback( + api: &Configuration, + provider_definition: Value, +) -> Result { + let response = api + .client + .post(format!("{}/layerDb/providers", api.base_path)) + .bearer_auth( + api.bearer_access_token + .as_ref() + .ok_or_else(|| anyhow!("missing bearer token in API configuration"))?, + ) + .json(&provider_definition) + .send() + .await + .context("registering STAC provider")?; + + let status = response.status(); + let body = response + .text() + .await + .context("reading provider registration response")?; + + if !status.is_success() { + bail!("provider registration failed: status={status} body={body}"); + } + + let id_response: models::IdResponse = + serde_json::from_str(&body).context("decoding provider registration response")?; + + Ok(id_response.id) +} + +fn map_api_error( + context_msg: &'static str, +) -> impl FnOnce(apis::Error) -> anyhow::Error { + move |err| match err { + apis::Error::ResponseError(content) => anyhow!( + "{} failed: status={} body={}", + context_msg, + content.status, + content.content + ), + other => anyhow!("{context_msg} failed: {other}"), + } +} + +fn require_http_200(status: reqwest::StatusCode, what: &str) -> Result<()> { + if status != reqwest::StatusCode::OK { + bail!("Unexpected HTTP status for {what}: {status}"); + } + + Ok(()) +} + +fn write_record(writer: &mut File, record: &BenchRecord) -> Result<()> { + writeln!( + writer, + "{},{},{},{},{},{},{}", + record.run, + record.scenario, + record.import_ms, + record.workflow_registration_ms, + record.wms_ms, + record.loading_info_ms, + record.http_status + ) + .context("writing CSV row") +} + +fn write_gdal_access_records( + writer: &mut File, + run: u32, + scenario: &str, + log_file: &Path, +) -> Result<()> { + let access_counts = collect_gdal_file_access_counts(log_file)?; + + for (file_path, count) in access_counts { + writeln!(writer, "{run},{scenario},{file_path},{count}") + .context("writing GDAL access row")?; + } + + Ok(()) +} + +fn collect_gdal_file_access_counts(log_file: &Path) -> Result> { + let log_text = + fs::read_to_string(log_file).with_context(|| format!("reading {}", log_file.display()))?; + let mut counts: BTreeMap = BTreeMap::new(); + + for line in log_text.lines() { + let clean_line = strip_ansi_escape_sequences(line); + let Some((_, rest)) = clean_line.split_once("Loading raster tile from file:") else { + continue; + }; + + let mut path = rest.trim(); + if let Some(stripped) = path.strip_prefix('"') { + path = stripped; + } + if let Some(stripped) = path.strip_suffix('"') { + path = stripped; + } + + if path.is_empty() { + continue; + } + + *counts.entry(path.to_owned()).or_insert(0) += 1; + } + + Ok(counts) +} + +fn print_summary(records: &[BenchRecord]) { + #[derive(Default)] + struct Agg { + count: u32, + import_ms: u128, + workflow_registration_ms: u128, + loading_info_ms: u128, + wms_ms: u128, + } + + let mut by_scenario: BTreeMap<&str, Agg> = BTreeMap::new(); + + for record in records { + let agg = by_scenario.entry(record.scenario).or_default(); + agg.count += 1; + agg.import_ms += record.import_ms; + agg.workflow_registration_ms += record.workflow_registration_ms; + agg.loading_info_ms += record.loading_info_ms; + agg.wms_ms += record.wms_ms; + } + + for (name, agg) in &by_scenario { + if agg.count == 0 { + continue; + } + + let count = f64::from(agg.count); + println!( + "{name}: runs={} avg_import_ms={:.2} avg_workflow_registration_ms={:.2} avg_wms_ms={:.2} avg_loading_info_ms={:.2}", + agg.count, + agg.import_ms as f64 / count, + agg.workflow_registration_ms as f64 / count, + agg.wms_ms as f64 / count, + agg.loading_info_ms as f64 / count + ); + } +} + +fn generate_gdal_file_comparison(input_file: &Path, output_file: &Path) -> Result<()> { + // Read the gdal_file_accesses.csv file + let content = fs::read_to_string(input_file) + .with_context(|| format!("reading {}", input_file.display()))?; + + // Parse the CSV and organize by file_path and scenario + let mut data: HashMap> = HashMap::new(); + + for line in content.lines().skip(1) { + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() != 4 { + continue; + } + + let scenario = parts[1]; + let file_path = parts[2]; + let access_count = parts[3].parse::().unwrap_or(0); + + data.entry(file_path.to_owned()) + .or_default() + .insert(scenario.to_owned(), access_count); + } + + // Collect all scenarios + let mut scenarios: HashSet = HashSet::new(); + for file_data in data.values() { + for scenario in file_data.keys() { + scenarios.insert(scenario.clone()); + } + } + + let mut scenarios: Vec = scenarios.into_iter().collect(); + scenarios.sort(); + + // Create output CSV + let mut output = + File::create(output_file).with_context(|| format!("creating {}", output_file.display()))?; + + // Write header + write!(output, "file_path")?; + for scenario in &scenarios { + write!(output, ",{scenario}")?; + } + writeln!(output)?; + + // Write data rows (sorted by file_path) + let mut file_paths: Vec = data.keys().cloned().collect(); + file_paths.sort(); + + for file_path in file_paths { + write!(output, "{file_path}")?; + if let Some(file_data) = data.get(&file_path) { + for scenario in &scenarios { + let count = file_data.get(scenario).copied().unwrap_or(0); + write!(output, ",{count}")?; + } + } else { + for _ in &scenarios { + write!(output, ",")?; + } + } + writeln!(output)?; + } + + output.flush()?; + + Ok(()) +} + +fn workspace_root() -> PathBuf { + let services_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + services_dir + .parent() + .expect("services crate must have a parent directory") + .to_path_buf() +} + +impl Config { + fn from_env() -> Result { + let runs = env_or_default("RUNS", "3")? + .parse::() + .context("parsing RUNS")?; + let host = env_or_default("HOST", "127.0.0.1")?; + let port = env_or_default("PORT", "3030")? + .parse::() + .context("parsing PORT")?; + let stac_url = env_or_default("STAC_URL", "https://stac.nsiscloud.polsa.gov.pl/v1")?; + let stac_s3_endpoint = env_or_default("STAC_S3_ENDPOINT", "eodata.nsiscloud.polsa.gov.pl")?; + let stac_s3_access_key = env::var("STAC_S3_ACCESS_KEY") + .context("missing STAC_S3_ACCESS_KEY environment variable")?; + let stac_s3_secret_key = env::var("STAC_S3_SECRET_KEY") + .context("missing STAC_S3_SECRET_KEY environment variable")?; + let out_dir = PathBuf::from(env_or_default("OUT_DIR", "/tmp/geoengine-stac-benchmark")?); + let import_limit = env_or_default("IMPORT_LIMIT", "100")? + .parse::() + .context("parsing IMPORT_LIMIT")?; + let import_bbox = env_or_default("IMPORT_BBOX", IMPORT_BBOX)?; + let import_time_start = env_or_default("IMPORT_TIME_START", "2026-01-01T00:00:00Z")?; + let import_time_end = env_or_default("IMPORT_TIME_END", "2026-01-31T23:59:59Z")?; + + Ok(Self { + runs, + host, + port, + stac_url, + stac_s3_endpoint, + stac_s3_access_key, + stac_s3_secret_key, + out_dir, + import_limit, + import_bbox, + import_time_start, + import_time_end, + }) + } + + fn base_url(&self) -> String { + format!("http://{}:{}/api", self.host, self.port) + } +} + +#[allow(clippy::unnecessary_wraps)] +fn env_or_default(key: &str, default: &str) -> Result { + Ok(env::var(key).unwrap_or_else(|_| default.to_owned())) +} diff --git a/geoengine/services/examples/ndvi_remote_jp2.rs b/geoengine/services/examples/ndvi_remote_jp2.rs new file mode 100644 index 0000000000..f8dff328b0 --- /dev/null +++ b/geoengine/services/examples/ndvi_remote_jp2.rs @@ -0,0 +1,459 @@ +#![allow(clippy::print_stdout)] + +use geoengine_datatypes::operations::image::{Colorizer, RgbaColor, ToPng}; +use geoengine_datatypes::primitives::{CacheHint, Coordinate2D, TimeInterval}; +use geoengine_datatypes::raster::{ + GeoTransform, Grid2D, GridIdx2D, GridOrEmpty2D, GridShape2D, MaskedGrid2D, RasterTile2D, +}; +use geoengine_datatypes::util::gdal::gdal_open_dataset; +use std::time::{Duration, Instant}; + +const CODE_DE_S3_ENDPOINT: &str = "eodata.nsiscloud.polsa.gov.pl"; +const B04_URL: &str = "/vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_B04_10m.jp2"; +const B08_URL: &str = "/vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_B08_10m.jp2"; +const SCL_URL: &str = "/vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_SCL_20m.jp2"; + +struct BandData { + b04: Vec, + b08: Vec, + scl: Vec, +} + +struct NdviResult { + data: Vec, + valid_mask: Vec, +} + +#[derive(Debug, Clone, Copy)] +enum ReadStrategy { + /// Read the entire raster in one GDAL call + FullRaster, + /// Read in tiles, opening dataset once and reusing it + TiledReusedDataset { tile_size: usize }, + /// Read in tiles, opening dataset for each tile + TiledReopenDataset { tile_size: usize }, +} + +/// Workflow of reading remote Sentinel-2 data using vsicurl, calculating NDVI, and exporting as PNG. +/// Compare direct reads to tiled reads. This is a baseline to compare a Geo Engine workflow against. +fn main() -> Result<(), Box> { + const N_RUNS: usize = 5; + + // Set global GDAL config for code-de /vsis3 S3 access. + gdal::config::set_config_option("AWS_S3_ENDPOINT", CODE_DE_S3_ENDPOINT)?; + gdal::config::set_config_option("AWS_VIRTUAL_HOSTING", "FALSE")?; + if let Ok(access_key) = std::env::var("AWS_ACCESS_KEY_ID") { + gdal::config::set_config_option("AWS_ACCESS_KEY_ID", &access_key)?; + } + if let Ok(secret_key) = std::env::var("AWS_SECRET_ACCESS_KEY") { + gdal::config::set_config_option("AWS_SECRET_ACCESS_KEY", &secret_key)?; + } + + // minx: 419_960.0, + // miny: 5_610_200.0, + // maxx: 489_760.0, + // maxy: 5_680_000.0, + let origin = Coordinate2D::new(419_960.0, 5_680_000.0); + + let pixel_size = 10.0; // Sentinel-2 10m resolution + + let sentinel_tile_pixel_size = 10980; + // clip tile, to make it comparable to Geo Engine workflow where we omit the borders as to avoid also downloading overlapping neighboring tiles + let pixel_border = 2000; + + let x_offset = pixel_border as isize; + let y_offset = pixel_border as isize; + + let width = sentinel_tile_pixel_size - 2 * pixel_border; + let height = sentinel_tile_pixel_size - 2 * pixel_border; + let pixel_size_x = pixel_size; + let pixel_size_y = -pixel_size; // negative because y decreases as we go down + + // Test all three strategies + let strategies = vec![ + ReadStrategy::FullRaster, + ReadStrategy::TiledReusedDataset { tile_size: 512 }, + ReadStrategy::TiledReopenDataset { tile_size: 512 }, + ]; + + // Store timings for each strategy + let mut timing_results: Vec<(ReadStrategy, Vec<(Duration, Duration)>)> = Vec::new(); + let mut first_ndvi: Option = None; + let mut reference_ndvi: Option<&NdviResult> = None; + + for strategy in &strategies { + println!("Running {strategy:?} {N_RUNS} times..."); + let mut timings = Vec::new(); + + for run in 1..=N_RUNS { + print!(" Run {run}/{N_RUNS}... "); + std::io::Write::flush(&mut std::io::stdout())?; + + // Process data + let (bands, read_duration) = read_bands(width, height, x_offset, y_offset, *strategy)?; + let (ndvi, ndvi_duration) = compute_ndvi(&bands, width, height); + let _tile = + create_raster_tile(&ndvi, origin, width, height, pixel_size_x, pixel_size_y)?; + + timings.push((read_duration, ndvi_duration)); + + // Store first result for verification and PNG export + if first_ndvi.is_none() { + first_ndvi = Some(ndvi); + reference_ndvi = first_ndvi.as_ref(); + } else if run == 1 { + // Verify this strategy matches the first strategy's result + if let Some(reference) = reference_ndvi + && (ndvi.data != reference.data || ndvi.valid_mask != reference.valid_mask) + { + println!("\n✗ {strategy:?} differs from reference!"); + } + } + + let total = read_duration.as_secs_f64() + ndvi_duration.as_secs_f64(); + println!("{total:.2}s"); + } + + timing_results.push((*strategy, timings)); + } + + // Export PNG from first result + let png_duration = export_png( + first_ndvi.as_ref().expect("at least one run completed"), + width, + height, + "ndvi_output.png", + )?; + + // Print comparison table with mean values + println!("\n=== Performance Comparison (mean of {N_RUNS} runs) ==="); + println!( + "{:<35} {:>12} {:>12} {:>12}", + "Strategy", "Read (s)", "NDVI (s)", "Total (s)" + ); + println!("{:-<72}", ""); + + for (strategy, timings) in &timing_results { + let mean_read = timings.iter().map(|(r, _)| r.as_secs_f64()).sum::() / N_RUNS as f64; + let mean_ndvi = timings.iter().map(|(_, n)| n.as_secs_f64()).sum::() / N_RUNS as f64; + let mean_total = mean_read + mean_ndvi; + + println!( + "{:<35} {:>12.2} {:>12.2} {:>12.2}", + format!("{strategy:?}"), + mean_read, + mean_ndvi, + mean_total + ); + } + println!("\nPNG export: {:.2}s", png_duration.as_secs_f64()); + + Ok(()) +} + +fn read_bands( + width: usize, + height: usize, + x_offset: isize, + y_offset: isize, + strategy: ReadStrategy, +) -> Result<(BandData, Duration), Box> { + let start = Instant::now(); + + let data = match strategy { + ReadStrategy::FullRaster => read_bands_full_raster(width, height, x_offset, y_offset)?.0, + ReadStrategy::TiledReusedDataset { tile_size } + | ReadStrategy::TiledReopenDataset { tile_size } => { + let reopen_per_tile = matches!(strategy, ReadStrategy::TiledReopenDataset { .. }); + let scl_dataset = gdal_open_dataset(std::path::Path::new(SCL_URL))?; + let b08_dataset = gdal_open_dataset(std::path::Path::new(B08_URL))?; + let b04_dataset = gdal_open_dataset(std::path::Path::new(B04_URL))?; + read_bands_tiled_internal( + &b04_dataset, + &b08_dataset, + &scl_dataset, + width, + height, + x_offset, + y_offset, + tile_size, + reopen_per_tile, + )? + .0 + } + }; + + Ok((data, start.elapsed())) +} + +fn read_bands_full_raster( + width: usize, + height: usize, + x_offset: isize, + y_offset: isize, +) -> Result<(BandData, Duration), Box> { + let start = Instant::now(); + + let scl_dataset = gdal_open_dataset(std::path::Path::new(SCL_URL))?; + let b08_dataset = gdal_open_dataset(std::path::Path::new(B08_URL))?; + let b04_dataset = gdal_open_dataset(std::path::Path::new(B04_URL))?; + + // Read B04 (10m resolution) + let b04_band = b04_dataset.rasterband(1)?; + let b04_buffer = + b04_band.read_as::((x_offset, y_offset), (width, height), (width, height), None)?; + let (_, b04_data) = b04_buffer.into_shape_and_vec(); + + // Read B08 (10m resolution) + let b08_band = b08_dataset.rasterband(1)?; + let b08_buffer = + b08_band.read_as::((x_offset, y_offset), (width, height), (width, height), None)?; + let (_, b08_data) = b08_buffer.into_shape_and_vec(); + + // Read SCL (20m resolution) - need to adjust offset and size + let scl_width = width / 2; + let scl_height = height / 2; + let scl_x_offset = x_offset / 2; + let scl_y_offset = y_offset / 2; + + let scl_band = scl_dataset.rasterband(1)?; + let scl_buffer = scl_band.read_as::( + (scl_x_offset, scl_y_offset), + (scl_width, scl_height), + (width, height), + None, + )?; + let (_, scl_data) = scl_buffer.into_shape_and_vec(); + + let duration = start.elapsed(); + + Ok(( + BandData { + b04: b04_data, + b08: b08_data, + scl: scl_data, + }, + duration, + )) +} + +#[allow(clippy::too_many_arguments)] +fn read_bands_tiled_internal( + b04_dataset: &gdal::Dataset, + b08_dataset: &gdal::Dataset, + scl_dataset: &gdal::Dataset, + width: usize, + height: usize, + x_offset: isize, + y_offset: isize, + tile_size: usize, + reopen_per_tile: bool, +) -> Result<(BandData, usize), Box> { + // Allocate output buffers + let mut b04_data = vec![0u16; width * height]; + let mut b08_data = vec![0u16; width * height]; + let mut scl_data = vec![0u8; width * height]; + + let mut tile_count = 0; + + // Read in tiles + for tile_y in (0..height).step_by(tile_size) { + for tile_x in (0..width).step_by(tile_size) { + let tile_width = tile_size.min(width - tile_x); + let tile_height = tile_size.min(height - tile_y); + + // Get datasets for this tile (reopen if needed) + let (b04_ds, b08_ds, scl_ds); + let (b04_band, b08_band, scl_band) = if reopen_per_tile { + b04_ds = gdal_open_dataset(std::path::Path::new(B04_URL))?; + b08_ds = gdal_open_dataset(std::path::Path::new(B08_URL))?; + scl_ds = gdal_open_dataset(std::path::Path::new(SCL_URL))?; + ( + b04_ds.rasterband(1)?, + b08_ds.rasterband(1)?, + scl_ds.rasterband(1)?, + ) + } else { + ( + b04_dataset.rasterband(1)?, + b08_dataset.rasterband(1)?, + scl_dataset.rasterband(1)?, + ) + }; + + // Read B04 tile + let tile_b04_buffer = b04_band.read_as::( + (x_offset + tile_x as isize, y_offset + tile_y as isize), + (tile_width, tile_height), + (tile_width, tile_height), + None, + )?; + let (_, tile_b04_data) = tile_b04_buffer.into_shape_and_vec(); + + // Read B08 tile + let tile_b08_buffer = b08_band.read_as::( + (x_offset + tile_x as isize, y_offset + tile_y as isize), + (tile_width, tile_height), + (tile_width, tile_height), + None, + )?; + let (_, tile_b08_data) = tile_b08_buffer.into_shape_and_vec(); + + // Read SCL tile (20m resolution) + let scl_tile_x = tile_x / 2; + let scl_tile_y = tile_y / 2; + let scl_tile_width = tile_width / 2; + let scl_tile_height = tile_height / 2; + let scl_x_offset = x_offset / 2; + let scl_y_offset = y_offset / 2; + + let tile_scl_buffer = scl_band.read_as::( + ( + scl_x_offset + scl_tile_x as isize, + scl_y_offset + scl_tile_y as isize, + ), + (scl_tile_width, scl_tile_height), + (tile_width, tile_height), // upsample to 10m + None, + )?; + let (_, tile_scl_data) = tile_scl_buffer.into_shape_and_vec(); + + // Copy tile data into output buffers + for ty in 0..tile_height { + let src_start = ty * tile_width; + let dst_start = (tile_y + ty) * width + tile_x; + b04_data[dst_start..dst_start + tile_width] + .copy_from_slice(&tile_b04_data[src_start..src_start + tile_width]); + b08_data[dst_start..dst_start + tile_width] + .copy_from_slice(&tile_b08_data[src_start..src_start + tile_width]); + scl_data[dst_start..dst_start + tile_width] + .copy_from_slice(&tile_scl_data[src_start..src_start + tile_width]); + } + + tile_count += 1; + } + } + + Ok(( + BandData { + b04: b04_data, + b08: b08_data, + scl: scl_data, + }, + tile_count, + )) +} + +fn compute_ndvi(bands: &BandData, width: usize, height: usize) -> (NdviResult, Duration) { + let start = Instant::now(); + + let mut ndvi_data = Vec::with_capacity(width * height); + let mut valid_mask = Vec::with_capacity(width * height); + + for i in 0..width * height { + let scl = bands.scl[i]; + let b04 = f32::from(bands.b04[i]); + let b08 = f32::from(bands.b08[i]); + + // Check SCL mask: if (A == 3 || (A >= 7 && A <= 11)) { NODATA } + if scl == 3 || (7..=11).contains(&scl) { + ndvi_data.push(0.0); + valid_mask.push(false); + } else { + // Compute NDVI: (B08 - B04) / (B08 + B04) + let sum = b08 + b04; + if sum == 0.0 { + ndvi_data.push(0.0); + valid_mask.push(false); + } else { + let ndvi = (b08 - b04) / sum; + ndvi_data.push(ndvi); + valid_mask.push(true); + } + } + } + + let duration = start.elapsed(); + + ( + NdviResult { + data: ndvi_data, + valid_mask, + }, + duration, + ) +} + +fn create_raster_tile( + ndvi: &NdviResult, + origin: Coordinate2D, + width: usize, + height: usize, + pixel_size_x: f64, + pixel_size_y: f64, +) -> Result, Box> { + let grid_shape = GridShape2D::new([height, width]); + let validity_grid = Grid2D::new(grid_shape, ndvi.valid_mask.clone())?; + let masked_grid = + MaskedGrid2D::new(Grid2D::new(grid_shape, ndvi.data.clone())?, validity_grid)?; + let grid_or_empty: GridOrEmpty2D = masked_grid.into(); + + let geo_transform = GeoTransform::new(origin, pixel_size_x, pixel_size_y); + + let datetime: geoengine_datatypes::primitives::DateTime = + chrono::NaiveDate::from_ymd_opt(2020, 8, 7) + .expect("valid date") + .and_hms_opt(0, 0, 0) + .expect("valid time") + .and_utc() + .into(); + + let time = TimeInterval::new_instant(geoengine_datatypes::primitives::TimeInstance::from( + datetime, + ))?; + + Ok(RasterTile2D::new( + time, + GridIdx2D::new([0, 0]), + 0, + geo_transform, + grid_or_empty, + CacheHint::default(), + )) +} + +fn export_png( + ndvi: &NdviResult, + width: usize, + height: usize, + output_path: &str, +) -> Result> { + let start = Instant::now(); + + // Create a MaskedGrid2D with the NDVI data + let grid_shape = GridShape2D::new([height, width]); + + // Create colorizer: black (0,0,0, 255) to green (0,255,0, 255) for values 0 to 1 + let colorizer = Colorizer::linear_gradient( + vec![ + (0.0, RgbaColor::new(0, 0, 0, 255)).try_into()?, + (1.0, RgbaColor::new(0, 255, 0, 255)).try_into()?, + ], + RgbaColor::black(), // no data color + RgbaColor::new(0, 255, 0, 255), // over color + RgbaColor::black(), // under color + )?; + + // Create grid and masked grid + let data_grid = Grid2D::new(grid_shape, ndvi.data.clone())?; + let validity_grid = Grid2D::new(grid_shape, ndvi.valid_mask.clone())?; + let masked_grid = MaskedGrid2D::new(data_grid, validity_grid)?; + + // Use the ToPng trait to create PNG bytes + let png_bytes = masked_grid.to_png(width as u32, height as u32, &colorizer)?; + + // Write to file + std::fs::write(output_path, png_bytes)?; + + Ok(start.elapsed()) +} diff --git a/geoengine/services/examples/stac_provider_ndvi.rs b/geoengine/services/examples/stac_provider_ndvi.rs new file mode 100644 index 0000000000..3793cf5f17 --- /dev/null +++ b/geoengine/services/examples/stac_provider_ndvi.rs @@ -0,0 +1,284 @@ +#![allow(clippy::print_stdout)] + +use anyhow::{Context, Result}; +use futures::StreamExt; +use geoengine_datatypes::{ + dataset::DataProviderId, + primitives::{BandSelection, DateTime, RasterQueryRectangle, SpatialPartition2D, TimeInterval}, + raster::{GridBoundingBox2D, GridBounds, SpatialGridDefinition}, + util::Identifier, +}; +use geoengine_operators::engine::{ExecutionContext, WorkflowOperatorPath}; +use geoengine_services::{ + api::model::services::StacDataProviderDefinition as ApiStacDataProviderDefinition, + contexts::{ApplicationContext, PostgresContext, SessionContext}, + datasets::external::stac::StacDataProviderDefinition, + layers::storage::LayerProviderDb, + users::UserSession, + util::tests::with_temp_context, + workflows::workflow::Workflow, +}; +use std::{collections::BTreeMap, fs, path::Path}; +use tokio_postgres::NoTls; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; +use uuid::Uuid; + +const PROVIDER_TEMPLATE_ID: &str = "b274275c-373d-4a3f-8b45-9b48e9614329"; + +const QUERY_MIN_X: f64 = 399_960.0; +const QUERY_MIN_Y: f64 = 5_590_200.0; +const QUERY_MAX_X: f64 = 405_080.0; +const QUERY_MAX_Y: f64 = 5_595_320.0; + +const QUERY_TIME_YEAR: i32 = 2026; +const QUERY_TIME_MONTH: u8 = 1; +const QUERY_TIME_DAY: u8 = 1; + +#[derive(Debug, Default)] +struct ProfileMetrics { + file_access_counts: BTreeMap, + raster_read_time_seconds: f64, + stac_request_count: u32, + stac_wait_time_seconds: f64, +} + +#[tokio::main] +async fn main() -> Result<()> { + let log_file = std::env::temp_dir().join("geoengine_stac_ndvi_profile.log"); + let _guard = init_logging(&log_file)?; + + let metrics = with_temp_context( + move |app_ctx: PostgresContext, _db_config| async move { + run_profile_query(app_ctx, &log_file).await + }, + ) + .await?; + + println!("=== STAC NDVI Profile ==="); + println!("stac_requests: {}", metrics.stac_request_count); + println!( + "stac_wait_time_seconds: {:.6}", + metrics.stac_wait_time_seconds + ); + println!( + "raster_read_time_seconds: {:.6}", + metrics.raster_read_time_seconds + ); + println!("file_accesses:"); + for (path, count) in &metrics.file_access_counts { + println!(" {count:>5} {path}"); + } + + Ok(()) +} + +fn init_logging(log_file: &Path) -> Result { + let log_file_handle = fs::File::create(log_file) + .with_context(|| format!("creating log file {}", log_file.display()))?; + let (non_blocking_writer, guard) = tracing_appender::non_blocking(log_file_handle); + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::new("debug")) + .with_ansi(false) + .with_writer(non_blocking_writer) + .try_init() + .map_err(|e| anyhow::anyhow!("initializing tracing subscriber: {e}"))?; + + Ok(guard) +} + +async fn run_profile_query( + app_ctx: PostgresContext, + log_file: &Path, +) -> Result { + let admin_session = UserSession::admin_session(); + let admin_ctx = app_ctx.session_context(admin_session); + + // Use a fresh provider id in the temporary database schema. + let provider_id = DataProviderId::new(); + let provider = build_provider_definition(provider_id)?; + + admin_ctx + .db() + .add_layer_provider(provider.into()) + .await + .context("registering STAC provider")?; + + let workflow_json = include_str!("../../test_data/api_calls/stac_provider/ndvi-workflow.json") + .replace(PROVIDER_TEMPLATE_ID, &provider_id.to_string()); + + let workflow: Workflow = + serde_json::from_str(&workflow_json).context("deserializing NDVI workflow")?; + + let operator = workflow + .operator() + .context("extracting workflow operator")? + .get_raster() + .context("expecting raster workflow")?; + + let execution_ctx = admin_ctx + .execution_context() + .context("creating execution context")?; + + let initialized = operator + .clone() + .initialize(WorkflowOperatorPath::initialize_root(), &execution_ctx) + .await + .context("initializing workflow operator")?; + + let processor = initialized + .query_processor() + .context("creating query processor")? + .get_f32() + .context("expecting f32 NDVI output")?; + + let query_bounds = SpatialPartition2D::new( + (QUERY_MIN_X, QUERY_MAX_Y).into(), + (QUERY_MAX_X, QUERY_MIN_Y).into(), + ) + .context("creating query bounds")?; + + let full_query_grid = initialized + .result_descriptor() + .spatial_grid_descriptor() + .tiling_grid_definition(execution_ctx.tiling_specification()) + .tiling_spatial_grid_definition() + .spatial_bounds_to_compatible_spatial_grid(query_bounds); + + // Clamp to a single tile: use only the top-left tile index from the computed grid + let single_tile_idx = full_query_grid.grid_bounds().min_index(); + let single_tile_grid_bounds = + GridBoundingBox2D::new(single_tile_idx, single_tile_idx).context("single tile bounds")?; + let query_grid = + SpatialGridDefinition::new(full_query_grid.geo_transform(), single_tile_grid_bounds); + + let query_time = TimeInterval::new_instant(DateTime::new_utc( + QUERY_TIME_YEAR, + QUERY_TIME_MONTH, + QUERY_TIME_DAY, + 0, + 0, + 0, + )) + .context("creating query time")?; + + let query_rect = + RasterQueryRectangle::new(query_grid.grid_bounds(), query_time, BandSelection::first()); + + let query_ctx = admin_ctx + .query_context(Uuid::new_v4(), Uuid::new_v4()) + .context("creating query context")?; + + let mut stream = processor + .raster_query(query_rect, &query_ctx) + .await + .context("starting raster query")?; + + let mut tile_count = 0_u64; + while let Some(tile) = stream.next().await { + tile.context("query stream returned an error")?; + tile_count += 1; + } + + println!("tiles_received: {tile_count}"); + + parse_profile_metrics(log_file) +} + +fn build_provider_definition(provider_id: DataProviderId) -> Result { + let mut provider_definition: ApiStacDataProviderDefinition = serde_json::from_str( + include_str!("../../test_data/provider_defs_api/stac_sentinel2.json"), + ) + .context("deserializing STAC provider definition from test data")?; + + provider_definition.id = provider_id.into(); + + Ok(provider_definition.into()) +} + +fn parse_profile_metrics(log_file: &Path) -> Result { + let log_text = fs::read_to_string(log_file) + .with_context(|| format!("reading log file {}", log_file.display()))?; + + let mut metrics = ProfileMetrics::default(); + + for line in log_text.lines() { + let clean_line = strip_ansi_escape_sequences(line); + + if let Some((_, rest)) = clean_line.split_once("Loading raster tile from file:") { + let mut path = rest.trim(); + if let Some(stripped) = path.strip_prefix('"') { + path = stripped; + } + if let Some(stripped) = path.strip_suffix('"') { + path = stripped; + } + + if !path.is_empty() { + *metrics + .file_access_counts + .entry(path.to_owned()) + .or_insert(0) += 1; + } + } + + if let Some((_, rest)) = clean_line.split_once("read raster band in") + && let Some(seconds) = first_f64(rest) + { + metrics.raster_read_time_seconds += seconds; + } + + if clean_line.contains("STAC query first page") + || clean_line.contains("STAC query next page") + { + metrics.stac_request_count += 1; + } + + if let Some((_, rest)) = clean_line.split_once("STAC response received in") + && let Some(seconds) = first_f64(rest) + { + metrics.stac_wait_time_seconds += seconds; + } + } + + Ok(metrics) +} + +fn first_f64(input: &str) -> Option { + let mut token = String::new(); + for c in input.chars() { + if c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E') { + token.push(c); + } else if !token.is_empty() { + return token.parse::().ok(); + } + } + + if token.is_empty() { + None + } else { + token.parse::().ok() + } +} + +fn strip_ansi_escape_sequences(input: &str) -> String { + let mut output = String::new(); + let mut chars = input.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch == '\u{1b}' && matches!(chars.peek(), Some('[')) { + let _ = chars.next(); + for next_ch in chars.by_ref() { + if ('@'..='~').contains(&next_ch) { + break; + } + } + continue; + } + + output.push(ch); + } + + output +} diff --git a/geoengine/services/src/api/handlers/datasets.rs b/geoengine/services/src/api/handlers/datasets.rs index 1975cd9813..58701f836c 100755 --- a/geoengine/services/src/api/handlers/datasets.rs +++ b/geoengine/services/src/api/handlers/datasets.rs @@ -278,6 +278,11 @@ fn validate_tile( .file_path .to_string_lossy() .starts_with("/vsicurl") + || tile + .params + .file_path + .to_string_lossy() + .starts_with("/vsis3") { // do not validate remote files // TODO: add flag to do this? diff --git a/geoengine/services/src/api/model/datatypes.rs b/geoengine/services/src/api/model/datatypes.rs index 3260334ebe..e199c91e4d 100644 --- a/geoengine/services/src/api/model/datatypes.rs +++ b/geoengine/services/src/api/model/datatypes.rs @@ -2201,7 +2201,7 @@ impl From for geoengine_datatypes::primitives::MultiPolygon { } #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] -pub struct StringPair((String, String)); +pub struct StringPair(pub (String, String)); pub type GdalConfigOption = StringPair; pub type AxisLabels = StringPair; diff --git a/geoengine/services/src/api/model/services.rs b/geoengine/services/src/api/model/services.rs index bb3c322639..b721a2d482 100644 --- a/geoengine/services/src/api/model/services.rs +++ b/geoengine/services/src/api/model/services.rs @@ -1,9 +1,13 @@ -use super::datatypes::{CacheTtlSeconds, DataId, DataProviderId, DatasetId, GdalConfigOption}; +use super::datatypes::{ + CacheTtlSeconds, DataId, DataProviderId, DatasetId, GdalConfigOption, RasterDataType, + SpatialReference, SpatialResolution, TimeGranularity, +}; use super::operators::TypedResultDescriptor; use crate::api::model::datatypes::MlModelName; use crate::api::model::operators::{ GdalMetaDataList, GdalMetaDataRegular, GdalMetaDataStatic, GdalMetadataNetCdfCf, - MlModelMetadata, MockMetaData, OgrMetaData, + MlModelMetadata, MockMetaData, OgrMetaData, RegularTimeDimension, SpatialGridDescriptor, + TimeDimension, }; use crate::datasets::DatasetName; use crate::datasets::external::{GdalRetries, WildliveDataConnectorAuth}; @@ -887,6 +891,208 @@ impl From, + pub secret_key: Option, +} + +impl From for crate::datasets::external::stac::StacProviderS3Config { + fn from(value: StacProviderS3Config) -> Self { + Self { + endpoint: value.endpoint, + access_key: value.access_key, + secret_key: value.secret_key, + } + } +} + +impl From for StacProviderS3Config { + fn from(value: crate::datasets::external::stac::StacProviderS3Config) -> Self { + Self { + endpoint: value.endpoint, + access_key: value.access_key.map(|_| SECRET_REPLACEMENT.to_string()), + secret_key: value.secret_key.map(|_| SECRET_REPLACEMENT.to_string()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StacProviderDatasetBand { + pub asset_title: String, + pub band_name: Option, +} + +impl From for crate::datasets::external::stac::StacProviderDatasetBand { + fn from(value: StacProviderDatasetBand) -> Self { + Self { + asset_title: value.asset_title, + band_name: value.band_name, + } + } +} + +impl From for StacProviderDatasetBand { + fn from(value: crate::datasets::external::stac::StacProviderDatasetBand) -> Self { + Self { + asset_title: value.asset_title, + band_name: value.band_name, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StacProviderDataset { + pub name: String, + pub description: String, + pub data_type: RasterDataType, + pub resolution: SpatialResolution, + #[schema(value_type = String)] + pub projection: SpatialReference, + pub spatial_grid: SpatialGridDescriptor, + pub bands: Vec, +} + +impl From for crate::datasets::external::stac::StacProviderDataset { + fn from(value: StacProviderDataset) -> Self { + Self { + name: value.name, + description: value.description, + data_type: value.data_type.into(), + resolution: value.resolution.into(), + projection: value.projection.into(), + spatial_grid: value.spatial_grid.into(), + bands: value.bands.into_iter().map(Into::into).collect(), + } + } +} + +impl From for StacProviderDataset { + fn from(value: crate::datasets::external::stac::StacProviderDataset) -> Self { + Self { + name: value.name, + description: value.description, + data_type: value.data_type.into(), + resolution: value.resolution.into(), + projection: value.projection.into(), + spatial_grid: value.spatial_grid.into(), + bands: value.bands.into_iter().map(Into::into).collect(), + } + } +} + +#[allow(clippy::needless_pass_by_value)] +fn api_time_dimension_to_datatypes( + value: TimeDimension, +) -> geoengine_datatypes::primitives::TimeDimension { + match value { + TimeDimension::Regular(RegularTimeDimension { origin, step }) => { + geoengine_datatypes::primitives::TimeDimension::Regular( + geoengine_datatypes::primitives::RegularTimeDimension { + origin: origin.into(), + step: step.into(), + }, + ) + } + TimeDimension::Irregular => geoengine_datatypes::primitives::TimeDimension::Irregular, + } +} + +fn datatypes_time_dimension_to_api( + value: geoengine_datatypes::primitives::TimeDimension, +) -> TimeDimension { + match value { + geoengine_datatypes::primitives::TimeDimension::Regular( + geoengine_datatypes::primitives::RegularTimeDimension { origin, step }, + ) => TimeDimension::Regular(RegularTimeDimension { + origin: origin.into(), + step: step.into(), + }), + geoengine_datatypes::primitives::TimeDimension::Irregular => TimeDimension::Irregular, + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StacTimeStep { + pub granularity: TimeGranularity, + pub value: u32, +} + +impl From for geoengine_datatypes::primitives::TimeStep { + fn from(value: StacTimeStep) -> Self { + Self { + granularity: value.granularity.into(), + step: value.value, + } + } +} + +impl From for StacTimeStep { + fn from(value: geoengine_datatypes::primitives::TimeStep) -> Self { + Self { + granularity: value.granularity.into(), + value: value.step, + } + } +} + +#[type_tag(value = "StacProviderDefinition")] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StacDataProviderDefinition { + pub name: String, + pub id: DataProviderId, + pub description: String, + pub priority: Option, + pub api_url: String, + pub collection_name: String, + pub s3_config: Option, + pub time_dimension: TimeDimension, + pub datasets: Vec, +} + +impl From + for crate::datasets::external::stac::StacDataProviderDefinition +{ + fn from(value: StacDataProviderDefinition) -> Self { + crate::datasets::external::stac::StacDataProviderDefinition { + name: value.name, + id: value.id.into(), + description: value.description, + priority: value.priority, + api_url: value.api_url, + collection_name: value.collection_name, + s3_config: value.s3_config.map(Into::into), + time_dimension: api_time_dimension_to_datatypes(value.time_dimension), + datasets: value.datasets.into_iter().map(Into::into).collect(), + } + } +} + +impl From + for StacDataProviderDefinition +{ + fn from(value: crate::datasets::external::stac::StacDataProviderDefinition) -> Self { + StacDataProviderDefinition { + r#type: Default::default(), + name: value.name, + id: value.id.into(), + description: value.description, + priority: value.priority, + api_url: value.api_url, + collection_name: value.collection_name, + s3_config: value.s3_config.map(Into::into), + time_dimension: datatypes_time_dimension_to_api(value.time_dimension), + datasets: value.datasets.into_iter().map(Into::into).collect(), + } + } +} + #[type_tag(value = "DatasetLayerListing")] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] @@ -1037,6 +1243,7 @@ pub enum TypedDataProviderDefinition { NetCdfCfDataProviderDefinition(NetCdfCfDataProviderDefinition), PangaeaDataProviderDefinition(PangaeaDataProviderDefinition), SentinelS2L2ACogsProviderDefinition(SentinelS2L2ACogsProviderDefinition), + StacDataProviderDefinition(StacDataProviderDefinition), WildliveDataConnectorDefinition(WildliveDataConnectorDefinition), } @@ -1054,6 +1261,7 @@ impl From for crate::layers::external::TypedDataPro TypedDataProviderDefinition::NetCdfCfDataProviderDefinition(def) => crate::layers::external::TypedDataProviderDefinition::NetCdfCfDataProviderDefinition(def.into()), TypedDataProviderDefinition::PangaeaDataProviderDefinition(def) => crate::layers::external::TypedDataProviderDefinition::PangaeaDataProviderDefinition(def.into()), TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition(def) => crate::layers::external::TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition(def.into()), + TypedDataProviderDefinition::StacDataProviderDefinition(def) => crate::layers::external::TypedDataProviderDefinition::StacDataProviderDefinition(def.into()), TypedDataProviderDefinition::WildliveDataConnectorDefinition(def) => crate::layers::external::TypedDataProviderDefinition::WildliveDataConnectorDefinition(def.into()), } } @@ -1073,6 +1281,7 @@ impl From for TypedDataPro crate::layers::external::TypedDataProviderDefinition::NetCdfCfDataProviderDefinition(def) => TypedDataProviderDefinition::NetCdfCfDataProviderDefinition(def.into()), crate::layers::external::TypedDataProviderDefinition::PangaeaDataProviderDefinition(def) => TypedDataProviderDefinition::PangaeaDataProviderDefinition(def.into()), crate::layers::external::TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition(def) => TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition(def.into()), + crate::layers::external::TypedDataProviderDefinition::StacDataProviderDefinition(def) => TypedDataProviderDefinition::StacDataProviderDefinition(def.into()), crate::layers::external::TypedDataProviderDefinition::WildliveDataConnectorDefinition(def) => TypedDataProviderDefinition::WildliveDataConnectorDefinition(def.into()), } } @@ -1176,3 +1385,58 @@ impl From for MlModel { } } } + +#[cfg(test)] +mod tests { + use super::{ + SpatialResolution, StacDataProviderDefinition, StacProviderS3Config, + TypedDataProviderDefinition, + }; + use crate::api::model::services::SECRET_REPLACEMENT; + use geoengine_datatypes::test_data; + use std::fs; + + #[test] + #[allow(clippy::float_cmp)] + fn stac_fixture_deserializes_to_api_type() { + let fixture = fs::read_to_string(test_data!("provider_defs_api/stac_sentinel2.json")) + .expect("failed to read stac fixture"); + + let stac_definition: StacDataProviderDefinition = serde_json::from_str(&fixture) + .expect("stac fixture must deserialize to StacDataProviderDefinition"); + + assert_eq!(stac_definition.collection_name, "sentinel-2-l2a"); + assert!(matches!( + stac_definition.datasets[0].resolution, + SpatialResolution { x, y } if x == 10. && y == 10. + )); + + let definition: TypedDataProviderDefinition = + serde_json::from_str(&fixture).expect("stac fixture must deserialize"); + + let TypedDataProviderDefinition::StacDataProviderDefinition(definition) = definition else { + panic!("fixture must deserialize into StacDataProviderDefinition"); + }; + + assert_eq!(definition.collection_name, "sentinel-2-l2a"); + assert!(matches!( + definition.datasets[0].resolution, + SpatialResolution { x, y } if x == 10. && y == 10. + )); + } + + #[test] + fn stac_s3_credentials_are_redacted_in_api_conversion() { + let internal = crate::datasets::external::stac::StacProviderS3Config { + endpoint: "https://example-s3.local".to_owned(), + access_key: Some("my-access-key".to_owned()), + secret_key: Some("my-secret-key".to_owned()), + }; + + let api: StacProviderS3Config = internal.into(); + + assert_eq!(api.endpoint, "https://example-s3.local"); + assert_eq!(api.access_key.as_deref(), Some(SECRET_REPLACEMENT)); + assert_eq!(api.secret_key.as_deref(), Some(SECRET_REPLACEMENT)); + } +} diff --git a/geoengine/services/src/cli/stac_import.rs b/geoengine/services/src/cli/stac_import.rs index 330afe35dd..73aa4c81fb 100644 --- a/geoengine/services/src/cli/stac_import.rs +++ b/geoengine/services/src/cli/stac_import.rs @@ -3,13 +3,14 @@ use ordered_float::OrderedFloat; use std::{ collections::{HashMap, HashSet}, + path::PathBuf, str::FromStr, time::{Duration, Instant}, }; use anyhow::Context; use chrono::Timelike; -use clap::Parser; +use clap::{Parser, ValueEnum}; use futures::StreamExt; use geoengine_datatypes::{ primitives::{DateTime, TimeInstance, TimeInterval}, @@ -29,15 +30,16 @@ use crate::{ }, model::{ datatypes::{ - GridBoundingBox2D, GridIdx2D, LayerId, Measurement, RasterDataType, - SpatialGridDefinition, TimeGranularity, TimeStep, UnitlessMeasurement, + GdalConfigOption, GridBoundingBox2D, GridIdx2D, LayerId, Measurement, + RasterDataType, SpatialGridDefinition, StringPair, TimeGranularity, TimeStep, + UnitlessMeasurement, }, operators::{ GdalDatasetParameters, GdalMultiBand, RasterBandDescriptor, RasterBandDescriptors, RasterResultDescriptor, RegularTimeDimension, SpatialGridDescriptor, SpatialGridDescriptorState, TimeDescriptor, TimeDimension, }, - responses::IdResponse, + responses::{ErrorResponse, IdResponse}, services::{ AddDataset, CreateDataset, DataPath, DatasetDefinition, MetaDataDefinition, }, @@ -45,9 +47,9 @@ use crate::{ }, datasets::{DatasetName, upload::VolumeName}, layers::{ - layer::{AddLayer, AddLayerCollection}, + layer::{AddLayer, AddLayerCollection, CollectionItem, LayerCollection}, listing::LayerCollectionId, - storage::INTERNAL_LAYER_DB_ROOT_COLLECTION_ID, + storage::{INTERNAL_LAYER_DB_ROOT_COLLECTION_ID, INTERNAL_PROVIDER_ID}, }, permissions::{Permission, Role}, workflows::workflow::Workflow, @@ -58,36 +60,66 @@ use geoengine_operators::{ source::{MultiBandGdalSource, MultiBandGdalSourceParameters}, }; -const EXTERNAL_VOLUME_NAME: &str = "external"; const MAX_RETRIES: u32 = 10; const INITIAL_RETRY_DELAY_MS: u64 = 1000; +// TODO: make the filter depend on stac version and stac extension versions? +// TODO: also filter fields of collections api? +const STAC_ITEMS_FIELDS_FILTER: &[&str] = &[ + "id", + "type", + "geometry", + "bbox", + "links", + "stac_version", + "stac_extensions", + "properties.datetime", + "properties.updated", + "properties.proj:epsg", + "properties.proj:code", + "assets.*.title", + "assets.*.type", + "assets.*.data_type", + "assets.*.gsd", + "assets.*.href", + "assets.*.bands", + "assets.*.eo:bands", + "assets.*.raster:bands", + "assets.*.proj:epsg", + "assets.*.proj:transform", + "assets.*.proj:shape", + "assets.*.proj:code", +]; + /// STAC catalog importer for Geo Engine #[derive(Debug, Parser)] pub struct StacImport { /// STAC API URL - #[arg(long, default_value = "https://earth-search.aws.element84.com/v1")] + #[arg(long)] stac_url: String, // collection to import from #[arg(long, default_value = "sentinel-2-l2a")] stac_collection: String, - // import limit + // import limit (page size) #[arg(long, default_value = None)] limit: Option, - // time range start to import - #[arg(long, default_value = "2020-12-25T00:00:00Z")] - time_start: String, + /// Time range start to import (optional) + /// Example: 2020-12-25T00:00:00Z + #[arg(long)] + time_start: Option, - // time range end to import - #[arg(long, default_value = "2020-12-31T23:59:59Z")] - time_end: String, + /// Time range end to import (optional) + /// Example: 2020-12-31T23:59:59Z + #[arg(long)] + time_end: Option, - // bbox to import: minx miny maxx maxy - #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ', default_values = &["0.3184423382324359", "-0.0884814721075339", "20.784002046916676", "72.0970954730969"])] - bbox: Vec, + /// Bounding box to import: minx miny maxx maxy (optional) + /// Example: 0.3184423382324359 -0.0884814721075339 20.784002046916676 72.0970954730969 + #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')] + bbox: Option>, // // bands to import // #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ', default_values = &["aot", "nir08", "rededge1", "rededge2", "rededge3", "scl", "swir16", "swir22"])] @@ -124,16 +156,34 @@ pub struct StacImport { #[arg(long, default_value_t = 2)] prefetch_pages: usize, - // handle missing bands: - // no eo:bands and only single-band -> use item key as band name - // no raster:bands and role visual and three bands -> use u8 bands - // if this parameter is false, assets with missing eo:bands or raster:bands will be skipped - #[arg(long, default_value_t = false)] - missing_bands_handling: bool, // TODO: time granularity (validity of items) /// Filter datasets by EPSG codes (only create and insert tiles for these EPSG codes) #[clap(long, value_parser, num_args = 0.., value_delimiter = ' ')] epsgs: Vec, + + /// feature/item property to use as z-index for tiles (if not provided, z-index will be set to 0 or computed from item properties in a hardcoded way) + /// must be date time property + #[arg(long, default_value = "updated")] + z_index_property_name: Option, + + #[arg(long)] + s3_endpoint: Option, + + #[arg(long)] + s3_access_key: Option, + + #[arg(long)] + s3_secret_key: Option, + + /// File types to import from STAC assets. + /// Supported values: cog, jp2. + /// Defaults to COG only. + #[arg(long, value_enum, num_args = 1.., value_delimiter = ' ', default_values_t = [ImportFileType::Cog])] + file_types: Vec, + + /// if true, filter the stac item fields in the query to only fetch the fields required for the import, which may reduce the response size and speed up the import. + #[arg(long, default_value_t = false)] + filter_item_fields: bool, // /// Parent layer collection ID // #[arg(long, default_value_t = INTERNAL_LAYER_DB_ROOT_COLLECTION_ID)] // parent_layer_collection_id: Uuid, @@ -148,7 +198,30 @@ pub struct StacImport { } /// Example call for Sentinel 2 from Element 84: -/// `cargo run --bin geoengine-cli stac-import --limit 267 --missing-bands-handling --verbose` +/// ```bash +/// cargo run --bin geoengine-cli stac-import \ +/// --verbose \ +/// --limit 267 \ +/// --bbox "8.766 50.802 8.767 50.803" \ +/// --time-start 2020-08-01T00:00:00Z \ +/// --time-end 2020-08-31T23:59:59Z \ +/// --stac-url https://earth-search.aws.element84.com/v1 +/// ``` +/// +/// Example call for Sentinel 2 from CODE-DE: +/// ```bash +/// cargo run --bin geoengine-cli stac-import \ +/// --verbose \ +/// --limit 100 \ +/// --bbox "8.766 50.802 8.767 50.803" \ +/// --time-start 2020-08-01T00:00:00Z \ +/// --time-end 2020-08-31T23:59:59Z \ +/// --stac-url https://stac.nsiscloud.polsa.gov.pl/v1 \ +/// --s3-endpoint eodata.nsiscloud.polsa.gov.pl \ +/// --s3-access-key XXX \ +/// --s3-secret-key YYY \ +/// --file-types jp2 +/// ``` pub async fn stac_import(params: StacImport) -> Result<(), anyhow::Error> { let mut importer = StacImporter::new(params).await?; importer.run().await @@ -159,8 +232,10 @@ struct StacImporter { client: reqwest::Client, session_id: String, bands: HashMap>, + known_datasets: HashSet, created_datasets: HashSet, - time_range: TimeInterval, + items_processed_total: u64, + import_start_time: Instant, } impl StacImporter { @@ -177,24 +252,23 @@ impl StacImporter { .context("Failed to scan collection")?; Ok(Self { - time_range: TimeInterval::new( - DateTime::from_str(¶ms.time_start)?, - DateTime::from_str(¶ms.time_end)?, - )?, params, client, session_id, bands, + known_datasets: HashSet::new(), created_datasets: HashSet::new(), + items_processed_total: 0, + import_start_time: Instant::now(), }) } async fn run(&mut self) -> Result<(), anyhow::Error> { if self.params.verbose { - println!("Scanned collection, found bands for data type and resolution:"); + println!("[INFO] Scanned collection, found bands for data type and resolution:"); for (partial_dataset_key, bands) in &self.bands { println!( - " {:?}, {}: {}", + "[INFO] {:?}, {}: {}", partial_dataset_key.data_type, partial_dataset_key.resolution, bands @@ -226,7 +300,7 @@ impl StacImporter { self.process_pages(pages).await?; if self.params.verbose { - println!("Dataset tiles added successfully"); + println!("[INFO] Dataset tiles added successfully"); } self.create_collections_and_layers().await?; @@ -247,13 +321,13 @@ impl StacImporter { return None; } - println!("Fetching page: {state:?}"); + println!("[DEBUG] Fetching page: {state:?}"); let start = Instant::now(); let result = query_item_collection(&client, &state).await; let elapsed = start.elapsed(); - println!("Page fetched in {:.2}s", elapsed.as_secs_f64()); + println!("[DEBUG] Page fetched in {:.2}s", elapsed.as_secs_f64()); match result { Ok((item_collection, new_state)) => { @@ -265,13 +339,12 @@ impl StacImporter { } Err(e) => { // TODO: abort or retry - println!("Error fetching page: {e:#}"); + println!("[ERROR]Error fetching page: {e:#}"); Some((Err(e), (client, QueryState::Finished))) } } }, ); - page_stream .map(|result| async move { result }) .buffered(prefetch_buffer) @@ -285,13 +358,18 @@ impl StacImporter { futures::pin_mut!(buffered_stream); while let Some(result) = buffered_stream.next().await { let item_collection = result?; + let number_returned = item_collection.items.len() as u64; - let dataset_tiles = self.process_items(item_collection).await?; + let dataset_tiles = self.process_items(item_collection.clone()).await?; for (dataset_key, tiles) in &dataset_tiles { let dataset_name = dataset_key.dataset_name(&self.params.stac_collection); - println!("Adding {} tiles to dataset {}", tiles.len(), dataset_name,); + println!( + "[DEBUG] Adding {} tiles to dataset {}", + tiles.len(), + dataset_name, + ); let response = retry_with_backoff( || async { @@ -322,7 +400,8 @@ impl StacImporter { } } - let _ = self.print_progress(&dataset_tiles); + self.items_processed_total += number_returned; + self.print_progress(&item_collection); } Ok(()) @@ -340,18 +419,21 @@ impl StacImporter { if let Err(err) = self.process_item(item, &mut dataset_tiles).await && self.params.verbose { - println!("Skipping item {item_id}: {err:#}"); + println!("[ERROR] Skipping item {item_id}: {err:#}"); } } Ok(dataset_tiles) } + #[allow(clippy::too_many_lines)] async fn process_item( &mut self, item: stac::Item, dataset_tiles: &mut HashMap>, ) -> Result<(), anyhow::Error> { + let stac_extension_versions = StacExtensionVersions::try_from(&item)?; + let datetime = item .properties .datetime @@ -367,15 +449,15 @@ impl StacImporter { let date_without_time: DateTime = date_without_time.into(); let time: TimeInstance = date_without_time.into(); - let epsg = item - .properties - .additional_fields - .get("proj:epsg") - .and_then(serde_json::Value::as_u64) - .context("Missing proj:epsg in item properties")? as u32; + // item-level epsg code (may be overwritten by asset) + let item_epsg_code = epsg_code_from_item(&item, stac_extension_versions.projection)?; // Filter by EPSG code if epsgs parameter is provided - if !self.params.epsgs.is_empty() && !self.params.epsgs.contains(&epsg) { + // TODO: also filter when epsg code is at asset and not item level + if let Some(epsg) = item_epsg_code + && !self.params.epsgs.is_empty() + && !self.params.epsgs.contains(&epsg) + { anyhow::bail!("EPSG {epsg} not in filter list"); } @@ -397,24 +479,82 @@ impl StacImporter { // 0 // }; - // TODO: make z-index computation configurable - let z_index = item - .properties - .updated - .ok_or(anyhow::anyhow!( - "Missing updated datetime in item properties" - )) - .and_then(|updated| { - chrono::DateTime::parse_from_rfc3339(&updated) - .context("Failed to parse updated datetime") - })? - .timestamp_millis(); + // note: we need special handling here because some fields are properly mappen by stac crate while others are only available in additional_fields + // TODO: properly handle all values mapped by stac crate + // TODO: support non-datetime properties for z-index + let z_index = match self.params.z_index_property_name.as_deref() { + Some("updated") => { + // use `updated` datetime as z-index, so that newer updates are on top of older ones + item.properties + .updated + .ok_or(anyhow::anyhow!( + "Missing updated datetime in item properties" + )) + .and_then(|updated| { + chrono::DateTime::parse_from_rfc3339(&updated) + .context("Failed to parse updated datetime") + })? + .timestamp_millis() + } + Some(property) => item + .properties + .additional_fields + .get(property) + .ok_or(anyhow::anyhow!( + "Missing z index property '{property}' in item additional fields" + )) + .and_then(|v| { + v.as_str().ok_or(anyhow::anyhow!( + "Z index property '{property}' is not a string" + )) + }) + .and_then(|updated| { + chrono::DateTime::parse_from_rfc3339(updated) + .context(format!("Failed to parse '{property}' datetime")) + })? + .timestamp_millis(), + _ => 0, + }; for (asset_key, asset) in &item.assets { - match self - .process_item_asset(asset_key, asset, epsg, time, z_index) - .await - { + if !matches_selected_file_types(asset.r#type.as_deref(), &self.params.file_types) { + println!( + "[DEBUG] skipping {asset_key}: unsupported asset type: {:?}", + asset.r#type + ); + continue; + } + + let tiles = match (&item.version, stac_extension_versions) { + ( + &stac::Version::v1_0_0, + StacExtensionVersions { + projection: StacExtensionMajorVersion::V1, + raster: StacExtensionMajorVersion::V1, + eo: StacExtensionMajorVersion::V1, + }, + ) => { + self.process_item_asset_v1_0_0(asset, item_epsg_code, time, z_index) + .await + } + ( + &stac::Version::v1_1_0, + StacExtensionVersions { + projection: StacExtensionMajorVersion::V2, + raster: StacExtensionMajorVersion::V2, + eo: StacExtensionMajorVersion::V2, + }, + ) => { + self.process_item_asset_v1_1_0(asset, item_epsg_code, time, z_index) + .await + } + _ => Err(anyhow::anyhow!( + "Unsupported STAC version or extension versions: {:?}, {stac_extension_versions:?}", + item.version + )), + }; + + match tiles { Ok(tiles) => { for (dataset_key, tile) in tiles { dataset_tiles.entry(dataset_key).or_default().push(tile); @@ -422,7 +562,10 @@ impl StacImporter { } Err(err) => { if self.params.verbose { - println!("Skipping asset {asset_key} of item {}: {err:#}", item.id); + println!( + "[ERROR] Skipping asset {asset_key} of item {}: {err:#}", + item.id + ); } } } @@ -431,36 +574,22 @@ impl StacImporter { Ok(()) } - async fn process_item_asset( + async fn process_item_asset_v1_0_0( &mut self, - asset_key: &str, asset: &Asset, - epsg: u32, + epsg: Option, time: TimeInstance, z_index: i64, ) -> Result, anyhow::Error> { - if asset.r#type - != Some("image/tiff; application=geotiff; profile=cloud-optimized".to_string()) - { - anyhow::bail!("non-geotiff asset"); - } - let geo_transform = geo_transform_from_fields(&asset.additional_fields) .ok_or(anyhow::anyhow!("missing proj:transform"))?; - let data_type = if let Ok(data_type) = data_type_from_asset(asset) { - data_type - } else if self.params.missing_bands_handling - && asset - .roles - .iter() - .any(|roles| roles.contains(&"visual".to_string())) - { - // if no data type can be determined but asset has role visual, assume u8 data type with one band per visual asset - RasterDataType::U8 - } else { - anyhow::bail!("Failed to determine data type from asset"); - }; + let data_type = data_type_from_asset(stac::Version::v1_0_0, asset) + .context("Failed to determine data type from asset")?; + + let epsg = epsg_code_from_fields(StacExtensionMajorVersion::V1, &asset.additional_fields) + .or(epsg) + .ok_or(anyhow::anyhow!("Failed to determine EPSG code from asset"))?; let dataset_key = DatasetKey { epsg, @@ -468,75 +597,205 @@ impl StacImporter { resolution: geo_transform.x_pixel_size().into(), }; - if !self.created_datasets.contains(&dataset_key) { - // create dataset on-the-fly - // TODO: if dataset already exists on server, skip creation, but check compatibility? - self.create_dataset(&dataset_key, geo_transform) + let partial_key = PartialDatasetKey { + data_type: dataset_key.data_type, + resolution: dataset_key.resolution, + }; + + if !self.bands.contains_key(&partial_key) { + if self.params.verbose { + println!( + "[DEBUG] skipping asset {}: no scanned dataset definition for {:?}", + asset.href, dataset_key + ); + } + return Ok(Vec::new()); + } + + if !self.known_datasets.contains(&dataset_key) { + let dataset_name = dataset_key.dataset_name(&self.params.stac_collection); + let dataset_exists = self + .dataset_exists(&dataset_name) .await - .context(format!("failed to create dataset {dataset_key:?}"))?; + .with_context(|| format!("failed to check dataset existence for {dataset_name}"))?; - self.created_datasets.insert(dataset_key.clone()); + if !dataset_exists { + self.create_dataset(&dataset_key, geo_transform) + .await + .context(format!("failed to create dataset {dataset_key:?}"))?; + self.created_datasets.insert(dataset_key.clone()); + } - debug_assert!( - self.created_datasets.contains(&dataset_key), - "Dataset should have been marked as created" - ); + self.known_datasets.insert(dataset_key.clone()); } - let eo_bands: Result, _> = asset - .additional_fields - .get("eo:bands") - .ok_or(anyhow::anyhow!("Missing eo:bands in asset")) - .and_then(|eo_bands| { - serde_json::from_value(eo_bands.clone()) - .map_err(|e| anyhow::anyhow!("invalid eo:bands: {e}")) - }); + let eo_bands = asset.additional_fields.get("eo:bands"); + let band_count = if let Some(eo_bands_value) = eo_bands { + let parsed_eo_bands: Vec = serde_json::from_value(eo_bands_value.clone()) + .map_err(|e| anyhow::anyhow!("invalid eo:bands: {e}"))?; + Some(parsed_eo_bands) + } else { + None + }; + + let dataset_bands = self + .bands + .get(&partial_key) + .ok_or(anyhow::anyhow!("unknown dataset key: {dataset_key:?}"))?; + + let processor = AssetBandProcessor { + asset, + params: &self.params, + time, + geo_transform, + dataset_bands, + z_index, + }; - let eo_bands = match eo_bands { - Ok(eo_bands) => eo_bands, - Err(_) if self.params.missing_bands_handling => { - handle_missing_eo_bands_for_asset(asset_key, asset)? + let mut tiles = Vec::new(); + match band_count { + Some(eo_bands) => { + // Asset has both raster:bands and eo:bands + for (band_idx, eo_band) in eo_bands.iter().enumerate() { + let band_name = + v1_0_0_band_name(asset.title.as_deref(), Some(eo_band), eo_bands.len()); + + let tile = processor + .process_band_v1_0_0(band_idx, &band_name) + .context(format!("Failed to process band {}", eo_band.name))?; + tiles.push((dataset_key.clone(), tile)); + } } - Err(e) => { - return Err(e); + None => { + // Asset has only raster:bands (no eo:bands) - single-band asset + let band_name = v1_0_0_band_name(asset.title.as_deref(), None, 1); + let tile = processor + .process_band_v1_0_0(0, &band_name) + .context("Failed to process single-band asset")?; + tiles.push((dataset_key.clone(), tile)); } + } + + Ok(tiles) + } + + async fn process_item_asset_v1_1_0( + &mut self, + asset: &Asset, + epsg: Option, + time: TimeInstance, + z_index: i64, + ) -> Result, anyhow::Error> { + let geo_transform = geo_transform_from_fields(&asset.additional_fields) + .ok_or(anyhow::anyhow!("missing proj:transform"))?; + + let data_type = data_type_from_asset(stac::Version::v1_1_0, asset) + .context("Failed to determine data type from asset")?; + + let epsg = epsg_code_from_fields(StacExtensionMajorVersion::V2, &asset.additional_fields) + .or(epsg) + .ok_or(anyhow::anyhow!("Failed to determine EPSG code from asset"))?; + + let dataset_key = DatasetKey { + epsg, + data_type, + resolution: geo_transform.x_pixel_size().into(), + }; + + let partial_key = PartialDatasetKey { + data_type: dataset_key.data_type, + resolution: dataset_key.resolution, }; + if !self.bands.contains_key(&partial_key) { + if self.params.verbose { + println!( + "[DEBUG] skipping asset {}: no scanned dataset definition for {:?}", + asset.href, dataset_key + ); + } + return Ok(Vec::new()); + } + + if !self.known_datasets.contains(&dataset_key) { + let dataset_name = dataset_key.dataset_name(&self.params.stac_collection); + let dataset_exists = self + .dataset_exists(&dataset_name) + .await + .with_context(|| format!("failed to check dataset existence for {dataset_name}"))?; + + if !dataset_exists { + self.create_dataset(&dataset_key, geo_transform) + .await + .context(format!("failed to create dataset {dataset_key:?}"))?; + self.created_datasets.insert(dataset_key.clone()); + } + + self.known_datasets.insert(dataset_key.clone()); + } + + let asset_bands = band_names_from_asset_v1_1_0(asset)?; + let dataset_bands = self .bands - .get(&PartialDatasetKey { - data_type: dataset_key.data_type, - resolution: dataset_key.resolution, - }) + .get(&partial_key) .ok_or(anyhow::anyhow!("unknown dataset key: {dataset_key:?}"))?; - // if multiple bands for asset, prefix band names with asset key - let prefix = if eo_bands.len() > 1 { - format!("{asset_key}_",) - } else { - String::new() - }; - let processor = AssetBandProcessor { asset, + params: &self.params, time, geo_transform, dataset_bands, - prefix: &prefix, z_index, }; let mut tiles = Vec::new(); - for (band_idx, eo_band) in eo_bands.iter().enumerate() { + for (band_idx, band_name) in asset_bands.iter().enumerate() { let tile = processor - .process_band(band_idx, eo_band) - .context(format!("Failed to process band {}", eo_band.name))?; + .process_band_v1_1_0(band_idx, band_name) + .context(format!("Failed to process band {band_name}"))?; tiles.push((dataset_key.clone(), tile)); } Ok(tiles) } + async fn dataset_exists(&self, dataset_name: &str) -> Result { + let response = retry_with_backoff( + || async { + self.client + .get(format!( + "{}/dataset/{}", + self.params.geo_engine_url, dataset_name + )) + .header("Authorization", format!("Bearer {}", self.session_id)) + .send() + .await + }, + &format!("Check dataset existence for {dataset_name}"), + ) + .await + .context("Failed to send dataset existence request")?; + + if response.status().is_success() { + return Ok(true); + } + + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + if status == reqwest::StatusCode::BAD_REQUEST + && let Ok(error_response) = serde_json::from_str::(&body) + { + // The dataset API may return a generic CannotLoadDataset message for unknown names. + if error_response.error == "CannotLoadDataset" { + return Ok(false); + } + } + + anyhow::bail!("Failed to check dataset '{dataset_name}': HTTP {status}: {body}"); + } + #[allow(clippy::too_many_lines)] async fn create_dataset( &self, @@ -554,7 +813,7 @@ impl StacImporter { .context(format!("Failed to get bands for dataset: {dataset_key:?}"))?; let create_dataset = CreateDataset { - data_path: DataPath::Volume(VolumeName(EXTERNAL_VOLUME_NAME.to_string())), + data_path: DataPath::Volume(VolumeName(self.params.volume_name.clone())), definition: DatasetDefinition { properties: AddDataset { name: Some( @@ -626,6 +885,14 @@ impl StacImporter { .await .context("Failed to send dataset creation request")?; + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + anyhow::bail!( + "Failed to create dataset {dataset_name_str}: HTTP {status}: {error_text}" + ); + } + let dataset_name = if let Ok(json) = response.json::().await { if let Some(id) = json.get("datasetName").and_then(|v| v.as_str()) { // println!( @@ -692,44 +959,52 @@ impl StacImporter { Ok(()) } - fn print_progress( - &self, - dataset_tiles: &HashMap>, - ) -> anyhow::Result<()> { - let min_date = dataset_tiles - .values() - .flatten() - .map(|tile| tile.time.start) - .min(); - - if let Some(min_date) = min_date { - let start_millis = self.time_range.start().inner() as f64; - let end_millis = self.time_range.end().inner() as f64; - let current_millis = min_date.inner() as f64; - - // Items are received in descending order (from end to start) - let progress = ((end_millis - current_millis) / (end_millis - start_millis) * 100.0) + fn print_progress(&self, item_collection: &stac::ItemCollection) { + let elapsed_secs = self.import_start_time.elapsed().as_secs_f64(); + let items_per_sec = if elapsed_secs > 0.0 { + self.items_processed_total as f64 / elapsed_secs + } else { + 0.0 + }; + + if let Some(number_matched) = item_collection + .additional_fields + .get("numberMatched") + .and_then(serde_json::Value::as_u64) + { + let progress = (self.items_processed_total as f64 / number_matched as f64 * 100.0) .clamp(0.0, 100.0); + let remaining = number_matched.saturating_sub(self.items_processed_total); + let eta_secs = if items_per_sec > 0.0 { + remaining as f64 / items_per_sec + } else { + f64::INFINITY + }; + let eta_str = if eta_secs.is_finite() { + format_duration(eta_secs as u64) + } else { + "unknown".to_string() + }; + println!( - "[{:.1}%] Processed items down to date: {} in range {}/{} ", - progress, - DateTime::try_from(geoengine_datatypes::primitives::TimeInstance::from( - min_date - ))?, - DateTime::try_from(self.time_range.start())?, - DateTime::try_from(self.time_range.end())?, + "[{:.1}%] Processed {}/{} items ({:.1} items/s, ETA: {})", + progress, self.items_processed_total, number_matched, items_per_sec, eta_str + ); + } else if !item_collection.items.is_empty() { + // If number_matched is not available, just show the count and rate + println!( + "[INFO] Processed {} items ({:.1} items/s)", + self.items_processed_total, items_per_sec ); } - - Ok(()) } async fn create_collections_and_layers(&self) -> anyhow::Result<()> { // Create root collection for STAC collection let root_collection_id = self .create_layer_collection( - &INTERNAL_LAYER_DB_ROOT_COLLECTION_ID.to_string(), + &LayerCollectionId(INTERNAL_LAYER_DB_ROOT_COLLECTION_ID.to_string()), &self.params.stac_collection, &format!( "{} datasets imported from STAC", @@ -806,10 +1081,23 @@ impl StacImporter { async fn create_layer_collection( &self, - parent_id: &str, + parent_id: &LayerCollectionId, name: &str, description: &str, - ) -> anyhow::Result { + ) -> anyhow::Result { + // Reuse existing collection from previous runs if there is a child collection with the same name. + if let Some(existing_id) = self + .find_child_collection_id_by_name(parent_id, name) + .await + .context("Failed to query existing child collections")? + { + if self.params.verbose { + println!("Found existing layer collection '{name}' with id {existing_id}"); + } + + return Ok(existing_id); + } + let add_collection = AddLayerCollection { name: name.to_string(), description: description.to_string(), @@ -841,17 +1129,60 @@ impl StacImporter { if self.params.verbose { println!( - "Created layer collection '{}' with id {}", + "[DEBUG] Created layer collection '{}' with id {}", name, response.id.0 ); } - Ok(response.id.0) + Ok(response.id) + } + + async fn find_child_collection_id_by_name( + &self, + parent_id: &LayerCollectionId, + child_name: &str, + ) -> anyhow::Result> { + let mut offset: u32 = 0; + let limit: u32 = 20; + + loop { + let response = retry_with_backoff( + || async { + self.client + .get(format!( + "{}/layers/collections/{}/{}", + self.params.geo_engine_url, INTERNAL_PROVIDER_ID, parent_id + )) + .query(&[("offset", offset), ("limit", limit)]) + .header("Authorization", format!("Bearer {}", self.session_id)) + .send() + .await? + .json::() + .await + }, + &format!("List child collections of {parent_id}"), + ) + .await?; + + for item in &response.items { + if let CollectionItem::Collection(collection) = item + && collection.name == child_name + { + return Ok(Some(collection.id.collection_id.clone())); + } + } + + if response.items.len() < limit as usize { + return Ok(None); + } + + offset += limit; + } } async fn create_layer( &self, - collection_id: &str, + collection_id: &LayerCollectionId, dataset_key: &DatasetKey, ) -> anyhow::Result { let dataset_name = dataset_key.dataset_name(&self.params.stac_collection); @@ -907,7 +1238,7 @@ impl StacImporter { self.share_layer(&response.id).await?; if self.params.verbose { - println!("Created layer '{layer_name}' in collection {collection_id}"); + println!("[DEBUG] Created layer '{layer_name}' in collection {collection_id}"); } Ok(response.id) @@ -915,7 +1246,7 @@ impl StacImporter { async fn add_existing_layer_to_collection( &self, - collection_id: &str, + collection_id: &LayerCollectionId, layer_id: &LayerId, ) -> anyhow::Result<()> { retry_with_backoff( @@ -935,7 +1266,10 @@ impl StacImporter { .context("Failed to add existing layer to collection")?; if self.params.verbose { - println!("Added layer {} to collection {}", layer_id.0, collection_id); + println!( + "[DEBUG] Added layer {} to collection {}", + layer_id.0, collection_id + ); } Ok(()) @@ -943,7 +1277,7 @@ impl StacImporter { async fn create_hierarchy_with_branches( &self, - root_id: &str, + root_id: &LayerCollectionId, layer_ids: &HashMap, first_attr: Attribute, second_attrs: &[Attribute; 2], @@ -1100,6 +1434,79 @@ impl StacImporter { } } +fn format_duration(secs: u64) -> String { + if secs < 60 { + format!("{secs}s") + } else if secs < 3600 { + format!("{}m{}s", secs / 60, secs % 60) + } else { + format!("{}h{}m{}s", secs / 3600, (secs % 3600) / 60, secs % 60) + } +} + +fn epsg_code_from_fields( + proj_extension_version: StacExtensionMajorVersion, + fields: &serde_json::Map, +) -> Option { + let proj_epsg = fields.get("proj:epsg").and_then(|value| { + value + .as_u64() + .map(|code| code as u32) + .or_else(|| value.as_str().and_then(|code| code.parse::().ok())) + }); + + let proj_code = fields + .get("proj:code") + .and_then(serde_json::Value::as_str) + .and_then(parse_epsg_from_proj_code); + + match proj_extension_version { + StacExtensionMajorVersion::V1 => proj_epsg.or(proj_code), + StacExtensionMajorVersion::V2 => proj_code.or(proj_epsg), + } +} + +#[allow(clippy::unnecessary_wraps)] +fn epsg_code_from_item( + item: &stac::Item, + proj_extension_version: StacExtensionMajorVersion, +) -> Result, anyhow::Error> { + let from_additional = + epsg_code_from_fields(proj_extension_version, &item.properties.additional_fields); + if from_additional.is_some() { + return Ok(from_additional); + } + + let Some(properties) = serde_json::to_value(item) + .ok() + .and_then(|value| value.get("properties").cloned()) + .and_then(|value| value.as_object().cloned()) + else { + return Ok(None); + }; + + let from_properties = epsg_code_from_fields(proj_extension_version, &properties); + if from_properties.is_some() { + return Ok(from_properties); + } + + let fallback_version = match proj_extension_version { + StacExtensionMajorVersion::V1 => StacExtensionMajorVersion::V2, + StacExtensionMajorVersion::V2 => StacExtensionMajorVersion::V1, + }; + + Ok(epsg_code_from_fields(fallback_version, &properties)) +} + +fn parse_epsg_from_proj_code(code: &str) -> Option { + if let Some(code) = code.strip_prefix("EPSG:") { + return code.parse::().ok(); + } + + // e.g. http://www.opengis.net/def/crs/EPSG/0/32632 + code.rsplit('/').next()?.parse::().ok() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Attribute { DataType, @@ -1141,43 +1548,27 @@ enum AttributeValue { Epsg(u32), } -fn handle_missing_eo_bands_for_asset( - asset_key: &str, - asset: &Asset, -) -> anyhow::Result> { - let raster_bands: Vec = asset - .additional_fields - .get("raster:bands") - .ok_or(anyhow::anyhow!("Missing raster:bands")) - .and_then(|bands| { - serde_json::from_value(bands.clone()) - .map_err(|e| anyhow::anyhow!("invalid raster:bands: {e}")) - })?; - - handle_missing_eo_bands(asset_key, &raster_bands) -} - struct AssetBandProcessor<'a> { asset: &'a Asset, + params: &'a StacImport, time: TimeInstance, geo_transform: GeoTransform, dataset_bands: &'a [RasterBandDescriptor], - prefix: &'a str, z_index: i64, } impl AssetBandProcessor<'_> { - fn process_band(&self, band_idx: usize, eo_band: &EoBand) -> anyhow::Result { - let band_name = format!("{}{}", self.prefix, eo_band.name); - + fn process_band_v1_0_0( + &self, + band_idx: usize, + band_name: &str, + ) -> anyhow::Result { let band_index = self .dataset_bands .iter() - .position(|b| b.name == band_name.as_str()) + .position(|b| b.name == band_name) .ok_or(anyhow::anyhow!("unknown band: {band_name}"))?; - let tile_file = self.asset.href.clone(); - let proj_shape = self .asset .additional_fields @@ -1213,6 +1604,10 @@ impl AssetBandProcessor<'_> { // self.date, self.asset_key, self.asset.href // ); + let file_path = gdal_file_path(self.asset)?; + + let gdal_config_options = self.gdal_options_for_file_path(&file_path)?; + let tile = AddDatasetTile { time: TimeInterval::new(self.time, self.time + i64::from(24 * 60 * 60 * 1000)) // TODO: make time validity configurable .context("Failed to create time interval")? @@ -1221,7 +1616,7 @@ impl AssetBandProcessor<'_> { band: band_index as u32, z_index: self.z_index, params: GdalDatasetParameters { - file_path: format!("/vsicurl/{tile_file}").into(), + file_path: file_path.into(), rasterband_channel: band_idx + 1, // gdal channels are 1-based geo_transform: self.geo_transform.into(), width, @@ -1230,44 +1625,178 @@ impl AssetBandProcessor<'_> { no_data_value: None, properties_mapping: None, gdal_open_options: None, - gdal_config_options: None, + gdal_config_options, allow_alphaband_as_mask: false, }, }; Ok(tile) } -} -async fn query_item_collection( - client: &reqwest::Client, - query_state: &QueryState, -) -> Result<(stac::ItemCollection, QueryState), anyhow::Error> { - match query_state { - QueryState::FirstPage { - query_url, - query_params, - } => { - let item_collection: stac::ItemCollection = retry_with_backoff( - || async { - client - .get(query_url) - .query(&query_params) - .send() - .await? - .json() - .await - }, - "Query STAC first page", - ) - .await?; + fn gdal_options_for_file_path( + &self, + file_path: &GdalFilePath, + ) -> Result>, anyhow::Error> { + let gdal_open_options = if let GdalFilePath::S3(_) = *file_path { + // TODO: allow skipping s3 assets on missing credentials? + let s3_endpoint = self.params.s3_endpoint.as_ref().ok_or(anyhow::anyhow!( + "S3 endpoint must be provided for S3 assets" + ))?; + let s3_access_key = self.params.s3_access_key.as_ref().ok_or(anyhow::anyhow!( + "S3 access key must be provided for S3 assets" + ))?; + let s3_secret_key = self.params.s3_secret_key.as_ref().ok_or(anyhow::anyhow!( + "S3 secret key must be provided for S3 assets" + ))?; + + // for old gdal version s3 endpoint may not include the protocol + if s3_endpoint.starts_with("http://") || s3_endpoint.starts_with("https://") { + anyhow::bail!( + "S3 endpoint should not include protocol (http/https), got: {s3_endpoint}" + ); + } - let new_query_state = if let Some(next_link) = - item_collection.links.iter().find(|link| link.rel == "next") - { - QueryState::NextPage { - next_url: next_link.href.clone(), - } + Some(vec![ + StringPair(("AWS_S3_ENDPOINT".to_string(), s3_endpoint.clone())), + StringPair(("AWS_ACCESS_KEY_ID".to_string(), s3_access_key.clone())), + StringPair(("AWS_SECRET_ACCESS_KEY".to_string(), s3_secret_key.clone())), + // StringPair(("AWS_HTTPS".to_string(), "YES".to_string())), // TODO: make configurable? + StringPair(("AWS_VIRTUAL_HOSTING".to_string(), "FALSE".to_string())), // TODO: make configurable? + ]) + } else { + None + }; + Ok(gdal_open_options) + } + + fn process_band_v1_1_0( + &self, + band_idx: usize, + band_name: &str, + ) -> anyhow::Result { + let band_index = self + .dataset_bands + .iter() + .position(|b| b.name == band_name) + .ok_or(anyhow::anyhow!("unknown band: {band_name}"))?; + + let proj_shape = self + .asset + .additional_fields + .get("proj:shape") + .ok_or(anyhow::anyhow!("missing proj:shape"))?; + + let proj_shape = proj_shape + .as_array() + .ok_or(anyhow::anyhow!("proj:shape is not an array"))?; + + let (height, width) = ( + proj_shape[0] + .as_u64() + .ok_or(anyhow::anyhow!("proj:shape[0] is not a u64"))? as usize, + proj_shape[1] + .as_u64() + .ok_or(anyhow::anyhow!("proj:shape[1] is not a u64"))? as usize, + ); + + let grid_bounds = geoengine_datatypes::raster::GridBoundingBox2D::new( + GridIdx2D { x_idx: 0, y_idx: 0 }, + GridIdx2D { + x_idx: (width - 1) as isize, + y_idx: (height - 1) as isize, + }, + ) + .context("Failed to create grid bounds from proj:shape")?; + + let spatial_partition = self.geo_transform.grid_to_spatial_bounds(&grid_bounds); + + // println!( + // "Importing tile: date: {}, band: {}, href: {}", + // self.date, self.asset_key, self.asset.href + // ); + + let file_path = gdal_file_path(self.asset)?; + + let gdal_config_options = self.gdal_options_for_file_path(&file_path)?; + + let tile = AddDatasetTile { + time: TimeInterval::new(self.time, self.time + i64::from(24 * 60 * 60 * 1000)) // TODO: make time validity configurable + .context("Failed to create time interval")? + .into(), + spatial_partition: spatial_partition.into(), + band: band_index as u32, + z_index: self.z_index, + params: GdalDatasetParameters { + file_path: file_path.into(), + rasterband_channel: band_idx + 1, // gdal channels are 1-based + geo_transform: self.geo_transform.into(), + width, + height, + file_not_found_handling: crate::api::model::operators::FileNotFoundHandling::Error, + no_data_value: None, + properties_mapping: None, + gdal_open_options: None, + gdal_config_options, + allow_alphaband_as_mask: false, + }, + }; + + Ok(tile) + } +} + +fn gdal_file_path(asset: &Asset) -> anyhow::Result { + if asset.href.starts_with("http") { + Ok(GdalFilePath::Http(format!("/vsicurl/{}", asset.href))) + } else if let Some(s3_url) = asset.href.strip_prefix("s3://") { + Ok(GdalFilePath::S3(format!("/vsis3/{s3_url}"))) + } else { + anyhow::bail!("Unsupported asset href format for GDAL: {}", asset.href); + } +} + +enum GdalFilePath { + Http(String), + S3(String), +} + +impl GdalFilePath { + fn into(self) -> PathBuf { + match self { + GdalFilePath::Http(path) | GdalFilePath::S3(path) => PathBuf::from(path), + } + } +} + +async fn query_item_collection( + client: &reqwest::Client, + query_state: &QueryState, +) -> Result<(stac::ItemCollection, QueryState), anyhow::Error> { + match query_state { + QueryState::FirstPage { + query_url, + query_params, + } => { + let item_collection: stac::ItemCollection = retry_with_backoff( + || async { + client + .get(query_url) + .query(&query_params) + .send() + .await? + .json() + .await + }, + "Query STAC first page", + ) + .await?; + + let new_query_state = if let Some(next_link) = + item_collection.links.iter().find(|link| link.rel == "next") + { + QueryState::NextPage { + next_url: next_link.href.clone(), + } } else { QueryState::Finished }; @@ -1298,24 +1827,41 @@ async fn query_item_collection( } fn create_query_params(params: &StacImport) -> Vec<(String, String)> { - let mut query_params = vec![ - ( + let mut query_params: Vec<(String, String)> = Vec::new(); + + // Add bbox if provided + if let Some(bbox) = ¶ms.bbox + && bbox.len() == 4 + { + query_params.push(( "bbox".to_owned(), format!( "{},{},{},{}", // array-brackets are not used in standard but required here for unknkown reason - params.bbox[0], params.bbox[1], params.bbox[2], params.bbox[3] + bbox[0], bbox[1], bbox[2], bbox[3] ), - ), // TODO: order coordinates depending on projection - ( + )); // TODO: order coordinates depending on projection + } + + if params.time_start.is_some() || params.time_end.is_some() { + query_params.push(( "datetime".to_owned(), - format!("{}/{}", params.time_start, params.time_end), - ), - ]; + format!( + "{}/{}", + params.time_start.as_deref().unwrap_or(""), + params.time_end.as_deref().unwrap_or("") + ), + )); + } if let Some(limit) = params.limit { query_params.push(("limit".to_owned(), limit.to_string())); } + // TODO: only filter if server supports it? check via `conformance` field in API? + if params.filter_item_fields { + query_params.push(("fields".to_owned(), STAC_ITEMS_FIELDS_FILTER.join(","))); + } + query_params } @@ -1403,10 +1949,120 @@ impl From for crate::api::model::datatypes::GeoTransform { #[derive(Debug, Deserialize)] struct EoBand { name: String, + #[serde(default)] + common_name: Option, // description: String, // ... } +fn normalize_label(value: &str) -> String { + value + .trim() + .to_lowercase() + .split_whitespace() + .collect::>() + .join("_") +} + +fn title_fallback_label(title: Option<&str>) -> String { + if let Some(title) = title { + // Prefer concise acronym-like labels in parentheses, e.g. "Scene classification map (SCL)". + if let (Some(start), Some(end)) = (title.rfind('('), title.rfind(')')) + && start < end + { + let short = title[start + 1..end].trim(); + if !short.is_empty() + && short.len() <= 32 + && short + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return short.to_lowercase(); + } + } + + let normalized = normalize_label(title); + if !normalized.is_empty() { + return normalized; + } + } + + "band".to_string() +} + +fn rededge_variant_from_metadata(eo_name: &str, title: Option<&str>) -> Option<&'static str> { + let eo = eo_name.to_lowercase(); + let title_lower = title.map(str::to_lowercase).unwrap_or_default(); + + if eo.contains("b05") + || eo.contains("band_5") + || title_lower.contains("band 5") + || title_lower.contains("b05") + { + return Some("rededge1"); + } + if eo.contains("b06") + || eo.contains("band_6") + || title_lower.contains("band 6") + || title_lower.contains("b06") + { + return Some("rededge2"); + } + if eo.contains("b07") + || eo.contains("band_7") + || title_lower.contains("band 7") + || title_lower.contains("b07") + { + return Some("rededge3"); + } + + None +} + +fn v1_0_0_band_name(title: Option<&str>, eo_band: Option<&EoBand>, band_count: usize) -> String { + let eo_name = eo_band.and_then(|band| { + let eo_name = band.name.to_lowercase(); + let common_name = band.common_name.as_ref().map(|name| name.to_lowercase()); + + match common_name.as_deref() { + // `rededge` is used for multiple Sentinel-2 bands (B05/B06/B07). + // Keep stable, unique names to avoid band collisions. + Some("rededge") => rededge_variant_from_metadata(&eo_name, title) + .map(std::string::ToString::to_string) + .or_else(|| Some(format!("rededge[{eo_name}]"))), + Some(common_name) => Some(common_name.to_string()), + None => Some(eo_name), + } + }); + + if band_count > 1 { + let asset_label = title_fallback_label(title); + let eo_name = eo_name.unwrap_or_else(|| "band".to_string()); + return format!("{asset_label}[{eo_name}]"); + } + + if let Some(eo_name) = eo_name { + return eo_name; + } + + title_fallback_label(title) +} + +fn is_cog_media_type(media_type: Option<&str>) -> bool { + media_type == Some("image/tiff; application=geotiff; profile=cloud-optimized") +} + +fn is_jp2_media_type(media_type: Option<&str>) -> bool { + media_type == Some("image/jp2") +} + +fn matches_selected_file_types(media_type: Option<&str>, file_types: &[ImportFileType]) -> bool { + file_types.iter().any(|file_type| match file_type { + ImportFileType::Cog => is_cog_media_type(media_type), + ImportFileType::Jp2 => is_jp2_media_type(media_type), + }) +} + async fn scan_collection( params: &StacImport, client: &reqwest::Client, @@ -1430,71 +2086,105 @@ async fn scan_collection( // create datasets by grouping items by (epsg, data_type, resolution/grid) // because datasets must have uniform epsg, data_type, resolution + let stac_extension_versions = StacExtensionVersions::try_from(&collection)?; + let mut dataset_bands: HashMap> = HashMap::new(); for (asset_key, asset) in &collection.item_assets { - if let Err(err) = scan_item_asset( - asset_key, - asset, - &mut dataset_bands, - params.missing_bands_handling, - ) - .context(format!("Failed to scan item asset {asset_key}")) - && params.verbose - { - println!("Skipping asset {asset_key}: {err:#}"); + if !matches_selected_file_types(asset.r#type.as_deref(), ¶ms.file_types) { + println!( + "[DEBUG] Skipping asset {asset_key} with unsupported media type: {:?}", + asset.r#type + ); + continue; } + + let asset_bands = match (&collection.version, stac_extension_versions) { + ( + &stac::Version::v1_0_0, + StacExtensionVersions { + projection: StacExtensionMajorVersion::V1, + raster: StacExtensionMajorVersion::V1, + eo: StacExtensionMajorVersion::V1, + }, + ) => scan_item_asset_v1_0_0(asset), + ( + &stac::Version::v1_1_0, + StacExtensionVersions { + projection: StacExtensionMajorVersion::V2, + raster: StacExtensionMajorVersion::V2, + eo: StacExtensionMajorVersion::V2, + }, + ) => scan_item_asset_v1_1_0(asset), + _ => Err(anyhow::anyhow!( + "Unsupported STAC version or extension versions: {:?}, {stac_extension_versions:?}", + collection.version + )), + }; + + match asset_bands { + Ok(Some(asset_bands)) => { + merge_dataset_bands(&mut dataset_bands, asset_bands); + } + Err(err) => { + println!("[ERROR] Skipping asset {asset_key}: {err:#}"); + } + _ => {} + } + } + + for bands in dataset_bands.values_mut() { + bands.sort_by(|a: &RasterBandDescriptor, b| a.name.cmp(&b.name)); } Ok(dataset_bands) } -fn scan_item_asset( - asset_key: &str, - asset: &stac::ItemAsset, +fn merge_dataset_bands( dataset_bands: &mut HashMap>, - missing_bands_handling: bool, -) -> anyhow::Result<()> { - if asset.r#type != Some("image/tiff; application=geotiff; profile=cloud-optimized".to_string()) - { - anyhow::bail!("Skipping non-geotiff asset: {asset_key}"); + additions: HashMap>, +) { + for (partial_key, band_descriptors) in additions { + let existing_bands = dataset_bands.entry(partial_key).or_default(); + + for descriptor in band_descriptors { + if existing_bands.iter().all(|b| b.name != descriptor.name) { + existing_bands.push(descriptor); + } + } } +} - let raster_bands: anyhow::Result> = asset - .additional_fields - .get("raster:bands") - .ok_or(anyhow::anyhow!("Missing raster:bands")) - .and_then(|bands| { - serde_json::from_value(bands.clone()) - .map_err(|e| anyhow::anyhow!("invalid raster:bands: {e}",)) - }); - - let eo_bands: anyhow::Result> = asset +fn scan_item_asset_v1_0_0( + asset: &stac::ItemAsset, +) -> anyhow::Result>>> { + let mut dataset_bands: HashMap> = HashMap::new(); + + let Some(raster_bands) = asset.additional_fields.get("raster:bands") else { + return Ok(None); + }; + let raster_bands: Vec = + serde_json::from_value(raster_bands.clone()) + .map_err(|e| anyhow::anyhow!("invalid raster:bands: {e}",))?; + + let band_count = raster_bands.len(); + + // Try to get eo:bands (optional) + let eo_bands = asset .additional_fields .get("eo:bands") - .ok_or(anyhow::anyhow!("Missing eo:bands in asset")) - .and_then(|eo_bands| { - serde_json::from_value(eo_bands.clone()) - .map_err(|e| anyhow::anyhow!("invalid eo:bands: {e}")) - }); - - let (raster_bands, eo_bands) = if missing_bands_handling { - handle_missing_bands(raster_bands, eo_bands, asset, asset_key)? - } else { - (raster_bands?, eo_bands?) - }; + .and_then(|v| serde_json::from_value::>(v.clone()).ok()); - if raster_bands.len() != eo_bands.len() { - anyhow::bail!("Skipping asset with mismatched raster:bands and eo:bands length",); + // If eo:bands is present, it must match raster:bands length + if let Some(ref eo_bands_vec) = eo_bands { + if band_count != eo_bands_vec.len() { + return Ok(None); + } + } else if band_count != 1 { + // If eo:bands is missing, only support single-band assets + return Ok(None); } - // if multiple bands for asset, prefix band names with asset key - let prefix = if raster_bands.len() > 1 { - format!("{asset_key}_") - } else { - String::new() - }; - - for (raster_band, eo_band) in raster_bands.into_iter().zip(eo_bands.into_iter()) { + for (index, raster_band) in raster_bands.into_iter().enumerate() { let data_type = raster_band .data_type .ok_or(anyhow::anyhow!("Missing data_type in raster band"))?; @@ -1502,14 +2192,24 @@ fn scan_item_asset( let geo_transform = geo_transform_from_fields(&asset.additional_fields) .ok_or(anyhow::anyhow!("missing proj:transform"))?; - let resoution = geo_transform.x_pixel_size().into(); - - let band_name = format!("{}{}", prefix, eo_band.name); + let resolution = geo_transform.x_pixel_size().into(); + + let band_name = if let Some(ref eo_bands_vec) = eo_bands { + // Use eo:bands metadata if available + v1_0_0_band_name( + asset.title.as_deref(), + Some(&eo_bands_vec[index]), + band_count, + ) + } else { + // For single-band assets without eo:bands, derive a stable name from title metadata. + v1_0_0_band_name(asset.title.as_deref(), None, 1) + }; dataset_bands .entry(PartialDatasetKey { data_type: raster_data_type, - resolution: resoution, + resolution, }) .or_default() .push(RasterBandDescriptor { @@ -1519,92 +2219,115 @@ fn scan_item_asset( }); } - Ok(()) + Ok(Some(dataset_bands)) } -fn handle_missing_bands( - raster_bands_result: anyhow::Result>, - eo_bands_result: anyhow::Result>, +fn scan_item_asset_v1_1_0( asset: &stac::ItemAsset, - asset_key: &str, -) -> anyhow::Result<(Vec, Vec)> { - match (raster_bands_result, eo_bands_result) { - (Ok(raster_bands), Ok(eo_bands)) => Ok((raster_bands, eo_bands)), - (Err(_), Ok(eo_bands)) => { - let raster_bands = - handle_missing_raster_bands(asset, &eo_bands).with_context(|| { - format!("Missing raster:bands and cannot create defaults for asset {asset_key}") - })?; - Ok((raster_bands, eo_bands)) - } - (Ok(raster_bands), Err(_)) => { - let eo_bands = - handle_missing_eo_bands(asset_key, &raster_bands).with_context(|| { - format!("Missing eo:bands and cannot create defaults for asset {asset_key}") - })?; - Ok((raster_bands, eo_bands)) - } - (Err(_), Err(_)) => { - anyhow::bail!("Missing both raster:bands and eo:bands for asset {asset_key}") - } +) -> anyhow::Result>>> { + let mut dataset_bands: HashMap> = HashMap::new(); + + // in STAC 1.1.0 the `data_type` is now common metadata + let data_type = asset + .additional_fields + .get("data_type") + .ok_or(anyhow::anyhow!( + "Missing data_type in asset additional fields" + ))? + .as_str() + .ok_or(anyhow::anyhow!("data_type is not a string"))?; + + let data_type = raster_data_type_from_stac_data_type_str(data_type) + .context(format!("Unsupported data_type: {data_type}"))?; + + // in STAC 1.1.0 `raster:bands` and `eo:bands` are merged into common metadata `bands` + let band_names = band_names_from_item_asset_v1_1_0(asset)?; + + let resolution = asset + .additional_fields + .get("gsd") + .ok_or(anyhow::anyhow!("missing attribute `gsd`"))? + .as_f64() + .ok_or(anyhow::anyhow!("attribute `gsd` is not a number"))?; + + for band_name in band_names { + dataset_bands + .entry(PartialDatasetKey { + data_type, + resolution: resolution.into(), + }) + .or_default() + .push(RasterBandDescriptor { + name: band_name.clone(), + // TODO: unit from raster_band.unit + measurement: Measurement::Unitless(UnitlessMeasurement { r#type: crate::api::model::datatypes::UnitlessMeasurementTypeTag::UnitlessMeasurementTypeTag }), + }); } + + Ok(Some(dataset_bands)) } -/// Handle missing raster:bands by creating defaults for visual assets -/// Returns Ok(bands) if defaults can be created, Err otherwise -fn handle_missing_raster_bands( - asset: &stac::ItemAsset, - eo_bands: &[EoBand], -) -> anyhow::Result> { - // Check if asset has visual role by looking at additional_fields - let has_visual_role = asset.roles.iter().any(|r| r == "visual"); +fn band_names_from_asset_v1_1_0(asset: &stac::Asset) -> anyhow::Result> { + let asset_title = asset + .title + .as_deref() + .ok_or(anyhow::anyhow!("Missing title in asset metadata"))?; + + let bands = &asset.bands; - if !has_visual_role { - anyhow::bail!("Asset does not have visual role"); + if bands.is_empty() { + return Ok(vec![asset_title.to_string()]); } - if eo_bands.len() != 3 { - anyhow::bail!( - "Asset has visual role but does not have exactly 3 eo:bands (found {})", - eo_bands.len() - ); + let mut names = Vec::new(); + if bands.len() == 1 { + names.push(asset_title.to_string()); + return Ok(names); } - // Create 3 default U8 bands for visual assets (RGB) - Ok(vec![ - stac_extensions::raster::Band { - data_type: Some(stac_extensions::raster::DataType::UInt8), - ..Default::default() - }, - stac_extensions::raster::Band { - data_type: Some(stac_extensions::raster::DataType::UInt8), - ..Default::default() - }, - stac_extensions::raster::Band { - data_type: Some(stac_extensions::raster::DataType::UInt8), - ..Default::default() - }, - ]) + for band in bands { + let Some(band_name) = &band.name else { + anyhow::bail!("Band is missing name for multi-band asset"); + }; + names.push(format!("{asset_title} [{band_name}]")); + } + + Ok(names) } -/// Handle missing eo:bands by creating defaults based on the number of raster bands -/// Returns Ok(bands) if defaults can be created, Err otherwise -fn handle_missing_eo_bands( - asset_key: &str, - raster_bands: &[stac_extensions::raster::Band], -) -> anyhow::Result> { - match raster_bands.len() { - 1 => { - // Use asset key as band name for single-band assets without eo:bands - Ok(vec![EoBand { - name: asset_key.to_string(), - }]) - } - _ => anyhow::bail!( - "Cannot create default eo:bands for {} raster bands", - raster_bands.len() - ), +fn band_names_from_item_asset_v1_1_0(asset: &stac::ItemAsset) -> anyhow::Result> { + let asset_title = asset + .title + .as_deref() + .ok_or(anyhow::anyhow!("Missing title in asset metadata"))?; + + let band_names = asset + .additional_fields + .get("bands") + .and_then(serde_json::Value::as_array); + + let Some(bands) = band_names else { + return Ok(vec![asset_title.to_string()]); + }; + + if bands.is_empty() { + return Ok(vec![asset_title.to_string()]); + } + + if bands.len() == 1 { + return Ok(vec![asset_title.to_string()]); } + + let mut names = Vec::new(); + for band in bands { + let band_name = band + .get("name") + .and_then(serde_json::Value::as_str) + .ok_or(anyhow::anyhow!("Band is missing name for multi-band asset"))?; + names.push(format!("{asset_title} [{band_name}]")); + } + + Ok(names) } fn geo_transform_from_fields( @@ -1628,17 +2351,36 @@ fn geo_transform_from_fields( Some(geo_transform) } -fn data_type_from_asset(asset: &Asset) -> anyhow::Result { - let data_type_str = asset - .additional_fields - .get("raster:bands") - .and_then(|v| v.as_array()) - .and_then(|bands| bands.first()) - .and_then(|band| band.get("data_type")) - .and_then(|v| v.as_str()) - .ok_or(anyhow::anyhow!("Missing data_type in raster:bands[0]"))?; - - raster_data_type_from_stac_data_type_str(data_type_str) +#[allow(clippy::needless_pass_by_value)] +fn data_type_from_asset( + stac_version: stac::Version, + asset: &Asset, +) -> anyhow::Result { + match stac_version { + stac::Version::v1_0_0 => { + let data_type_str = asset + .additional_fields + .get("raster:bands") + .and_then(|v| v.as_array()) + .and_then(|bands| bands.first()) + .and_then(|band| band.get("data_type")) + .and_then(|v| v.as_str()) + .ok_or(anyhow::anyhow!("Missing data_type in raster:bands[0]"))?; + + raster_data_type_from_stac_data_type_str(data_type_str) + } + stac::Version::v1_1_0 => { + let data_type = asset + .data_type + .as_ref() + .ok_or(anyhow::anyhow!("Missing data_type in asset"))?; + + raster_data_type_from_stac_data_type(data_type) + } + _ => { + anyhow::bail!("Unsupported STAC version: {stac_version}"); + } + } } fn raster_data_type_from_stac_data_type_str(data_type_str: &str) -> anyhow::Result { @@ -1727,12 +2469,12 @@ where Err(err) => { attempt += 1; if attempt >= MAX_RETRIES { - println!("{operation_name} failed after {MAX_RETRIES} attempts: {err}",); + println!("[ERROR] {operation_name} failed after {MAX_RETRIES} attempts: {err}",); return Err(err); } let delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS * 2_u64.pow(attempt - 1)); println!( - "{operation_name} failed (attempt {attempt}/{MAX_RETRIES}): {err}. Retrying in {delay:?}...", + "[WARN] {operation_name} failed (attempt {attempt}/{MAX_RETRIES}): {err}. Retrying in {delay:?}...", ); tokio::time::sleep(delay).await; } @@ -1740,24 +2482,476 @@ where } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] + +enum StacExtensionMajorVersion { + V1, + V2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ImportFileType { + Cog, + Jp2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct StacExtensionVersions { + projection: StacExtensionMajorVersion, + raster: StacExtensionMajorVersion, + eo: StacExtensionMajorVersion, +} + +fn parse_stac_extension_versions(extensions: &[String]) -> anyhow::Result { + let mut projection = None; + let mut raster = None; + let mut eo = None; + + for extension in ["projection", "raster", "eo"] { + if let Some(ext_str) = extensions + .iter() + .find(|ext| ext.starts_with(&format!("https://stac-extensions.github.io/{extension}"))) + { + let version = + stac_extension_version_from_str(ext_str, extension).with_context(|| { + format!("Failed to parse version for {extension} extension {ext_str}") + })?; + match extension { + "projection" => projection = Some(version), + "raster" => raster = Some(version), + "eo" => eo = Some(version), + _ => unreachable!(), + } + } + } + + Ok(StacExtensionVersions { + projection: projection.ok_or(anyhow::anyhow!("Missing projection extension"))?, + raster: raster.ok_or(anyhow::anyhow!("Missing raster extension"))?, + eo: eo.ok_or(anyhow::anyhow!("Missing eo extension"))?, + }) +} + +fn infer_collection_extension_versions( + collection: &stac::Collection, +) -> anyhow::Result { + if collection.version != stac::Version::v1_0_0 { + anyhow::bail!( + "Cannot infer extension versions for STAC {}", + collection.version + ); + } + + let has_projection = collection.item_assets.values().any(|asset| { + asset.additional_fields.contains_key("proj:transform") + || asset.additional_fields.contains_key("proj:shape") + || asset.additional_fields.contains_key("proj:epsg") + }); + let has_raster = collection + .item_assets + .values() + .any(|asset| asset.additional_fields.contains_key("raster:bands")); + let has_eo = collection + .item_assets + .values() + .any(|asset| asset.additional_fields.contains_key("eo:bands")); + + Ok(StacExtensionVersions { + projection: has_projection + .then_some(StacExtensionMajorVersion::V1) + .ok_or(anyhow::anyhow!("Missing projection extension"))?, + raster: has_raster + .then_some(StacExtensionMajorVersion::V1) + .ok_or(anyhow::anyhow!("Missing raster extension"))?, + eo: has_eo + .then_some(StacExtensionMajorVersion::V1) + .ok_or(anyhow::anyhow!("Missing eo extension"))?, + }) +} + +impl TryFrom<&stac::Collection> for StacExtensionVersions { + type Error = anyhow::Error; + + fn try_from(collection: &stac::Collection) -> Result { + parse_stac_extension_versions(&collection.extensions) + .or_else(|_| infer_collection_extension_versions(collection)) + } +} + +impl TryFrom<&stac::Item> for StacExtensionVersions { + type Error = anyhow::Error; + + fn try_from(item: &stac::Item) -> Result { + parse_stac_extension_versions(&item.extensions) + } +} + +fn stac_extension_version_from_str( + extension_str: &str, + extension_name: &str, +) -> anyhow::Result { + let version_str = extension_str + .strip_prefix(&format!( + "https://stac-extensions.github.io/{extension_name}/v" + )) + .and_then(|rem| rem.strip_suffix("/schema.json")) + .ok_or_else(|| anyhow::anyhow!("Unknown version for extension {extension_name}"))?; + + match version_str.split('.').next() { + Some("1") => Ok(StacExtensionMajorVersion::V1), + Some("2") => Ok(StacExtensionMajorVersion::V2), + _ => Err(anyhow::anyhow!( + "Unknown version '{version_str}' for extension {extension_name}" + )), + } +} + #[cfg(test)] mod tests { + use super::*; + use httptest::{ + Expectation, Server, all_of, + matchers::{self, request}, + responders, + }; + use std::path::PathBuf; + + fn stac_params(stac_url: String, geo_engine_url: String) -> StacImport { + StacImport { + stac_url, + stac_collection: "sentinel-2-l2a".to_string(), + limit: Some(10), + time_start: Some("2026-01-01T00:00:00Z".to_string()), + time_end: Some("2026-01-31T23:59:59Z".to_string()), + bbox: Some(vec![8.766, 50.802, 8.767, 50.803]), + geo_engine_url, + geo_engine_email: "admin@localhost".to_string(), + geo_engine_password: "adminadmin".to_string(), + volume_name: "geodata-test".to_string(), + dataset_name_prefix: "Sentinel2".to_string(), + verbose: false, + prefetch_pages: 1, + epsgs: vec![], + z_index_property_name: Some("updated".to_string()), + s3_endpoint: None, + s3_access_key: None, + s3_secret_key: None, + file_types: vec![ImportFileType::Cog], + filter_item_fields: false, + } + } - #[test] - fn it_parses_raster_bands() { - let json = r#" [ - { - "nodata": 0, - "data_type": "uint16", - "bits_per_sample": 15, - "spatial_resolution": 20, - "unit": "cm", - "scale": 0.001, - "offset": 0 - } - ]"#; + #[tokio::test] + #[allow(clippy::too_many_lines)] + async fn test_element84_stac_v1_0_0_creates_expected_dataset_and_tiles() { + let stac_server = Server::run(); + let geo_server = Server::run(); + + stac_server.expect( + Expectation::matching(request::method_path("GET", "/collections/sentinel-2-l2a")) + .respond_with(responders::json_encoded( + serde_json::from_str::(include_str!( + "../../../test_data/stac_responses/collections/element84.json" + )) + .expect("valid element84 collection fixture"), + )), + ); + + stac_server.expect( + Expectation::matching(all_of![ + request::method("GET"), + request::path("/collections/sentinel-2-l2a/items"), + request::query(matchers::url_decoded(matchers::contains(( + "bbox", + "8.766,50.802,8.767,50.803" + )))), + request::query(matchers::url_decoded(matchers::contains(( + "datetime", + "2026-01-01T00:00:00Z/2026-01-31T23:59:59Z" + )))), + request::query(matchers::url_decoded(matchers::contains(("limit", "10")))), + ]) + .respond_with(responders::json_encoded( + serde_json::from_str::(include_str!( + "../../../test_data/stac_responses/items/element84-marburg-minimal.json" + )) + .expect("valid element84 items fixture"), + )), + ); + + geo_server.expect( + Expectation::matching(request::method_path("POST", "/api/login")).respond_with( + responders::json_encoded(serde_json::json!({ "id": "test-session" })), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path( + "GET", + "/api/dataset/sentinel-2-l2a_EPSG32632_U16_10", + )) + .respond_with( + responders::status_code(400) + .append_header("Content-Type", "application/json") + .body( + serde_json::json!({ + "error": "CannotLoadDataset", + "message": "Dataset not found" + }) + .to_string(), + ), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path("POST", "/api/dataset")).respond_with( + responders::json_encoded(serde_json::json!({ + "datasetName": "sentinel-2-l2a_EPSG32632_U16_10" + })), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path("PUT", "/api/permissions")) + .times(2) + .respond_with(responders::status_code(200)), + ); + + let mut importer = StacImporter::new(stac_params( + stac_server.url_str("").trim_end_matches('/').to_string(), + geo_server.url_str("/api").trim_end_matches('/').to_string(), + )) + .await + .expect("importer should initialize"); + + let query_state = QueryState::FirstPage { + query_url: format!( + "{}/collections/{}/items", + importer.params.stac_url, importer.params.stac_collection + ), + query_params: create_query_params(&importer.params), + }; + + let (item_collection, query_state) = query_item_collection(&importer.client, &query_state) + .await + .expect("items should be fetched"); + assert!(matches!(query_state, QueryState::Finished)); + + let dataset_tiles = importer + .process_items(item_collection) + .await + .expect("item processing should succeed"); + + assert_eq!(dataset_tiles.len(), 1); + + let partial_key = PartialDatasetKey { + data_type: RasterDataType::U16, + resolution: OrderedFloat(10.), + }; + + let expected_band_idx = importer + .bands + .get(&partial_key) + .expect("10m U16 dataset should be present") + .iter() + .position(|band| band.name == "blue") + .expect("blue band should exist") as u32; + + let (dataset_key, tiles) = dataset_tiles + .iter() + .next() + .expect("exactly one dataset key should be present"); + + assert_eq!(dataset_key.epsg, 32632); + assert_eq!(dataset_key.data_type, RasterDataType::U16); + assert_eq!(dataset_key.resolution, OrderedFloat(10.)); + assert_eq!(tiles.len(), 1); + + let tile = &tiles[0]; + assert_eq!(tile.band, expected_band_idx); + assert_eq!(tile.params.rasterband_channel, 1); + assert_eq!(tile.params.width, 10_980); + assert_eq!(tile.params.height, 10_980); + assert_eq!( + tile.params.file_path, + PathBuf::from( + "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/32/U/MB/2026/1/S2B_32UMB_20260128_0_L2A/B02.tif" + ) + ); + + let expected_z_index = chrono::DateTime::parse_from_rfc3339("2026-01-29T21:55:06.896Z") + .expect("valid updated datetime") + .timestamp_millis(); + assert_eq!(tile.z_index, expected_z_index); + } + + #[tokio::test] + #[allow(clippy::too_many_lines)] + async fn test_polsa_stac_v1_1_0_creates_expected_dataset_and_tiles() { + let stac_server = Server::run(); + let geo_server = Server::run(); + + stac_server.expect( + Expectation::matching(request::method_path("GET", "/collections/sentinel-2-l2a")) + .respond_with(responders::json_encoded( + serde_json::from_str::(include_str!( + "../../../test_data/stac_responses/collections/code-de.json" + )) + .expect("valid code-de collection fixture"), + )), + ); - let _raster_bands: Vec = - serde_json::from_str(json).expect("Failed to parse raster bands"); + stac_server.expect( + Expectation::matching(all_of![ + request::method("GET"), + request::path("/collections/sentinel-2-l2a/items"), + request::query(matchers::url_decoded(matchers::contains(( + "bbox", + "8.766,50.802,8.767,50.803" + )))), + request::query(matchers::url_decoded(matchers::contains(( + "datetime", + "2026-01-01T00:00:00Z/2026-01-31T23:59:59Z" + )))), + request::query(matchers::url_decoded(matchers::contains(("limit", "10")))), + ]) + .respond_with(responders::json_encoded( + serde_json::from_str::(include_str!( + "../../../test_data/stac_responses/items/polsa-marburg-minimal.json" + )) + .expect("valid polsa items fixture"), + )), + ); + + geo_server.expect( + Expectation::matching(request::method_path("POST", "/api/login")).respond_with( + responders::json_encoded(serde_json::json!({ "id": "test-session" })), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path( + "GET", + "/api/dataset/sentinel-2-l2a_EPSG32632_U16_10", + )) + .respond_with( + responders::status_code(400) + .append_header("Content-Type", "application/json") + .body( + serde_json::json!({ + "error": "CannotLoadDataset", + "message": "Dataset not found" + }) + .to_string(), + ), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path("POST", "/api/dataset")).respond_with( + responders::json_encoded(serde_json::json!({ + "datasetName": "sentinel-2-l2a_EPSG32632_U16_10" + })), + ), + ); + + geo_server.expect( + Expectation::matching(request::method_path("PUT", "/api/permissions")) + .times(2) + .respond_with(responders::status_code(200)), + ); + + let mut params = stac_params( + stac_server.url_str("").trim_end_matches('/').to_string(), + geo_server.url_str("/api").trim_end_matches('/').to_string(), + ); + params.s3_endpoint = Some("localhost:9000".to_string()); + params.s3_access_key = Some("mock-access-key".to_string()); + params.s3_secret_key = Some("mock-secret-key".to_string()); + params.file_types = vec![ImportFileType::Jp2]; + + let mut importer = StacImporter::new(params) + .await + .expect("importer should initialize"); + + let query_state = QueryState::FirstPage { + query_url: format!( + "{}/collections/{}/items", + importer.params.stac_url, importer.params.stac_collection + ), + query_params: create_query_params(&importer.params), + }; + + let (item_collection, query_state) = query_item_collection(&importer.client, &query_state) + .await + .expect("items should be fetched"); + assert!(matches!(query_state, QueryState::Finished)); + + let dataset_tiles = importer + .process_items(item_collection) + .await + .expect("item processing should succeed"); + + assert_eq!(dataset_tiles.len(), 1); + + let partial_key = PartialDatasetKey { + data_type: RasterDataType::U16, + resolution: OrderedFloat(10.), + }; + + let expected_band_idx = importer + .bands + .get(&partial_key) + .expect("10m U16 dataset should be present") + .iter() + .position(|band| band.name == "Blue (band 2) - 10m") + .expect("blue band should exist") as u32; + + let (dataset_key, tiles) = dataset_tiles + .iter() + .next() + .expect("exactly one dataset key should be present"); + + assert_eq!(dataset_key.epsg, 32632); + assert_eq!(dataset_key.data_type, RasterDataType::U16); + assert_eq!(dataset_key.resolution, OrderedFloat(10.)); + assert_eq!(tiles.len(), 1); + + let tile = &tiles[0]; + assert_eq!(tile.band, expected_band_idx); + assert_eq!(tile.params.rasterband_channel, 1); + assert_eq!(tile.params.width, 10_980); + assert_eq!(tile.params.height, 10_980); + assert_eq!( + tile.params.file_path, + PathBuf::from("/vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/28/example/B02_10m.jp2") + ); + + let expected_z_index = chrono::DateTime::parse_from_rfc3339("2026-01-29T14:19:30Z") + .expect("valid updated datetime") + .timestamp_millis(); + assert_eq!(tile.z_index, expected_z_index); + + let gdal_options = tile + .params + .gdal_config_options + .as_ref() + .expect("s3 tiles should include gdal config options"); + + assert!( + gdal_options + .iter() + .any(|opt| opt.0.0 == "AWS_S3_ENDPOINT" && opt.0.1 == "localhost:9000") + ); + assert!( + gdal_options + .iter() + .any(|opt| opt.0.0 == "AWS_ACCESS_KEY_ID" && opt.0.1 == "mock-access-key") + ); + assert!( + gdal_options + .iter() + .any(|opt| { opt.0.0 == "AWS_SECRET_ACCESS_KEY" && opt.0.1 == "mock-secret-key" }) + ); } } diff --git a/geoengine/services/src/contexts/db_types.rs b/geoengine/services/src/contexts/db_types.rs index 5acc08ff16..e253f5d959 100644 --- a/geoengine/services/src/contexts/db_types.rs +++ b/geoengine/services/src/contexts/db_types.rs @@ -12,6 +12,7 @@ use crate::{ gfbio_collections::GfbioCollectionsDataProviderDefinition, netcdfcf::{EbvPortalDataProviderDefinition, NetCdfCfDataProviderDefinition}, pangaea::PangaeaDataProviderDefinition, + stac::StacDataProviderDefinition, }, listing::Provenance, storage::MetaDataDefinition, @@ -937,6 +938,7 @@ pub struct TypedDataProviderDefinitionDbType { copernicus_dataspace_provider_definition: Option, sentinel_s2_l2_a_cogs_provider_definition: Option, wildlive_data_connector_definition: Option, + stac_data_provider_definition: Option, } #[allow(clippy::too_many_lines)] @@ -956,6 +958,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } } @@ -973,6 +976,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::GbifDataProviderDefinition(data_provider_definition) => { @@ -988,6 +992,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } } @@ -1005,6 +1010,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::GfbioCollectionsDataProviderDefinition( @@ -1021,6 +1027,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::EbvPortalDataProviderDefinition( @@ -1037,6 +1044,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::NetCdfCfDataProviderDefinition( @@ -1053,6 +1061,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::PangaeaDataProviderDefinition( @@ -1069,6 +1078,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::EdrDataProviderDefinition(data_provider_definition) => { @@ -1084,6 +1094,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: Some(data_provider_definition.clone()), copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } } @@ -1101,6 +1112,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: Some(data_provider_definition.clone()), sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition( @@ -1117,8 +1129,26 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: Some(data_provider_definition.clone()), + stac_data_provider_definition: None, wildlive_data_connector_definition: None, }, + TypedDataProviderDefinition::StacDataProviderDefinition(data_provider_definition) => { + Self { + aruna_data_provider_definition: None, + dataset_layer_listing_provider_definition: None, + gbif_data_provider_definition: None, + gfbio_abcd_data_provider_definition: None, + gfbio_collections_data_provider_definition: None, + ebv_portal_data_provider_definition: None, + net_cdf_cf_data_provider_definition: None, + pangaea_data_provider_definition: None, + edr_data_provider_definition: None, + copernicus_dataspace_provider_definition: None, + sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: Some(data_provider_definition.clone()), + wildlive_data_connector_definition: None, + } + } TypedDataProviderDefinition::WildliveDataConnectorDefinition( data_provider_definition, ) => Self { @@ -1133,6 +1163,7 @@ impl From<&TypedDataProviderDefinition> for TypedDataProviderDefinitionDbType { edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: Some(data_provider_definition.clone()), }, } @@ -1157,6 +1188,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok(TypedDataProviderDefinition::ArunaDataProviderDefinition( data_provider_definition, @@ -1173,6 +1205,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::DatasetLayerListingProviderDefinition( @@ -1191,6 +1224,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok(TypedDataProviderDefinition::GbifDataProviderDefinition( data_provider_definition, @@ -1207,6 +1241,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::GfbioAbcdDataProviderDefinition( @@ -1225,6 +1260,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::GfbioCollectionsDataProviderDefinition( @@ -1243,6 +1279,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::EbvPortalDataProviderDefinition( @@ -1261,6 +1298,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok(TypedDataProviderDefinition::NetCdfCfDataProviderDefinition( data_provider_definition, @@ -1277,6 +1315,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok(TypedDataProviderDefinition::PangaeaDataProviderDefinition( data_provider_definition, @@ -1293,6 +1332,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: Some(data_provider_definition), copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok(TypedDataProviderDefinition::EdrDataProviderDefinition( data_provider_definition, @@ -1309,6 +1349,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: Some(data_provider_definition), sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::CopernicusDataspaceDataProviderDefinition( @@ -1327,6 +1368,7 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: Some(data_provider_definition), + stac_data_provider_definition: None, wildlive_data_connector_definition: None, } => Ok( TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition( @@ -1345,6 +1387,24 @@ impl TryFrom for TypedDataProviderDefinition edr_data_provider_definition: None, copernicus_dataspace_provider_definition: None, sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: Some(data_provider_definition), + wildlive_data_connector_definition: None, + } => Ok(TypedDataProviderDefinition::StacDataProviderDefinition( + data_provider_definition, + )), + TypedDataProviderDefinitionDbType { + aruna_data_provider_definition: None, + dataset_layer_listing_provider_definition: None, + gbif_data_provider_definition: None, + gfbio_abcd_data_provider_definition: None, + gfbio_collections_data_provider_definition: None, + ebv_portal_data_provider_definition: None, + net_cdf_cf_data_provider_definition: None, + pangaea_data_provider_definition: None, + edr_data_provider_definition: None, + copernicus_dataspace_provider_definition: None, + sentinel_s2_l2_a_cogs_provider_definition: None, + stac_data_provider_definition: None, wildlive_data_connector_definition: Some(data_provider_definition), } => Ok( TypedDataProviderDefinition::WildliveDataConnectorDefinition( diff --git a/geoengine/services/src/contexts/migrations/current_schema.sql b/geoengine/services/src/contexts/migrations/current_schema.sql index 3d91c1c3f8..aaf769ef42 100644 --- a/geoengine/services/src/contexts/migrations/current_schema.sql +++ b/geoengine/services/src/contexts/migrations/current_schema.sql @@ -874,6 +874,39 @@ CREATE TYPE "WildliveDataConnectorDefinition" AS ( auth "WildliveDataConnectorAuth" ); +CREATE TYPE "StacProviderS3Config" AS ( + endpoint text, + access_key text, + secret_key text +); + +CREATE TYPE "StacProviderDatasetBand" AS ( + asset_title text, + band_name text +); + +CREATE TYPE "StacProviderDataset" AS ( + "name" text, + description text, + data_type "RasterDataType", + resolution "SpatialResolution", + projection "SpatialReference", + spatial_grid "SpatialGridDescriptor", + bands "StacProviderDatasetBand" [] +); + +CREATE TYPE "StacDataProviderDefinition" AS ( + "name" text, + id uuid, + description text, + priority smallint, + api_url text, + collection_name text, + s3_config "StacProviderS3Config", + time_dimension "TimeDimension", + datasets "StacProviderDataset" [] +); + CREATE TYPE "DataProviderDefinition" AS ( -- one of aruna_data_provider_definition "ArunaDataProviderDefinition", @@ -891,7 +924,8 @@ CREATE TYPE "DataProviderDefinition" AS ( "SentinelS2L2ACogsProviderDefinition", copernicus_dataspace_provider_definition "CopernicusDataspaceDataProviderDefinition", - wildlive_data_connector_definition "WildliveDataConnectorDefinition" + wildlive_data_connector_definition "WildliveDataConnectorDefinition", + stac_data_provider_definition "StacDataProviderDefinition" ); CREATE TABLE layer_providers ( diff --git a/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.rs b/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.rs new file mode 100644 index 0000000000..7a55df7cf1 --- /dev/null +++ b/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.rs @@ -0,0 +1,27 @@ +use super::database_migration::{DatabaseVersion, Migration}; +use crate::{ + contexts::migrations::migration_0027_tile_z_index::Migration0027TileZIndex, error::Result, +}; +use async_trait::async_trait; +use tokio_postgres::Transaction; + +/// This migration adds the STAC provider definition to the provider union type. +pub struct Migration0028StacProvider; + +#[async_trait] +impl Migration for Migration0028StacProvider { + fn prev_version(&self) -> Option { + Some(Migration0027TileZIndex.version()) + } + + fn version(&self) -> DatabaseVersion { + "0028_stac_provider".into() + } + + async fn migrate(&self, tx: &Transaction<'_>) -> Result<()> { + tx.batch_execute(include_str!("migration_0028_stac_provider.sql")) + .await?; + + Ok(()) + } +} diff --git a/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.sql b/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.sql new file mode 100644 index 0000000000..e5d96d21ad --- /dev/null +++ b/geoengine/services/src/contexts/migrations/migration_0028_stac_provider.sql @@ -0,0 +1,35 @@ +CREATE TYPE "StacProviderS3Config" AS ( + endpoint text, + access_key text, + secret_key text +); + +CREATE TYPE "StacProviderDatasetBand" AS ( + asset_title text, + band_name text +); + +CREATE TYPE "StacProviderDataset" AS ( + "name" text, + description text, + data_type "RasterDataType", + resolution "SpatialResolution", + projection "SpatialReference", + spatial_grid "SpatialGridDescriptor", + bands "StacProviderDatasetBand" [] +); + +CREATE TYPE "StacDataProviderDefinition" AS ( + "name" text, + id uuid, + description text, + priority smallint, + api_url text, + collection_name text, + s3_config "StacProviderS3Config", + time_dimension "TimeDimension", + datasets "StacProviderDataset" [] +); + +ALTER TYPE "DataProviderDefinition" ADD ATTRIBUTE +stac_data_provider_definition "StacDataProviderDefinition"; diff --git a/geoengine/services/src/contexts/migrations/mod.rs b/geoengine/services/src/contexts/migrations/mod.rs index 43fc880d20..5ef9d6773b 100644 --- a/geoengine/services/src/contexts/migrations/mod.rs +++ b/geoengine/services/src/contexts/migrations/mod.rs @@ -12,6 +12,7 @@ pub use crate::contexts::migrations::{ migration_0024_raster_result_desc::Migration0024RasterResultDesc, migration_0025_time_descriptor::Migration0025TimeDescriptor, migration_0027_tile_z_index::Migration0027TileZIndex, + migration_0028_stac_provider::Migration0028StacProvider, }; pub use database_migration::{ DatabaseVersion, Migration, MigrationResult, initialize_database, migrate_database, @@ -31,6 +32,7 @@ mod migration_0024_raster_result_desc; mod migration_0025_time_descriptor; mod migration_0026_gdal_tiles; mod migration_0027_tile_z_index; +mod migration_0028_stac_provider; #[cfg(test)] mod schema_info; @@ -64,6 +66,7 @@ pub fn all_migrations() -> Vec> { Box::new(Migration0025TimeDescriptor), Box::new(Migration0026GdalTiles), Box::new(Migration0027TileZIndex), + Box::new(Migration0028StacProvider), ] } diff --git a/geoengine/services/src/contexts/mod.rs b/geoengine/services/src/contexts/mod.rs index ae88530a8f..793b5eae54 100644 --- a/geoengine/services/src/contexts/mod.rs +++ b/geoengine/services/src/contexts/mod.rs @@ -522,8 +522,15 @@ where } }) } - DataId::External(_external) => { - Err(geoengine_operators::error::Error::NotYetImplemented) + DataId::External(external) => { + self.db + .load_layer_provider(external.provider_id) + .await + .map_err(|e| geoengine_operators::error::Error::DatasetMetaData { + source: Box::new(e), + })? + .meta_data(data_id) + .await } } } diff --git a/geoengine/services/src/datasets/external/aruna/mod.rs b/geoengine/services/src/datasets/external/aruna/mod.rs index d945509324..005cb9bd3d 100644 --- a/geoengine/services/src/datasets/external/aruna/mod.rs +++ b/geoengine/services/src/datasets/external/aruna/mod.rs @@ -47,7 +47,8 @@ use geoengine_operators::engine::{ use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; use geoengine_operators::source::{ FileNotFoundHandling, GdalDatasetParameters, GdalLoadingInfo, GdalLoadingInfoTemporalSlice, - GdalLoadingInfoTemporalSliceIterator, GdalSource, GdalSourceParameters, OgrSource, + GdalLoadingInfoTemporalSliceIterator, GdalSource, GdalSourceParameters, + MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, OgrSource, OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec, OgrSourceParameters, OgrSourceTimeFormat, }; @@ -784,6 +785,30 @@ impl MetaDataProvider for ArunaDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> geoengine_operators::util::Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + #[async_trait::async_trait] impl DataProvider for ArunaDataProvider { async fn provenance(&self, id: &DataId) -> crate::error::Result { diff --git a/geoengine/services/src/datasets/external/copernicus_dataspace/provider.rs b/geoengine/services/src/datasets/external/copernicus_dataspace/provider.rs index e69c549f1b..453f684d39 100644 --- a/geoengine/services/src/datasets/external/copernicus_dataspace/provider.rs +++ b/geoengine/services/src/datasets/external/copernicus_dataspace/provider.rs @@ -27,7 +27,10 @@ use geoengine_operators::{ VectorResultDescriptor, }, mock::MockDatasetDataSourceLoadingInfo, - source::{GdalLoadingInfo, GdalSource, GdalSourceParameters, OgrSourceDataset}, + source::{ + GdalLoadingInfo, GdalSource, GdalSourceParameters, MultiBandGdalLoadingInfo, + MultiBandGdalLoadingInfoQueryRectangle, OgrSourceDataset, + }, }; use ordered_float::NotNan; use serde::{Deserialize, Serialize}; @@ -454,6 +457,31 @@ impl MetaDataProvider for CopernicusDataspaceDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + #[async_trait] impl MetaDataProvider diff --git a/geoengine/services/src/datasets/external/edr.rs b/geoengine/services/src/datasets/external/edr.rs index 55f9bfccc7..2ecc762cf4 100644 --- a/geoengine/services/src/datasets/external/edr.rs +++ b/geoengine/services/src/datasets/external/edr.rs @@ -32,9 +32,10 @@ use geoengine_operators::engine::{ use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; use geoengine_operators::source::{ FileNotFoundHandling, GdalDatasetParameters, GdalLoadingInfo, GdalLoadingInfoTemporalSlice, - GdalMetaDataList, GdalSource, GdalSourceParameters, OgrSource, OgrSourceColumnSpec, - OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec, - OgrSourceParameters, OgrSourceTimeFormat, + GdalMetaDataList, GdalSource, GdalSourceParameters, MultiBandGdalLoadingInfo, + MultiBandGdalLoadingInfoQueryRectangle, OgrSource, OgrSourceColumnSpec, OgrSourceDataset, + OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec, OgrSourceParameters, + OgrSourceTimeFormat, }; use geoengine_operators::util::TemporaryGdalThreadLocalConfigOptions; use geoengine_operators::util::gdal::gdal_open_dataset; @@ -255,7 +256,7 @@ impl EdrDataProvider { &self, collection_id: &LayerCollectionId, collection_meta: EdrCollectionMetaData, - options: &LayerCollectionListOptions, + options: LayerCollectionListOptions, ) -> Result { let items = collection_meta .parameter_names @@ -314,7 +315,7 @@ impl EdrDataProvider { &self, collection_id: &LayerCollectionId, collection_meta: EdrCollectionMetaData, - options: &LayerCollectionListOptions, + options: LayerCollectionListOptions, ) -> Result { let items = collection_meta .extent @@ -360,7 +361,7 @@ impl EdrDataProvider { collection_id: &LayerCollectionId, collection_meta: EdrCollectionMetaData, parameter: &str, - options: &LayerCollectionListOptions, + options: LayerCollectionListOptions, ) -> Result { let items = collection_meta .extent @@ -949,13 +950,13 @@ impl LayerCollectionProvider for EdrDataProvider { if collection_meta.is_raster_file()? { // The collection is of type raster. A layer can only contain one parameter // of a raster dataset at a time, so let the user choose one. - self.get_raster_parameter_collection(collection_id, collection_meta, &options) + self.get_raster_parameter_collection(collection_id, collection_meta, options) } else if collection_meta.extent.vertical.is_some() { // The collection is of type vector and data is provided for multiple heights. // The user needs to be able to select the height he wants to see. It is not // needed to select a parameter, because for vector datasets all parameters // can be loaded simultaneously. - self.get_vector_height_collection(collection_id, collection_meta, &options) + self.get_vector_height_collection(collection_id, collection_meta, options) } else { // The collection is of type vector and there is only data for a single height. // No height or parameter needs to be selected by the user. Therefore the name @@ -984,7 +985,7 @@ impl LayerCollectionProvider for EdrDataProvider { collection_id, collection_meta, ¶meter, - &options, + options, ) } EdrCollectionId::ParameterAndHeight { .. } => Err(Error::InvalidLayerCollectionId), @@ -1229,6 +1230,31 @@ impl MetaDataProvider for EdrDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + // TODO: proper error handling #[allow(clippy::unnecessary_wraps)] fn time_interval_from_strings( diff --git a/geoengine/services/src/datasets/external/gbif.rs b/geoengine/services/src/datasets/external/gbif.rs index 6d49549259..dbe949faa4 100644 --- a/geoengine/services/src/datasets/external/gbif.rs +++ b/geoengine/services/src/datasets/external/gbif.rs @@ -31,8 +31,9 @@ use geoengine_operators::engine::{ use geoengine_operators::error::Error::{Bb8Postgres, Postgres}; use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; use geoengine_operators::source::{ - GdalLoadingInfo, OgrSource, OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, - OgrSourceDurationSpec, OgrSourceErrorSpec, OgrSourceParameters, OgrSourceTimeFormat, + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, OgrSource, + OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, + OgrSourceErrorSpec, OgrSourceParameters, OgrSourceTimeFormat, }; use itertools::Itertools; use postgres_types::{FromSql, ToSql}; @@ -1497,6 +1498,30 @@ impl MetaDataProvider for GbifDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> geoengine_operators::util::Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + #[async_trait] impl DataProvider for GbifDataProvider { async fn provenance(&self, id: &DataId) -> Result { diff --git a/geoengine/services/src/datasets/external/gfbio_abcd.rs b/geoengine/services/src/datasets/external/gfbio_abcd.rs index 9912ff169d..52dbd0619e 100644 --- a/geoengine/services/src/datasets/external/gfbio_abcd.rs +++ b/geoengine/services/src/datasets/external/gfbio_abcd.rs @@ -35,7 +35,10 @@ use geoengine_operators::source::{ use geoengine_operators::{ engine::{MetaData, MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor}, mock::MockDatasetDataSourceLoadingInfo, - source::{GdalLoadingInfo, OgrSourceDataset}, + source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, + }, }; use postgres_types::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; @@ -635,6 +638,31 @@ impl MetaDataProvider for GfbioAbcdDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + #[async_trait] impl MetaDataProvider diff --git a/geoengine/services/src/datasets/external/gfbio_collections.rs b/geoengine/services/src/datasets/external/gfbio_collections.rs index 2d94454dca..1b72733641 100644 --- a/geoengine/services/src/datasets/external/gfbio_collections.rs +++ b/geoengine/services/src/datasets/external/gfbio_collections.rs @@ -37,7 +37,10 @@ use geoengine_operators::source::{ use geoengine_operators::{ engine::{MetaData, MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor}, mock::MockDatasetDataSourceLoadingInfo, - source::{GdalLoadingInfo, OgrSourceDataset}, + source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, + }, }; use reqwest::{Client, header}; use serde::{Deserialize, Serialize}; @@ -836,6 +839,31 @@ impl MetaDataProvider for GfbioCollectionsDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/geoengine/services/src/datasets/external/mod.rs b/geoengine/services/src/datasets/external/mod.rs index 19a216bcec..d79f7bae57 100644 --- a/geoengine/services/src/datasets/external/mod.rs +++ b/geoengine/services/src/datasets/external/mod.rs @@ -7,12 +7,14 @@ pub mod gfbio_collections; pub mod netcdfcf; pub mod pangaea; pub mod sentinel_s2_l2a_cogs; +pub mod stac; mod wildlive; pub use copernicus_dataspace::CopernicusDataspaceDataProviderDefinition; pub use sentinel_s2_l2a_cogs::{ GdalRetries, SentinelS2L2ACogsProviderDefinition, StacApiRetries, StacQueryBuffer, }; +pub use stac::StacDataProviderDefinition; pub use wildlive::{ WildliveDataConnectorAuth, WildliveDataConnectorDefinition, WildliveDbCache, WildliveError, }; diff --git a/geoengine/services/src/datasets/external/netcdfcf/ebvportal_provider.rs b/geoengine/services/src/datasets/external/netcdfcf/ebvportal_provider.rs index 2339d8759e..7f69096301 100644 --- a/geoengine/services/src/datasets/external/netcdfcf/ebvportal_provider.rs +++ b/geoengine/services/src/datasets/external/netcdfcf/ebvportal_provider.rs @@ -26,7 +26,10 @@ use geoengine_datatypes::{ use geoengine_operators::{ engine::{MetaData, MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor}, mock::MockDatasetDataSourceLoadingInfo, - source::{GdalLoadingInfo, OgrSourceDataset}, + source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, + }, }; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -698,6 +701,31 @@ impl } } +#[async_trait] +impl + MetaDataProvider< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > for EbvPortalDataProvider +{ + async fn meta_data( + &self, + id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + self.netcdf_cf_provider.meta_data(id).await + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/geoengine/services/src/datasets/external/netcdfcf/mod.rs b/geoengine/services/src/datasets/external/netcdfcf/mod.rs index ffb7386c98..f6a5fc49d9 100644 --- a/geoengine/services/src/datasets/external/netcdfcf/mod.rs +++ b/geoengine/services/src/datasets/external/netcdfcf/mod.rs @@ -44,7 +44,10 @@ use geoengine_operators::util::gdal::gdal_open_dataset_ex; use geoengine_operators::{ engine::{MetaData, MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor}, mock::MockDatasetDataSourceLoadingInfo, - source::{GdalLoadingInfo, OgrSourceDataset}, + source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, + }, }; use serde::{Deserialize, Serialize}; use snafu::ResultExt; @@ -1530,6 +1533,31 @@ impl } } +#[async_trait] +impl + MetaDataProvider< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > for NetCdfCfDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + fn netcfg_gdal_path( base_path: Option<&Path>, path: &Path, diff --git a/geoengine/services/src/datasets/external/pangaea/mod.rs b/geoengine/services/src/datasets/external/pangaea/mod.rs index 870a5bc61a..b89e6a27d5 100644 --- a/geoengine/services/src/datasets/external/pangaea/mod.rs +++ b/geoengine/services/src/datasets/external/pangaea/mod.rs @@ -16,7 +16,10 @@ use geoengine_operators::engine::{ MetaData, MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor, }; use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; -use geoengine_operators::source::{GdalLoadingInfo, OgrSourceDataset}; +use geoengine_operators::source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, +}; use reqwest::Client; use serde::{Deserialize, Serialize}; use url::Url; @@ -236,6 +239,31 @@ impl MetaDataProvider for PangaeaDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + #[async_trait] impl MetaDataProvider diff --git a/geoengine/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs b/geoengine/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs index 3708c6a68f..bd2435d5cb 100644 --- a/geoengine/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs +++ b/geoengine/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs @@ -34,7 +34,7 @@ use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; use geoengine_operators::source::{ GdalDatasetGeoTransform, GdalDatasetParameters, GdalLoadingInfo, GdalLoadingInfoTemporalSlice, GdalLoadingInfoTemporalSliceIterator, GdalRetryOptions, GdalSource, GdalSourceParameters, - OgrSourceDataset, + MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, OgrSourceDataset, }; use geoengine_operators::util::retry::retry; use postgres_types::{FromSql, ToSql}; @@ -864,6 +864,31 @@ impl MetaDataProvider for SentinelS2L2aCogsDataProvider +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + #[async_trait] impl MetaDataProvider diff --git a/geoengine/services/src/datasets/external/stac/listing.rs b/geoengine/services/src/datasets/external/stac/listing.rs new file mode 100644 index 0000000000..595fee5ce7 --- /dev/null +++ b/geoengine/services/src/datasets/external/stac/listing.rs @@ -0,0 +1,686 @@ +use super::{StacDataProvider, StacProviderDataset}; +use crate::error; +use crate::layers::layer::{ + CollectionItem, Layer, LayerCollection, LayerCollectionListOptions, LayerCollectionListing, + LayerListing, ProviderLayerCollectionId, ProviderLayerId, +}; +use crate::layers::listing::{ + LayerCollectionId, LayerCollectionProvider, ProviderCapabilities, SearchCapabilities, +}; +use crate::workflows::workflow::Workflow; +use async_trait::async_trait; +use geoengine_datatypes::dataset::{DataId, LayerId, NamedData}; +use geoengine_operators::engine::{RasterOperator, TypedOperator}; +use geoengine_operators::source::{MultiBandGdalSource, MultiBandGdalSourceParameters}; +use snafu::ensure; +use std::collections::BTreeMap; + +const ROOT_COLLECTION_ID: &str = "root"; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum GroupingDimension { + DataType, + Resolution, + Projection, +} + +impl GroupingDimension { + fn id(self) -> &'static str { + match self { + Self::DataType => "dataTypes", + Self::Resolution => "resolutions", + Self::Projection => "projections", + } + } + + fn label(self) -> &'static str { + match self { + Self::DataType => "data type", + Self::Resolution => "resolution", + Self::Projection => "projection", + } + } + + fn all() -> [Self; 3] { + [Self::DataType, Self::Resolution, Self::Projection] + } + + fn from_id(value: &str) -> Option { + match value { + "dataTypes" => Some(Self::DataType), + "resolutions" => Some(Self::Resolution), + "projections" => Some(Self::Projection), + _ => None, + } + } +} + +impl StacDataProvider { + fn format_resolution_value(x: f64, y: f64) -> String { + let x = Self::format_resolution_component(x); + let y = Self::format_resolution_component(y); + + if x == y { x } else { format!("{x}x{y}") } + } + + fn format_resolution_component(value: f64) -> String { + let formatted = format!("{value:.9}"); + let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); + let normalized = if trimmed.is_empty() { "0" } else { trimmed }; + normalized.replace('.', "p") + } + + fn dataset_stable_id(dataset: &StacProviderDataset) -> String { + let projection = dataset + .projection + .to_string() + .to_ascii_lowercase() + .replace(':', ""); + let data_type = format!("{:?}", dataset.data_type).to_ascii_lowercase(); + let res_x = Self::format_resolution_component(dataset.resolution.x); + let res_y = Self::format_resolution_component(dataset.resolution.y); + + if (dataset.resolution.x - dataset.resolution.y).abs() < 1e-9 { + format!("{projection}_{data_type}_{res_x}") + } else { + format!("{projection}_{data_type}_{res_x}x{res_y}") + } + } + + fn dataset_dimension_display_value( + dataset: &StacProviderDataset, + dimension: GroupingDimension, + ) -> String { + match dimension { + GroupingDimension::DataType => format!("{:?}", dataset.data_type), + GroupingDimension::Resolution => { + Self::format_resolution_value(dataset.resolution.x, dataset.resolution.y) + } + GroupingDimension::Projection => dataset.projection.to_string(), + } + } + + fn dataset_dimension_slug_value( + dataset: &StacProviderDataset, + dimension: GroupingDimension, + ) -> String { + match dimension { + GroupingDimension::DataType => format!("{:?}", dataset.data_type).to_ascii_lowercase(), + GroupingDimension::Resolution => { + Self::format_resolution_value(dataset.resolution.x, dataset.resolution.y) + } + GroupingDimension::Projection => dataset + .projection + .to_string() + .to_ascii_lowercase() + .replace(':', ""), + } + } + + fn dataset_matches( + dataset: &StacProviderDataset, + filters: &[(GroupingDimension, &str)], + ) -> bool { + filters.iter().all(|(dimension, slug)| { + Self::dataset_dimension_slug_value(dataset, *dimension) == *slug + }) + } + + fn available_values( + &self, + dimension: GroupingDimension, + filters: &[(GroupingDimension, &str)], + ) -> Vec<(String, String)> { + let values_by_slug = self + .datasets + .iter() + .filter(|dataset| Self::dataset_matches(dataset, filters)) + .fold(BTreeMap::new(), |mut acc, dataset| { + let slug = Self::dataset_dimension_slug_value(dataset, dimension); + let display = Self::dataset_dimension_display_value(dataset, dimension); + acc.entry(slug).or_insert(display); + acc + }); + + values_by_slug.into_iter().collect::>() + } + + fn dataset_layer_id(dataset: &StacProviderDataset) -> LayerId { + LayerId(format!("dataset/{}", Self::dataset_stable_id(dataset))) + } + + pub(super) fn dataset_by_layer_id(&self, id: &LayerId) -> Option<&StacProviderDataset> { + let suffix = id.0.strip_prefix("dataset/")?; + + if let Ok(index) = suffix.parse::() { + return self.datasets.get(index); + } + + self.datasets + .iter() + .find(|dataset| Self::dataset_stable_id(dataset) == suffix) + } + + pub(super) fn dataset_from_data_id( + &self, + id: &DataId, + ) -> Result<&StacProviderDataset, geoengine_operators::error::Error> { + let external = id + .external() + .ok_or(geoengine_operators::error::Error::DataIdTypeMissMatch)?; + + self.dataset_by_layer_id(&external.layer_id) + .ok_or(geoengine_operators::error::Error::UnknownDataId) + } + + fn paginate( + items: Vec, + options: LayerCollectionListOptions, + ) -> Vec { + items + .into_iter() + .skip(options.offset as usize) + .take(options.limit as usize) + .collect() + } + + fn collection_items_for_path( + &self, + collection: &LayerCollectionId, + collection_path: &str, + ) -> error::Result> { + if collection_path == ROOT_COLLECTION_ID { + return Ok(self.root_collection_items()); + } + + let parts = collection_path.split('/').collect::>(); + + match parts.as_slice() { + [first_dimension_id] => self.items_by_first_dimension(collection, first_dimension_id), + [first_dimension_id, first_value] => self.items_for_second_dimension_selector( + collection, + first_dimension_id, + first_value, + ), + [first_dimension_id, first_value, second_dimension_id] => self + .items_by_second_dimension( + collection, + first_dimension_id, + first_value, + second_dimension_id, + ), + [ + first_dimension_id, + first_value, + second_dimension_id, + second_value, + ] => self.layer_items_for_two_dimension_filters( + collection, + first_dimension_id, + first_value, + second_dimension_id, + second_value, + ), + _ => Err(error::Error::UnknownLayerCollectionId { + id: collection.clone(), + }), + } + } + + fn root_collection_items(&self) -> Vec { + GroupingDimension::all() + .into_iter() + .map(|dimension| { + CollectionItem::Collection(LayerCollectionListing { + r#type: Default::default(), + id: ProviderLayerCollectionId { + provider_id: self.id, + collection_id: LayerCollectionId(dimension.id().to_owned()), + }, + name: format!("By {}", dimension.label()), + description: format!("Browse datasets by {}", dimension.label()), + properties: vec![], + }) + }) + .collect::>() + } + + fn parse_dimension_or_err( + collection: &LayerCollectionId, + dimension_id: &str, + ) -> error::Result { + GroupingDimension::from_id(dimension_id).ok_or(error::Error::UnknownLayerCollectionId { + id: collection.clone(), + }) + } + + fn items_by_first_dimension( + &self, + collection: &LayerCollectionId, + first_dimension_id: &str, + ) -> error::Result> { + let first_dimension = Self::parse_dimension_or_err(collection, first_dimension_id)?; + + Ok(self + .available_values(first_dimension, &[]) + .into_iter() + .map(|(slug, display)| { + CollectionItem::Collection(LayerCollectionListing { + r#type: Default::default(), + id: ProviderLayerCollectionId { + provider_id: self.id, + collection_id: LayerCollectionId(format!( + "{}/{slug}", + first_dimension.id(), + )), + }, + name: display.clone(), + description: format!("Datasets with {} {}", first_dimension.label(), display,), + properties: vec![], + }) + }) + .collect::>()) + } + + fn items_for_second_dimension_selector( + &self, + collection: &LayerCollectionId, + first_dimension_id: &str, + first_value: &str, + ) -> error::Result> { + let first_dimension = Self::parse_dimension_or_err(collection, first_dimension_id)?; + + Ok(GroupingDimension::all() + .into_iter() + .filter(|dimension| *dimension != first_dimension) + .filter(|dimension| { + !self + .available_values(*dimension, &[(first_dimension, first_value)]) + .is_empty() + }) + .map(|dimension| { + CollectionItem::Collection(LayerCollectionListing { + r#type: Default::default(), + id: ProviderLayerCollectionId { + provider_id: self.id, + collection_id: LayerCollectionId(format!( + "{}/{first_value}/{}", + first_dimension.id(), + dimension.id() + )), + }, + name: format!("By {}", dimension.label()), + description: format!( + "Filter datasets by {} with {} {}", + dimension.label(), + first_dimension.label(), + first_value, + ), + properties: vec![], + }) + }) + .collect::>()) + } + + fn items_by_second_dimension( + &self, + collection: &LayerCollectionId, + first_dimension_id: &str, + first_value: &str, + second_dimension_id: &str, + ) -> error::Result> { + let first_dimension = Self::parse_dimension_or_err(collection, first_dimension_id)?; + let second_dimension = Self::parse_dimension_or_err(collection, second_dimension_id)?; + + ensure!( + first_dimension != second_dimension, + error::UnknownLayerCollectionId { + id: collection.clone(), + } + ); + + Ok(self + .available_values(second_dimension, &[(first_dimension, first_value)]) + .into_iter() + .map(|(slug, display)| { + CollectionItem::Collection(LayerCollectionListing { + r#type: Default::default(), + id: ProviderLayerCollectionId { + provider_id: self.id, + collection_id: LayerCollectionId(format!( + "{}/{first_value}/{}/{slug}", + first_dimension.id(), + second_dimension.id(), + )), + }, + name: display.clone(), + description: format!( + "Datasets with {} {} and {} {}", + first_dimension.label(), + first_value, + second_dimension.label(), + display, + ), + properties: vec![], + }) + }) + .collect::>()) + } + + fn layer_items_for_two_dimension_filters( + &self, + collection: &LayerCollectionId, + first_dimension_id: &str, + first_value: &str, + second_dimension_id: &str, + second_value: &str, + ) -> error::Result> { + let first_dimension = Self::parse_dimension_or_err(collection, first_dimension_id)?; + let second_dimension = Self::parse_dimension_or_err(collection, second_dimension_id)?; + + ensure!( + first_dimension != second_dimension, + error::UnknownLayerCollectionId { + id: collection.clone(), + } + ); + + let mut items = self + .datasets + .iter() + .filter(|dataset| { + Self::dataset_matches( + dataset, + &[ + (first_dimension, first_value), + (second_dimension, second_value), + ], + ) + }) + .map(|dataset| { + CollectionItem::Layer(LayerListing { + r#type: Default::default(), + id: ProviderLayerId { + provider_id: self.id, + layer_id: Self::dataset_layer_id(dataset), + }, + name: dataset.name.clone(), + description: dataset.description.clone(), + properties: vec![], + }) + }) + .collect::>(); + + items.sort_by_key(|item| item.name().to_owned()); + + Ok(items) + } +} + +#[async_trait] +impl LayerCollectionProvider for StacDataProvider { + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities { + listing: true, + search: SearchCapabilities::none(), + } + } + + fn name(&self) -> &str { + &self.name + } + + fn description(&self) -> &str { + &self.description + } + + async fn load_layer_collection( + &self, + collection: &LayerCollectionId, + options: LayerCollectionListOptions, + ) -> error::Result { + let collection_path = collection.0.trim_start_matches('/'); + let items = self.collection_items_for_path(collection, collection_path)?; + + Ok(LayerCollection { + id: ProviderLayerCollectionId { + provider_id: self.id, + collection_id: collection.clone(), + }, + name: self.name.clone(), + description: self.description.clone(), + items: Self::paginate(items, options), + entry_label: None, + properties: vec![], + }) + } + + async fn get_root_layer_collection_id(&self) -> error::Result { + Ok(LayerCollectionId(ROOT_COLLECTION_ID.to_owned())) + } + + async fn load_layer(&self, id: &LayerId) -> error::Result { + let dataset = self + .dataset_by_layer_id(id) + .ok_or(error::Error::UnknownLayerId { id: id.clone() })?; + + Ok(Layer { + id: ProviderLayerId { + provider_id: self.id, + layer_id: id.clone(), + }, + name: dataset.name.clone(), + description: dataset.description.clone(), + workflow: Workflow::Legacy { + operator: TypedOperator::Raster( + MultiBandGdalSource { + params: MultiBandGdalSourceParameters::new(NamedData { + namespace: None, + provider: Some(self.id.to_string()), + name: id.to_string(), + }), + } + .boxed(), + ), + }, + symbology: None, + properties: vec![], + metadata: std::collections::HashMap::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use geoengine_datatypes::dataset::DataProviderId; + use geoengine_datatypes::primitives::{ + RegularTimeDimension, SpatialResolution, TimeDimension, TimeGranularity, TimeStep, + }; + use geoengine_datatypes::raster::{GeoTransform, GridBoundingBox2D, GridIdx2D}; + use geoengine_datatypes::spatial_reference::{SpatialReference, SpatialReferenceAuthority}; + use geoengine_datatypes::util::Identifier; + use geoengine_operators::engine::SpatialGridDescriptor; + + fn sample_provider() -> StacDataProvider { + StacDataProvider::new( + DataProviderId::new(), + "Sentinel 2 L2A from STAC".to_owned(), + String::new(), + "http://example.com".to_owned(), + "sentinel-2-l2a".to_owned(), + None, + TimeDimension::Regular(RegularTimeDimension::new_with_epoch_origin(TimeStep { + granularity: TimeGranularity::Days, + step: 1, + })), + vec![StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U16 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U16, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new(GridIdx2D::new([0, 0]), GridIdx2D::new([10979, 10979])) + .unwrap(), + ), + bands: vec![], + }], + ) + } + + fn sample_provider_with_projection_variants() -> StacDataProvider { + StacDataProvider::new( + DataProviderId::new(), + "Sentinel 2 L2A from STAC".to_owned(), + String::new(), + "http://example.com".to_owned(), + "sentinel-2-l2a".to_owned(), + None, + TimeDimension::Regular(RegularTimeDimension::new_with_epoch_origin(TimeStep { + granularity: TimeGranularity::Days, + step: 1, + })), + vec![ + StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U16 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U16, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([10979, 10979]), + ) + .unwrap(), + ), + bands: vec![], + }, + StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32633 U16 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U16, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32633), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([10979, 10979]), + ) + .unwrap(), + ), + bands: vec![], + }, + StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U8 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U8, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([10979, 10979]), + ) + .unwrap(), + ), + bands: vec![], + }, + ], + ) + } + + #[test] + fn dataset_stable_id_uses_projection_type_and_resolution() { + let provider = sample_provider(); + let dataset = &provider.datasets[0]; + + assert_eq!( + StacDataProvider::dataset_stable_id(dataset), + "epsg32632_u16_10" + ); + } + + #[tokio::test] + async fn root_layer_collection_lists_grouping_dimensions() { + let provider = sample_provider(); + let root = provider + .load_layer_collection( + &LayerCollectionId(ROOT_COLLECTION_ID.to_owned()), + Default::default(), + ) + .await + .expect("root collection should load"); + + assert_eq!(root.items.len(), 3); + assert_eq!(root.items[0].name(), "By data type"); + assert_eq!(root.items[1].name(), "By resolution"); + assert_eq!(root.items[2].name(), "By projection"); + } + + #[tokio::test] + async fn deep_path_data_type_then_resolution_yields_projection_specific_layers() { + let provider = sample_provider_with_projection_variants(); + + let by_resolution = provider + .load_layer_collection( + &LayerCollectionId("dataTypes/u16".to_owned()), + Default::default(), + ) + .await + .expect("data type branch should load"); + + assert!( + by_resolution + .items + .iter() + .any(|item| item.name() == "By resolution") + ); + + let resolutions = provider + .load_layer_collection( + &LayerCollectionId("dataTypes/u16/resolutions".to_owned()), + Default::default(), + ) + .await + .expect("resolution branch should load"); + + assert_eq!(resolutions.items.len(), 1); + assert_eq!(resolutions.items[0].name(), "10"); + + let layers = provider + .load_layer_collection( + &LayerCollectionId("dataTypes/u16/resolutions/10".to_owned()), + Default::default(), + ) + .await + .expect("deep path should resolve to layers"); + + assert_eq!(layers.items.len(), 2); + + let mut layer_ids = layers + .items + .iter() + .map(|item| match item { + CollectionItem::Layer(layer) => layer.id.layer_id.0.clone(), + CollectionItem::Collection(_) => { + panic!("deep path should only contain layers") + } + }) + .collect::>(); + layer_ids.sort(); + + assert_eq!( + layer_ids, + vec![ + "dataset/epsg32632_u16_10".to_owned(), + "dataset/epsg32633_u16_10".to_owned(), + ] + ); + } +} diff --git a/geoengine/services/src/datasets/external/stac/loading_info.rs b/geoengine/services/src/datasets/external/stac/loading_info.rs new file mode 100644 index 0000000000..8b5b6c62d3 --- /dev/null +++ b/geoengine/services/src/datasets/external/stac/loading_info.rs @@ -0,0 +1,1092 @@ +use super::{StacDataProvider, StacProviderDataset, StacProviderS3Config}; +use crate::error::Result; +use crate::util::join_base_url_and_path; +use async_trait::async_trait; +use chrono::DateTime as ChronoDateTime; +use geoengine_datatypes::dataset::DataId; +use geoengine_datatypes::operations::reproject::{ + CoordinateProjection, CoordinateProjector, ReprojectClipped, +}; +use geoengine_datatypes::primitives::{ + AxisAlignedRectangle, CacheHint, RasterQueryRectangle, TimeDimension, TimeInstance, + TimeInterval, TryRegularTimeFillIterExt, VectorQueryRectangle, +}; +use geoengine_datatypes::raster::{GeoTransform, GridBoundingBox2D, GridIdx2D, RasterDataType}; +use geoengine_datatypes::spatial_reference::SpatialReference; +use geoengine_operators::engine::{ + MetaData, MetaDataProvider, RasterBandDescriptors, RasterResultDescriptor, TimeDescriptor, + VectorResultDescriptor, +}; +use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; +use geoengine_operators::source::{ + FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters, GdalLoadingInfo, + GdalRetryOptions, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, TileFile, +}; +use serde_json::Value; +use stac::Item; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::Duration; +use tracing::debug; +use url::Url; + +const STAC_QUERY_TIMEOUT_SECS: u64 = 60; + +#[derive(Debug, Clone)] +struct StacMultiBandMetaData { + api_url: String, + collection_name: String, + s3_config: Option, + time_dimension: TimeDimension, + dataset: StacProviderDataset, +} + +#[derive(Debug, Clone)] +enum StacQueryState { + FirstPage { + query_url: Url, + query_params: Vec<(String, String)>, + }, + NextPage { + next_url: Url, + }, + Finished, +} + +async fn query_stac_item_collection( + client: &reqwest::Client, + query_state: &StacQueryState, +) -> geoengine_operators::util::Result<(stac::ItemCollection, StacQueryState)> { + match query_state { + StacQueryState::FirstPage { + query_url, + query_params, + } => { + debug!("STAC query first page with parameters: {:?}", query_params); + + let request_started = std::time::Instant::now(); + + let item_collection: stac::ItemCollection = client + .get(query_url.clone()) + .query(query_params) + .send() + .await + .map_err( + |e| geoengine_operators::error::Error::QueryingProcessorFailed { + source: Box::new(e), + }, + )? + .json() + .await + .map_err( + |e| geoengine_operators::error::Error::QueryingProcessorFailed { + source: Box::new(e), + }, + )?; + + debug!( + "STAC response received in {:?} s", + request_started.elapsed().as_secs_f64() + ); + + let next_state = item_collection + .links + .iter() + .find(|link| link.rel == "next") + .and_then(|link| Url::parse(&link.href).ok()) + .map_or(StacQueryState::Finished, |next_url| { + StacQueryState::NextPage { next_url } + }); + + Ok((item_collection, next_state)) + } + StacQueryState::NextPage { next_url } => { + debug!("STAC query next page with url: {}", next_url); + + let request_started = std::time::Instant::now(); + + let item_collection: stac::ItemCollection = client + .get(next_url.clone()) + .send() + .await + .map_err( + |e| geoengine_operators::error::Error::QueryingProcessorFailed { + source: Box::new(e), + }, + )? + .json() + .await + .map_err( + |e| geoengine_operators::error::Error::QueryingProcessorFailed { + source: Box::new(e), + }, + )?; + + debug!( + "STAC response received in {:?} s", + request_started.elapsed().as_secs_f64() + ); + + let next_state = item_collection + .links + .iter() + .find(|link| link.rel == "next") + .and_then(|link| Url::parse(&link.href).ok()) + .map_or(StacQueryState::Finished, |next_url| { + StacQueryState::NextPage { next_url } + }); + + Ok((item_collection, next_state)) + } + StacQueryState::Finished => { + Err(geoengine_operators::error::Error::QueryingProcessorFailed { + source: "no more STAC pages to query".into(), + }) + } + } +} + +#[async_trait] +impl + MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > for StacMultiBandMetaData +{ + async fn loading_info( + &self, + query: MultiBandGdalLoadingInfoQueryRectangle, + ) -> geoengine_operators::util::Result { + let base_url = Url::from_str(&self.api_url) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + let items_url = join_base_url_and_path( + &base_url, + &format!("collections/{}/items", self.collection_name), + ) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + let time_interval = + stac_query_time_interval(query.query_rectangle.time_interval(), self.time_dimension)?; + + let query_params = self.create_stac_query_params(&query, time_interval)?; + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(STAC_QUERY_TIMEOUT_SECS)) + .build() + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + let mut query_state = StacQueryState::FirstPage { + query_url: items_url, + query_params, + }; + + let mut files = Vec::new(); + let mut time_steps = Vec::new(); + + while !matches!(query_state, StacQueryState::Finished) { + let (item_collection, next_state) = + query_stac_item_collection(&client, &query_state).await?; + + for item in item_collection.items { + self.process_stac_item(&item, &mut time_steps, &mut files, query.fetch_tiles) + .map_err(|e| geoengine_operators::error::Error::LoadingInfo { + source: Box::new(e), + })?; + } + + query_state = next_state; + } + + time_steps.sort_by(|a, b| a.start().cmp(&b.start()).then(a.end().cmp(&b.end()))); + time_steps.dedup(); + + let time_steps = match self.time_dimension { + TimeDimension::Regular(regular) => time_steps + .into_iter() + .map(Ok::<_, geoengine_operators::error::Error>) + .try_time_regular_range_fill(regular, time_interval) + .collect::, _>>() + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?, + TimeDimension::Irregular => { + unreachable!("irregular time dimension rejected at provider initialization") + } + }; + + files.sort_by(|a, b| { + a.time + .start() + .cmp(&b.time.start()) + .then(a.time.end().cmp(&b.time.end())) + .then(a.band.cmp(&b.band)) + .then(a.z_index.cmp(&b.z_index)) + }); + + Ok(MultiBandGdalLoadingInfo::new( + time_steps, + files, + CacheHint::default(), + )) + } + + async fn result_descriptor(&self) -> geoengine_operators::util::Result { + Ok(RasterResultDescriptor { + data_type: self.dataset.data_type, + spatial_reference: self.dataset.projection.into(), + time: TimeDescriptor { + bounds: None, + dimension: self.time_dimension, + }, + spatial_grid: self.dataset.spatial_grid, + bands: RasterBandDescriptors::new_multiple_bands(self.dataset.bands.len() as u32), + }) + } + + fn box_clone( + &self, + ) -> Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + > { + Box::new(self.clone()) + } +} + +#[async_trait] +impl MetaDataProvider + for StacDataProvider +{ + async fn meta_data( + &self, + _id: &DataId, + ) -> Result< + Box>, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + +#[async_trait] +impl MetaDataProvider + for StacDataProvider +{ + async fn meta_data( + &self, + _id: &DataId, + ) -> Result< + Box>, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + +#[async_trait] +impl + MetaDataProvider + for StacDataProvider +{ + async fn meta_data( + &self, + _id: &DataId, + ) -> Result< + Box< + dyn MetaData< + MockDatasetDataSourceLoadingInfo, + VectorResultDescriptor, + VectorQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotImplemented) + } +} + +impl StacMultiBandMetaData { + fn create_stac_query_params( + &self, + query: &MultiBandGdalLoadingInfoQueryRectangle, + time_interval: TimeInterval, + ) -> geoengine_operators::util::Result> { + let bbox = stac_query_bbox( + query.query_rectangle.spatial_bounds(), + self.dataset.projection, + )?; + + let time_start = time_interval.start(); + let time_end = time_interval.end(); + + let query_params = vec![ + ( + "bbox".to_owned(), + format!( + "{},{},{},{}", + bbox.lower_left().x, + bbox.lower_left().y, + bbox.upper_right().x, + bbox.upper_right().y + ), + ), + ( + "datetime".to_owned(), + format!( + "{}/{}", + time_start + .as_date_time() + .ok_or(geoengine_operators::error::Error::InvalidDataProviderConfig)? + .to_datetime_string_with_millis(), + time_end + .as_date_time() + .ok_or(geoengine_operators::error::Error::InvalidDataProviderConfig)? + .to_datetime_string_with_millis(), + ), + ), + ("limit".to_owned(), "100".to_owned()), + ( + "fields".to_owned(), + "stac_version,properties.datetime,properties.updated,assets.*.title,assets.*.href,assets.*.data_type,assets.*.bands,assets.*.proj:code,assets.*.proj:shape,assets.*.proj:transform".to_owned(), + ), + ]; + + Ok(query_params) + } + + fn process_stac_item( + &self, + item: &Item, + time_steps: &mut Vec, + files: &mut Vec, + fetch_tiles: bool, + ) -> Result<()> { + if item.version != stac::Version::v1_1_0 { + tracing::warn!( + "Skipping STAC item with unsupported version: {:?}", + item.version + ); + return Ok(()); + } + + let Some(item_datetime) = item.properties.datetime else { + tracing::warn!("Skipping STAC item without datetime: {}", item.id); + return Ok(()); + }; + + let z_index = item + .properties + .updated + .as_deref() + .and_then(|updated| ChronoDateTime::parse_from_rfc3339(updated).ok()) + .map_or_else( + || item_datetime.timestamp_millis(), + |updated| updated.timestamp_millis(), + ); + + let item_time = TimeInstance::from_millis(item_datetime.timestamp_millis()) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + let time = match self.time_dimension { + TimeDimension::Regular(regular) => { + let time_start = regular + .snap_prev(item_time) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + let time_end = (time_start + regular.step) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + TimeInterval::new(time_start, time_end) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)? + } + TimeDimension::Irregular => { + unreachable!("irregular time dimension rejected at provider initialization") + } + }; + + time_steps.push(time); + + if !fetch_tiles { + tracing::trace!( + "STAC query does not require fetching tiles, skipping item with id: {}", + item.id + ); + return Ok(()); + } + + for (_asset_key, asset) in &item.assets { + if data_type_from_asset_v1_1_0(&asset) != Some(self.dataset.data_type) { + continue; + } + + if !proj_code_matches_dataset(&asset.additional_fields, self.dataset.projection) { + continue; + } + + let Some(geo_transform) = geo_transform_from_fields(&asset.additional_fields) else { + tracing::warn!( + "Skipping asset with href {} due to missing geo transform", + asset.href + ); + continue; + }; + + let Some((height, width)) = proj_shape_from_fields(&asset.additional_fields) else { + tracing::warn!( + "Skipping asset with href {} due to missing projection shape", + asset.href + ); + continue; + }; + + if (geo_transform.x_pixel_size().abs() - self.dataset.resolution.x).abs() > 1e-9 + || (geo_transform.y_pixel_size().abs() - self.dataset.resolution.y).abs() > 1e-9 + { + continue; + } + + let Some(asset_title) = asset.title.as_deref() else { + tracing::warn!( + "Skipping asset with href {} due to missing title", + asset.href + ); + continue; + }; + + let grid_bounds = GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([(width as isize) - 1, (height as isize) - 1]), + ) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + let spatial_partition = geo_transform.grid_to_spatial_bounds(&grid_bounds); + + let file_path = gdal_file_path(&asset.href) + .ok_or(geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + let gdal_config_options = self.gdal_config_options_for_file_path(&file_path); + + for (dataset_band_idx, dataset_band) in self.dataset.bands.iter().enumerate() { + if dataset_band.asset_title != asset_title { + continue; + } + + let rasterband_channel = if asset.bands.is_empty() { + if dataset_band.band_name.is_some() { + tracing::warn!( + "STAC asset with href {} does not include bands, but dataset configuration requires a band name. Skipping asset.", + asset.href + ); + continue; + } + + 1 + } else { + let Some(required_band_name) = dataset_band.band_name.as_deref() else { + tracing::warn!( + "STAC asset with href {} includes bands, but dataset configuration does not specify a band name. Skipping asset.", + asset.href + ); + continue; + }; + + let Some(asset_band_idx) = asset.bands.iter().position(|asset_band| { + asset_band.name.as_deref() == Some(required_band_name) + }) else { + tracing::debug!( + "Skipping asset with href {} due to missing required band {}", + asset.href, + required_band_name + ); + continue; + }; + + asset_band_idx + 1 + }; + + files.push(TileFile { + time, + spatial_partition, + band: dataset_band_idx as u32, + z_index, + params: GdalDatasetParameters { + file_path: file_path.clone(), + rasterband_channel, + geo_transform: GdalDatasetGeoTransform { + origin_coordinate: geo_transform.origin_coordinate(), + x_pixel_size: geo_transform.x_pixel_size(), + y_pixel_size: geo_transform.y_pixel_size(), + }, + width, + height, + file_not_found_handling: FileNotFoundHandling::Error, + no_data_value: None, + properties_mapping: None, + gdal_open_options: None, + gdal_config_options: gdal_config_options.clone(), + allow_alphaband_as_mask: false, + retry: Some(GdalRetryOptions { max_retries: 99 }), // TODO: make configurable? + }, + }); + } + } + + Ok(()) + } + + fn gdal_config_options_for_file_path(&self, file_path: &Path) -> Option> { + let file_path_str = file_path.to_string_lossy(); + let is_vsi_s3 = file_path_str.starts_with("/vsis3/"); + let is_vsi_curl = file_path_str.starts_with("/vsicurl/"); + + if !is_vsi_s3 && !is_vsi_curl { + return None; + } + + let mut options = vec![ + ( + "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(), + "EMPTY_DIR".to_owned(), + ), + ( + "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(), + ".tif,.tiff,.jp2".to_owned(), + ), + ]; + + if !is_vsi_s3 { + return Some(options); + } + + if let Some(config) = self.s3_config.as_ref() { + options.push(("AWS_S3_ENDPOINT".to_owned(), config.endpoint.clone())); + options.push(("AWS_VIRTUAL_HOSTING".to_owned(), "FALSE".to_owned())); // TODO: make configurable? + + if let Some(access_key) = &config.access_key { + options.push(("AWS_ACCESS_KEY_ID".to_owned(), access_key.clone())); + } + + if let Some(secret_key) = &config.secret_key { + options.push(("AWS_SECRET_ACCESS_KEY".to_owned(), secret_key.clone())); + } + } + + Some(options) + } +} + +fn stac_query_bbox( + spatial_bounds: geoengine_datatypes::primitives::SpatialPartition2D, + spatial_reference: SpatialReference, +) -> geoengine_operators::util::Result { + let projector = + CoordinateProjector::from_known_srs(spatial_reference, SpatialReference::epsg_4326()) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + spatial_bounds + .as_bbox() + .reproject_clipped(&projector) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)? + .ok_or(geoengine_operators::error::Error::InvalidDataProviderConfig) +} + +fn stac_query_time_interval( + query_time_interval: TimeInterval, + time_dimension: TimeDimension, +) -> geoengine_operators::util::Result { + match time_dimension { + TimeDimension::Regular(regular) => { + let start = regular + .snap_prev(query_time_interval.start()) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + let end = regular + .snap_next(query_time_interval.end()) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)?; + + let end = if end <= start { + (start + regular.step) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig)? + } else { + end + }; + + TimeInterval::new(start, end) + .map_err(|_e| geoengine_operators::error::Error::InvalidDataProviderConfig) + } + TimeDimension::Irregular => Ok(query_time_interval), + } +} + +fn gdal_file_path(href: &str) -> Option { + if href.starts_with("http") { + return Some(PathBuf::from(format!("/vsicurl/{href}"))); + } + + href.strip_prefix("s3://") + .map(|s3_path| PathBuf::from(format!("/vsis3/{s3_path}"))) +} + +fn proj_shape_from_fields(fields: &serde_json::Map) -> Option<(usize, usize)> { + let proj_shape = fields.get("proj:shape")?.as_array()?; + if proj_shape.len() != 2 { + return None; + } + + let height = proj_shape.first()?.as_u64()? as usize; + let width = proj_shape.get(1)?.as_u64()? as usize; + + Some((height, width)) +} + +fn geo_transform_from_fields(fields: &serde_json::Map) -> Option { + let proj_transform = fields.get("proj:transform")?; + let proj_transform_array = proj_transform.as_array()?; + if proj_transform_array.len() != 6 { + return None; + } + + let proj_transform_values = proj_transform_array + .iter() + .map(Value::as_f64) + .collect::>>()?; + + let gdal_geotransform = [ + proj_transform_values[2], + proj_transform_values[0], + proj_transform_values[1], + proj_transform_values[5], + proj_transform_values[3], + proj_transform_values[4], + ]; + + Some(gdal_geotransform.into()) +} + +fn data_type_from_asset_v1_1_0(asset: &stac::Asset) -> Option { + asset + .data_type + .as_ref() + .and_then(raster_data_type_from_stac_data_type) +} + +fn raster_data_type_from_stac_data_type( + data_type: &stac_extensions::raster::DataType, +) -> Option { + match data_type { + stac_extensions::raster::DataType::UInt8 => Some(RasterDataType::U8), + stac_extensions::raster::DataType::UInt16 => Some(RasterDataType::U16), + stac_extensions::raster::DataType::UInt32 => Some(RasterDataType::U32), + stac_extensions::raster::DataType::Int16 => Some(RasterDataType::I16), + stac_extensions::raster::DataType::Int32 => Some(RasterDataType::I32), + stac_extensions::raster::DataType::Float32 => Some(RasterDataType::F32), + stac_extensions::raster::DataType::Float64 => Some(RasterDataType::F64), + _ => None, + } +} + +fn proj_code_matches_dataset( + fields: &serde_json::Map, + dataset_projection: SpatialReference, +) -> bool { + let Some(code) = fields.get("proj:code") else { + return false; + }; + + let Some(proj_code) = proj_code_as_srs_string(code) else { + return false; + }; + + proj_code == dataset_projection.to_string() +} + +fn proj_code_as_srs_string(value: &Value) -> Option { + if let Some(code_number) = value.as_u64() { + return Some(format!("EPSG:{code_number}")); + } + + let code_str = value.as_str()?.trim(); + if code_str.contains(':') { + return Some(code_str.to_ascii_uppercase()); + } + + if let Ok(code_number) = code_str.parse::() { + return Some(format!("EPSG:{code_number}")); + } + + None +} + +#[async_trait] +impl + MetaDataProvider< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > for StacDataProvider +{ + async fn meta_data( + &self, + id: &DataId, + ) -> geoengine_operators::util::Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + > { + let dataset = self.dataset_from_data_id(id)?; + + Ok(Box::new(StacMultiBandMetaData { + api_url: self.api_url.clone(), + collection_name: self.collection_name.clone(), + s3_config: self.s3_config.clone(), + time_dimension: self.time_dimension, + dataset: dataset.clone(), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contexts::{ApplicationContext, PostgresContext, SessionContext}; + use crate::layers::storage::LayerProviderDb; + use crate::util::tests::admin_login; + use geoengine_datatypes::dataset::{DataProviderId, ExternalDataId}; + use geoengine_datatypes::primitives::{ + BandSelection, DateTime, RegularTimeDimension, SpatialPartition2D, SpatialResolution, + TimeGranularity, TimeStep, + }; + use geoengine_datatypes::raster::{ + GeoTransform, GridBoundingBox2D, GridIdx2D, GridShape, TileInformation, + }; + use geoengine_datatypes::spatial_reference::{SpatialReference, SpatialReferenceAuthority}; + use geoengine_datatypes::util::Identifier; + use geoengine_operators::engine::SpatialGridDescriptor; + use geoengine_operators::engine::{ + MetaData, MetaDataProvider, RasterResultDescriptor, WorkflowOperatorPath, + }; + use geoengine_operators::source::{ + MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + }; + use httptest::{Expectation, Server, matchers::request, responders}; + use tokio_postgres::NoTls; + + fn make_stac_provider_def( + provider_id: DataProviderId, + api_url: String, + ) -> crate::datasets::external::stac::StacDataProviderDefinition { + crate::datasets::external::stac::StacDataProviderDefinition { + name: "Sentinel 2 L2A from STAC".to_owned(), + id: provider_id, + description: String::new(), + priority: Some(50), + api_url, + collection_name: "sentinel-2-l2a".to_owned(), + s3_config: None, + time_dimension: TimeDimension::Regular(RegularTimeDimension::new_with_epoch_origin( + TimeStep { + granularity: TimeGranularity::Days, + step: 1, + }, + )), + datasets: vec![crate::datasets::external::stac::StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U16 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U16, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new(GridIdx2D::new([0, 0]), GridIdx2D::new([10979, 10979])) + .unwrap(), + ), + bands: vec![ + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "NIR 1 (band 8) - 10m".to_owned(), + band_name: Some("B08".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Red (band 4) - 10m".to_owned(), + band_name: Some("B04".to_owned()), + }, + ], + }], + } + } + + fn stac_items_response() -> serde_json::Value { + let json_str = + include_str!("../../../../../test_data/stac_responses/items/code-de-marburg.json"); + serde_json::from_str(json_str).expect("code-de-marburg.json should be valid JSON") + } + + /// Replicates the steps from `test_ndvi.http` without making real web requests: + #[crate::ge_context::test] + #[allow(clippy::too_many_lines)] + async fn ndvi_stac_loading_info(app_ctx: PostgresContext) { + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path( + "GET", + "/collections/sentinel-2-l2a/items", + )) + .times(1..=10) + .respond_with( + responders::status_code(200) + .append_header("Content-Type", "application/json") + .body(serde_json::to_string(&stac_items_response()).unwrap()), + ), + ); + + let provider_id = DataProviderId::new(); + + let admin_session = admin_login(&app_ctx).await; + let admin_ctx = app_ctx.session_context(admin_session); + admin_ctx + .db() + .add_layer_provider( + make_stac_provider_def( + provider_id, + server.url_str("").trim_end_matches('/').to_owned(), + ) + .into(), + ) + .await + .unwrap(); + + let provider = admin_ctx + .db() + .load_layer_provider(provider_id) + .await + .unwrap(); + + let layer_id = geoengine_datatypes::dataset::LayerId("dataset/epsg32632_u16_10".to_owned()); + let data_id: DataId = ExternalDataId { + provider_id, + layer_id, + } + .into(); + + let meta: Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + > = MetaDataProvider::meta_data(provider.as_ref(), &data_id) + .await + .expect("meta_data should succeed"); + + let spatial_bounds = SpatialPartition2D::new( + (499_980.0, 5_800_020.0).into(), + (510_000.0, 5_790_000.0).into(), + ) + .unwrap(); + let time_interval = + TimeInterval::new_instant(DateTime::new_utc(2026, 1, 3, 0, 0, 0)).unwrap(); + + let query = MultiBandGdalLoadingInfoQueryRectangle::new( + spatial_bounds, + time_interval, + BandSelection::new_unchecked(vec![0, 1]), + true, + ); + + let loading_info = meta + .loading_info(query) + .await + .expect("loading_info should succeed"); + + let expected_time = TimeInterval::new( + DateTime::new_utc(2026, 1, 3, 0, 0, 0), + DateTime::new_utc(2026, 1, 4, 0, 0, 0), + ) + .unwrap(); + + let time_steps = loading_info.time_steps(); + assert!( + time_steps.iter().any(|ts| ts == &expected_time), + "loading_info should contain time step for 2026-01-03, got {time_steps:?}" + ); + + let tile_geo_transform = GeoTransform::new((499_980.0, 5_800_020.0).into(), 10.0, -10.0); + let tile = TileInformation::new( + GridIdx2D::new([0, 0]), + GridShape::new([10980, 10980]), + tile_geo_transform, + ); + + if let Some(time_step) = time_steps.first() { + let b08_params = loading_info.tile_files(*time_step, tile, 0); + let b04_params = loading_info.tile_files(*time_step, tile, 1); + + assert!( + !b08_params.is_empty(), + "Should have B08 (NIR) band files for {time_step}" + ); + assert!( + !b04_params.is_empty(), + "Should have B04 (Red) band files for {time_step}" + ); + + for param in &b08_params { + assert!( + param.file_path.to_string_lossy().contains("/vsis3/"), + "B08 file should use S3 path: {:?}", + param.file_path + ); + } + for param in &b04_params { + assert!( + param.file_path.to_string_lossy().contains("/vsis3/"), + "B04 file should use S3 path: {:?}", + param.file_path + ); + } + } + } + + #[crate::ge_context::test] + #[allow(clippy::too_many_lines)] + async fn ndvi_stac_workflow(app_ctx: PostgresContext) { + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path( + "GET", + "/collections/sentinel-2-l2a/items", + )) + .times(0..=20) + .respond_with( + responders::status_code(200) + .append_header("Content-Type", "application/json") + .body(serde_json::to_string(&stac_items_response()).unwrap()), + ), + ); + + let provider_id = DataProviderId::new(); + + let admin_session = admin_login(&app_ctx).await; + let admin_ctx = app_ctx.session_context(admin_session); + + let provider_def = crate::datasets::external::stac::StacDataProviderDefinition { + name: "Sentinel 2 L2A from STAC".to_owned(), + id: provider_id, + description: "Test STAC provider for NDVI workflow".to_owned(), + priority: Some(50), + api_url: server.url_str(""), + collection_name: "sentinel-2-l2a".to_owned(), + s3_config: None, + time_dimension: TimeDimension::Regular(RegularTimeDimension::new_with_epoch_origin( + TimeStep { + granularity: TimeGranularity::Days, + step: 1, + }, + )), + datasets: vec![ + crate::datasets::external::stac::StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U16 10m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U16, + resolution: SpatialResolution::new_unchecked(10.0, 10.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 10.0, -10.0), + GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([10979, 10979]), + ) + .unwrap(), + ), + bands: vec![ + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Blue (band 2) - 10m".to_owned(), + band_name: Some("B02".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Green (band 3) - 10m".to_owned(), + band_name: Some("B03".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Water vapour (WVP) - 10m".to_owned(), + band_name: Some("WVP".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "NIR 1 (band 8) - 10m".to_owned(), + band_name: Some("B08".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Red (band 4) - 10m".to_owned(), + band_name: Some("B04".to_owned()), + }, + ], + }, + crate::datasets::external::stac::StacProviderDataset { + name: "Sentinel-2 L2A EPSG:32632 U8 20m".to_owned(), + description: String::new(), + data_type: geoengine_datatypes::raster::RasterDataType::U8, + resolution: SpatialResolution::new_unchecked(20.0, 20.0), + projection: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32632), + spatial_grid: SpatialGridDescriptor::source_from_parts( + GeoTransform::new((399_960.0, 5_700_000.0).into(), 20.0, -20.0), + GridBoundingBox2D::new( + GridIdx2D::new([0, 0]), + GridIdx2D::new([5489, 5489]), + ) + .unwrap(), + ), + bands: vec![ + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Aerosol optical thickness (AOT) - 20m".to_owned(), + band_name: Some("AOT".to_owned()), + }, + crate::datasets::external::stac::StacProviderDatasetBand { + asset_title: "Scene classification map (SCL) - 20m".to_owned(), + band_name: Some("SCL".to_owned()), + }, + ], + }, + ], + }; + + admin_ctx + .db() + .add_layer_provider(provider_def.into()) + .await + .unwrap(); + + let ndvi_workflow_json = + include_str!("../../../../../test_data/api_calls/stac_provider/ndvi-workflow.json"); + + let workflow_json_with_provider = ndvi_workflow_json.replace( + "_:b274275c-373d-4a3f-8b45-9b48e9614329", + &format!("_:{provider_id}"), + ); + + let workflow: crate::workflows::workflow::Workflow = + serde_json::from_str(&workflow_json_with_provider) + .expect("workflow JSON should deserialize"); + + let operator = workflow + .operator() + .expect("workflow should have operator") + .get_raster() + .expect("workflow operator should be raster"); + + let execution_ctx = admin_ctx.execution_context().expect("execution context"); + + let initialized = operator + .clone() + .initialize(WorkflowOperatorPath::initialize_root(), &execution_ctx) + .await + .expect("operator should initialize"); + + // Verify the operator initialized successfully - this tests that the workflow + // can be created and initialized with the STAC provider data + let _result_descriptor = initialized.result_descriptor(); + // If we get here, the operator initialized successfully + } +} diff --git a/geoengine/services/src/datasets/external/stac/mod.rs b/geoengine/services/src/datasets/external/stac/mod.rs new file mode 100644 index 0000000000..4c30931560 --- /dev/null +++ b/geoengine/services/src/datasets/external/stac/mod.rs @@ -0,0 +1,176 @@ +use crate::api::model::services::SECRET_REPLACEMENT; +use crate::contexts::GeoEngineDb; +use crate::datasets::listing::ProvenanceOutput; +use crate::layers::external::{DataProvider, DataProviderDefinition, TypedDataProviderDefinition}; +use async_trait::async_trait; +use geoengine_datatypes::dataset::DataProviderId; +use geoengine_datatypes::primitives::{SpatialResolution, TimeDimension}; +use geoengine_datatypes::raster::RasterDataType; +use geoengine_datatypes::spatial_reference::SpatialReference; +use geoengine_operators::engine::SpatialGridDescriptor; +use postgres_types::{FromSql, ToSql}; +use serde::{Deserialize, Serialize}; + +mod listing; +mod loading_info; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSql, FromSql)] +#[postgres(name = "StacDataProviderDefinition")] +#[serde(rename_all = "camelCase")] +pub struct StacDataProviderDefinition { + pub name: String, + pub id: DataProviderId, + pub description: String, + pub priority: Option, + pub api_url: String, + pub collection_name: String, + pub s3_config: Option, + pub time_dimension: TimeDimension, // TODO: should this be on dataset level? + pub datasets: Vec, + // TODO: page limit(?) +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSql, FromSql)] +#[postgres(name = "StacProviderS3Config")] +pub struct StacProviderS3Config { + pub endpoint: String, + pub access_key: Option, + pub secret_key: Option, +} + +/// A geo engine dataset derived from a STAC collection. +/// As all bands and tiles of a geo engine data set must have the same data type, resolution and projection, +/// a stac collection will be split into multiple geo engine datasets if it contains bands with different data types, resolutions or projections. +/// In order to make them browsable they are defined as part of the stac provider definition. +/// +/// TODO: different approach would be to just provide data type, resolution and projection + bands and compute all combinations as possible datasets, +/// but not all combinations actually exist and would lead to empty collection. +/// +/// TODO: could also be gathered from collection api and probed from items +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSql, FromSql)] +#[postgres(name = "StacProviderDataset")] +pub struct StacProviderDataset { + pub name: String, // TODO: derive from collection name + data type + resolution + projection? + pub description: String, + pub data_type: RasterDataType, + pub resolution: SpatialResolution, + pub projection: SpatialReference, + pub spatial_grid: SpatialGridDescriptor, // TODO: this could be fetched from STAC, however it is dependent on the projection and the STAC collection API does not include this information for all projections but only the first one. so we would have to probe the items API... + pub bands: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSql, FromSql)] +#[postgres(name = "StacProviderDatasetBand")] +pub struct StacProviderDatasetBand { + pub asset_title: String, + pub band_name: Option, +} + +#[async_trait] +impl DataProviderDefinition for StacDataProviderDefinition { + async fn initialize(self: Box, _db: D) -> crate::error::Result> { + if self.time_dimension == TimeDimension::Irregular { + return Err(crate::error::Error::StacIrregularTimeDimensionNotSupported); + } + Ok(Box::new(StacDataProvider::new( + self.id, + self.name, + self.description, + self.api_url, + self.collection_name, + self.s3_config, + self.time_dimension, + self.datasets, + ))) + } + + fn type_name(&self) -> &'static str { + "Stac" + } + + fn name(&self) -> String { + self.name.clone() + } + + fn id(&self) -> DataProviderId { + self.id + } + + fn priority(&self) -> i16 { + self.priority.unwrap_or(0) + } + + async fn update( + &self, + new: TypedDataProviderDefinition, + ) -> crate::error::Result + where + Self: Sized, + { + Ok(match new { + TypedDataProviderDefinition::StacDataProviderDefinition(mut new) => { + if let (Some(current_s3), Some(new_s3)) = (&self.s3_config, &mut new.s3_config) { + if new_s3.access_key.as_deref() == Some(SECRET_REPLACEMENT) { + new_s3.access_key.clone_from(¤t_s3.access_key); + } + + if new_s3.secret_key.as_deref() == Some(SECRET_REPLACEMENT) { + new_s3.secret_key.clone_from(¤t_s3.secret_key); + } + } + + TypedDataProviderDefinition::StacDataProviderDefinition(new) + } + _ => new, + }) + } +} + +#[derive(Debug, Clone)] +pub struct StacDataProvider { + id: DataProviderId, + name: String, + description: String, + api_url: String, + collection_name: String, + s3_config: Option, + time_dimension: TimeDimension, + datasets: Vec, +} + +impl StacDataProvider { + #[allow(clippy::too_many_arguments)] + pub fn new( + id: DataProviderId, + name: String, + description: String, + api_url: String, + collection_name: String, + s3_config: Option, + time_dimension: TimeDimension, + datasets: Vec, + ) -> Self { + Self { + id, + name, + description, + api_url, + collection_name, + s3_config, + time_dimension, + datasets, + } + } +} + +#[async_trait] +impl DataProvider for StacDataProvider { + async fn provenance( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> crate::error::Result { + Err(crate::error::Error::NotImplemented { + message: "STAC provenance is not yet implemented".to_owned(), + }) + } +} diff --git a/geoengine/services/src/datasets/external/wildlive/mod.rs b/geoengine/services/src/datasets/external/wildlive/mod.rs index 6a36bed18d..42f19c04ca 100644 --- a/geoengine/services/src/datasets/external/wildlive/mod.rs +++ b/geoengine/services/src/datasets/external/wildlive/mod.rs @@ -45,9 +45,9 @@ use geoengine_operators::{ mock::MockDatasetDataSourceLoadingInfo, processing::{VectorExpression, VectorExpressionParams}, source::{ - GdalLoadingInfo, OgrSource, OgrSourceColumnSpec, OgrSourceDataset, - OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec, OgrSourceParameters, - OgrSourceTimeFormat, + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSource, OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, + OgrSourceDurationSpec, OgrSourceErrorSpec, OgrSourceParameters, OgrSourceTimeFormat, }, }; use oauth2::AccessToken; @@ -624,6 +624,31 @@ impl MetaDataProvider + MetaDataProvider< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > for WildliveDataConnector +{ + async fn meta_data( + &self, + _id: &geoengine_datatypes::dataset::DataId, + ) -> Result< + Box< + dyn MetaData< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + >, + >, + geoengine_operators::error::Error, + > { + Err(geoengine_operators::error::Error::NotYetImplemented) + } +} + impl WildliveDataConnector { fn layer_id(&self, id: WildliveLayerId) -> Result { Ok(ProviderLayerId { diff --git a/geoengine/services/src/error.rs b/geoengine/services/src/error.rs index 77c7dabb7d..1d6b2cffd3 100644 --- a/geoengine/services/src/error.rs +++ b/geoengine/services/src/error.rs @@ -272,6 +272,8 @@ pub enum Error { }, StacInvalidGeoTransform, StacInvalidBbox, + #[snafu(display("Irregular time dimensions are not supported by the STAC provider"))] + StacIrregularTimeDimensionNotSupported, #[snafu(display( "Failed to parse stac response from '{url}'. Error: {error}\nOriginal Response: {response}" ))] diff --git a/geoengine/services/src/layers/external.rs b/geoengine/services/src/layers/external.rs index 4ff6436c88..2cbbc1f558 100644 --- a/geoengine/services/src/layers/external.rs +++ b/geoengine/services/src/layers/external.rs @@ -12,6 +12,7 @@ use crate::datasets::external::netcdfcf::NetCdfCfDataProviderDefinition; use crate::datasets::external::pangaea::PangaeaDataProviderDefinition; use crate::datasets::external::{ CopernicusDataspaceDataProviderDefinition, SentinelS2L2ACogsProviderDefinition, + StacDataProviderDefinition, }; use crate::datasets::listing::ProvenanceOutput; use crate::error::Result; @@ -24,7 +25,10 @@ use geoengine_operators::engine::{ MetaDataProvider, RasterResultDescriptor, VectorResultDescriptor, }; use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo; -use geoengine_operators::source::{GdalLoadingInfo, OgrSourceDataset}; +use geoengine_operators::source::{ + GdalLoadingInfo, MultiBandGdalLoadingInfo, MultiBandGdalLoadingInfoQueryRectangle, + OgrSourceDataset, +}; use serde::{Deserialize, Serialize}; #[async_trait] @@ -65,7 +69,11 @@ pub trait DataProvider: LayerCollectionProvider + MetaDataProvider + MetaDataProvider + MetaDataProvider - + Send + + MetaDataProvider< + MultiBandGdalLoadingInfo, + RasterResultDescriptor, + MultiBandGdalLoadingInfoQueryRectangle, + > + Send + Sync + std::fmt::Debug + AsAny @@ -89,6 +97,7 @@ pub enum TypedDataProviderDefinition { NetCdfCfDataProviderDefinition(NetCdfCfDataProviderDefinition), PangaeaDataProviderDefinition(PangaeaDataProviderDefinition), SentinelS2L2ACogsProviderDefinition(SentinelS2L2ACogsProviderDefinition), + StacDataProviderDefinition(StacDataProviderDefinition), WildliveDataConnectorDefinition(WildliveDataConnectorDefinition), } @@ -158,6 +167,12 @@ impl From for TypedDataProviderDefinition { } } +impl From for TypedDataProviderDefinition { + fn from(def: StacDataProviderDefinition) -> Self { + Self::StacDataProviderDefinition(def) + } +} + impl From for Box> { fn from(typed: TypedDataProviderDefinition) -> Self { match typed { @@ -178,6 +193,7 @@ impl From for Box { Box::new(def) } + TypedDataProviderDefinition::StacDataProviderDefinition(def) => Box::new(def), TypedDataProviderDefinition::WildliveDataConnectorDefinition(def) => Box::new(def), } } @@ -197,6 +213,7 @@ impl AsRef> for TypedDataProviderD Self::EdrDataProviderDefinition(def) => def, Self::CopernicusDataspaceDataProviderDefinition(def) => def, Self::SentinelS2L2ACogsProviderDefinition(def) => def, + Self::StacDataProviderDefinition(def) => def, Self::WildliveDataConnectorDefinition(def) => def, } } @@ -219,6 +236,7 @@ impl DataProviderDefinition for TypedDataProviderDefinition { Box::new(def).initialize(db).await } Self::SentinelS2L2ACogsProviderDefinition(def) => Box::new(def).initialize(db).await, + Self::StacDataProviderDefinition(def) => Box::new(def).initialize(db).await, Self::WildliveDataConnectorDefinition(def) => Box::new(def).initialize(db).await, } } @@ -246,6 +264,7 @@ impl DataProviderDefinition for TypedDataProviderDefinition { Self::EdrDataProviderDefinition(def) => DataProviderDefinition::::type_name(def), Self::CopernicusDataspaceDataProviderDefinition(_) => "CioDataProviderDefinition", Self::SentinelS2L2ACogsProviderDefinition(_) => "SentinelS2L2ACogsProviderDefinition", + Self::StacDataProviderDefinition(def) => DataProviderDefinition::::type_name(def), Self::WildliveDataConnectorDefinition(def) => { DataProviderDefinition::::type_name(def) } @@ -273,6 +292,7 @@ impl DataProviderDefinition for TypedDataProviderDefinition { Self::SentinelS2L2ACogsProviderDefinition(def) => { DataProviderDefinition::::name(def) } + Self::StacDataProviderDefinition(def) => DataProviderDefinition::::name(def), Self::WildliveDataConnectorDefinition(def) => DataProviderDefinition::::name(def), } } @@ -296,6 +316,7 @@ impl DataProviderDefinition for TypedDataProviderDefinition { DataProviderDefinition::::id(def) } Self::SentinelS2L2ACogsProviderDefinition(def) => DataProviderDefinition::::id(def), + Self::StacDataProviderDefinition(def) => DataProviderDefinition::::id(def), Self::WildliveDataConnectorDefinition(def) => DataProviderDefinition::::id(def), } } @@ -325,6 +346,7 @@ impl DataProviderDefinition for TypedDataProviderDefinition { Self::SentinelS2L2ACogsProviderDefinition(def) => { DataProviderDefinition::::priority(def) } + Self::StacDataProviderDefinition(def) => DataProviderDefinition::::priority(def), Self::WildliveDataConnectorDefinition(def) => { DataProviderDefinition::::priority(def) } @@ -372,6 +394,9 @@ impl DataProviderDefinition for TypedDataProviderDefinition { TypedDataProviderDefinition::SentinelS2L2ACogsProviderDefinition(def) => { DataProviderDefinition::::update(def, other).await } + TypedDataProviderDefinition::StacDataProviderDefinition(def) => { + DataProviderDefinition::::update(def, other).await + } TypedDataProviderDefinition::WildliveDataConnectorDefinition(def) => { DataProviderDefinition::::update(def, other).await } diff --git a/geoengine/services/src/layers/layer.rs b/geoengine/services/src/layers/layer.rs index 621797f1b3..d51a12662f 100644 --- a/geoengine/services/src/layers/layer.rs +++ b/geoengine/services/src/layers/layer.rs @@ -291,7 +291,7 @@ pub struct UpdateLayerCollection { pub properties: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, IntoParams, Validate)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, IntoParams, Validate)] #[into_params(parameter_in = Query)] // TODO: validate user input pub struct LayerCollectionListOptions { diff --git a/geoengine/test_data/api_calls/nsiscloud/test.http b/geoengine/test_data/api_calls/nsiscloud/test.http new file mode 100644 index 0000000000..9db3e5e834 --- /dev/null +++ b/geoengine/test_data/api_calls/nsiscloud/test.http @@ -0,0 +1,117 @@ + +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +{ + "type": "Raster", + "operator": { + "type": "MultiBandGdalSource", + "params": { + "data": "sentinel-2-l2a_EPSG32632_U16_60" + } + } +} + +### + + +# $ AWS_S3_ENDPOINT=eodata.nsiscloud.polsa.gov.pl AWS_HTTPS=YES AWS_VIRTUAL_HOSTING=FALSE AWS_ACCESS_KEY_ID=XX AWS_SECRET_ACCESS_KEY=YY gdalinfo /vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B11_60m.jp2 -mm +# Driver: JP2OpenJPEG/JPEG-2000 driver based on OpenJPEG library +# Files: /vsis3/eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B11_60m.jp2 +# Size is 1830, 1830 +# Coordinate System is: +# PROJCRS["WGS 84 / UTM zone 32N", +# BASEGEOGCRS["WGS 84", +# ENSEMBLE["World Geodetic System 1984 ensemble", +# MEMBER["World Geodetic System 1984 (Transit)"], +# MEMBER["World Geodetic System 1984 (G730)"], +# MEMBER["World Geodetic System 1984 (G873)"], +# MEMBER["World Geodetic System 1984 (G1150)"], +# MEMBER["World Geodetic System 1984 (G1674)"], +# MEMBER["World Geodetic System 1984 (G1762)"], +# MEMBER["World Geodetic System 1984 (G2139)"], +# ELLIPSOID["WGS 84",6378137,298.257223563, +# LENGTHUNIT["metre",1]], +# ENSEMBLEACCURACY[2.0]], +# PRIMEM["Greenwich",0, +# ANGLEUNIT["degree",0.0174532925199433]], +# ID["EPSG",4326]], +# CONVERSION["UTM zone 32N", +# METHOD["Transverse Mercator", +# ID["EPSG",9807]], +# PARAMETER["Latitude of natural origin",0, +# ANGLEUNIT["degree",0.0174532925199433], +# ID["EPSG",8801]], +# PARAMETER["Longitude of natural origin",9, +# ANGLEUNIT["degree",0.0174532925199433], +# ID["EPSG",8802]], +# PARAMETER["Scale factor at natural origin",0.9996, +# SCALEUNIT["unity",1], +# ID["EPSG",8805]], +# PARAMETER["False easting",500000, +# LENGTHUNIT["metre",1], +# ID["EPSG",8806]], +# PARAMETER["False northing",0, +# LENGTHUNIT["metre",1], +# ID["EPSG",8807]]], +# CS[Cartesian,2], +# AXIS["(E)",east, +# ORDER[1], +# LENGTHUNIT["metre",1]], +# AXIS["(N)",north, +# ORDER[2], +# LENGTHUNIT["metre",1]], +# USAGE[ +# SCOPE["Navigation and medium accuracy spatial referencing."], +# AREA["Between 6°E and 12°E, northern hemisphere between equator and 84°N, onshore and offshore. Algeria. Austria. Cameroon. Denmark. Equatorial Guinea. France. Gabon. Germany. Italy. Libya. Liechtenstein. Monaco. Netherlands. Niger. Nigeria. Norway. Sao Tome and Principe. Svalbard. Sweden. Switzerland. Tunisia. Vatican City State."], +# BBOX[0,6,84,12]], +# ID["EPSG",32632]] +# Data axis to CRS axis mapping: 1,2 +# Origin = (399960.000000000000000,5700000.000000000000000) +# Pixel Size = (60.000000000000000,-60.000000000000000) +# Image Structure Metadata: +# COMPRESSION_REVERSIBILITY=LOSSY +# Corner Coordinates: +# Upper Left ( 399960.000, 5700000.000) ( 7d33'37.97"E, 51d26'32.44"N) +# Lower Left ( 399960.000, 5590200.000) ( 7d35'26.60"E, 50d27'18.96"N) +# Upper Right ( 509760.000, 5700000.000) ( 9d 8'25.65"E, 51d27' 3.95"N) +# Lower Right ( 509760.000, 5590200.000) ( 9d 8'15.04"E, 50d27'49.38"N) +# Center ( 454860.000, 5645100.000) ( 8d21'26.32"E, 50d57'20.68"N) +# Band 1 Block=192x192 Type=UInt16, ColorInterp=Gray +# Computed Min/Max=1051.000,16015.000 +# Overviews: 915x915, 458x458, 229x229, 115x115 +# Overviews: arbitrary +# Image Structure Metadata: +# COMPRESSION=JPEG2000 +# NBITS=15 + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:32632 +@width = 1830 +@height = 1830 +@time = 2026-01-03T00%3A00%3A00.000Z + +@minx = 399960.0000000000000000 +@miny = 5590200.0000000000000000 + +@maxx = 509760.0000000000000000 +@maxy = 5700000.0000000000000000 + +@colorizer_min = 1051 +@colorizer_max = 16015 +@band = 8 +@styles = custom%3A%7B%22type%22%3A%22singleBand%22%2C%22band%22%3A{{band}}%2C%22bandColorizer%22%3A%7B%22type%22%3A%22linearGradient%22%2C%22breakpoints%22%3A%5B%7B%22value%22%3A{{colorizer_min}}%2C%22color%22%3A%5B0%2C0%2C0%2C255%5D%7D%2C%7B%22value%22%3A{{colorizer_max}}%2C%22color%22%3A%5B255%2C255%2C255%2C255%5D%7D%5D%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%2C%22overColor%22%3A%5B246%2C250%2C254%2C255%5D%2C%22underColor%22%3A%5B247%2C251%2C255%2C255%5D%7D%7D + +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} + diff --git a/geoengine/test_data/api_calls/stac_provider/ndvi-workflow.json b/geoengine/test_data/api_calls/stac_provider/ndvi-workflow.json new file mode 100644 index 0000000000..9a2d837b57 --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/ndvi-workflow.json @@ -0,0 +1,97 @@ +{ + "type": "Raster", + "operator": { + "params": { + "aggregation": { + "ignoreNoData": true, + "type": "first" + }, + "window": { + "granularity": "months", + "step": 1 + } + }, + "sources": { + "raster": { + "params": { + "expression": "if (A == 3 || (A >= 7 && A <= 11)) { NODATA } else { (B - C) / (B + C) }", + "mapNoData": false, + "outputBand": { + "measurement": { + "measurement": "NDVI", + "type": "continuous", + "unit": "NDVI" + }, + "name": "NDVI" + }, + "outputType": "F32" + }, + "sources": { + "raster": { + "params": { + "renameBands": { + "type": "default" + } + }, + "sources": { + "rasters": [ + { + "params": { + "outputDataType": "U16" + }, + "sources": { + "raster": { + "params": { + "interpolation": "nearestNeighbor", + "outputResolution": { + "type": "fraction", + "x": 2.0, + "y": 2.0 + } + }, + "sources": { + "raster": { + "params": { + "bands": [1] + }, + "sources": { + "raster": { + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u8_20`" + }, + "type": "MultiBandGdalSource" + } + }, + "type": "BandFilter" + } + }, + "type": "Interpolation" + } + }, + "type": "RasterTypeConversion" + }, + { + "params": { + "bands": [3, 4] + }, + "sources": { + "raster": { + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u16_10`" + }, + "type": "MultiBandGdalSource" + } + }, + "type": "BandFilter" + } + ] + }, + "type": "RasterStacker" + } + }, + "type": "Expression" + } + }, + "type": "TemporalRasterAggregation" + } +} diff --git a/geoengine/test_data/api_calls/stac_provider/register_and_share_provider.http b/geoengine/test_data/api_calls/stac_provider/register_and_share_provider.http new file mode 100644 index 0000000000..b7580a8f1f --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/register_and_share_provider.http @@ -0,0 +1,32 @@ +### + +# @name session +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{session.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +@providerId = b274275c-373d-4a3f-8b45-9b48e9614329 +@anonymousRoleId = fd8e87bf-515c-4f36-8da6-1a53702ff102 + +PUT http://localhost:3030/api/permissions +Authorization: Bearer {{session.response.body.$.id}} +Content-Type: application/json + +{ + "resource": { + "type": "provider", + "id": "{{providerId}}" + }, + "roleId": "{{anonymousRoleId}}", + "permission": "Read" +} \ No newline at end of file diff --git a/geoengine/test_data/api_calls/stac_provider/stac.http b/geoengine/test_data/api_calls/stac_provider/stac.http new file mode 100644 index 0000000000..e8cc1636f2 --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/stac.http @@ -0,0 +1,94 @@ + +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +# ### + +# GET http://localhost:3030/api/layers/{{provider.response.body.$.id}}/capabilities +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/collections/{{provider.response.body.$.id}}/root?offset=0&limit=10 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/collections/{{provider.response.body.$.id}}/projections?offset=0&limit=10 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/collections/{{provider.response.body.$.id}}/projections%2Fepsg32632?offset=0&limit=10 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/collections/{{provider.response.body.$.id}}/projections%2Fepsg32632%2FdataTypes?offset=0&limit=10 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/collections/{{provider.response.body.$.id}}/projections%2Fepsg32632%2FdataTypes%2Fu8?offset=0&limit=10 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +# GET http://localhost:3030/api/layers/{{provider.response.body.$.id}}/dataset%2F1 +# Authorization: Bearer {{anonymousSession.response.body.$.id}} + +# ### + +### + +GET http://localhost:3030/api/layers/{{provider.response.body.$.id}}/dataset%2F2 +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +# Register workflow from a STAC-backed layer. The layer uses MultiBandGdalSource. +# @name workflow +POST http://localhost:3030/api/layers/{{provider.response.body.$.id}}/dataset%2F2/workflowId +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +# Resolve metadata to trigger metadata provider access for multiband loading info plumbing. +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:32632 +@width = 1830 +@height = 1830 +@time = 2026-01-03T00%3A00%3A00.000Z + +@minx = 399960.0000000000000000 +@miny = 5590200.0000000000000000 + +@maxx = 509760.0000000000000000 +@maxy = 5700000.0000000000000000 + +@colorizer_min = 1051 +@colorizer_max = 16015 +@band = 9 +@styles = custom%3A%7B%22type%22%3A%22singleBand%22%2C%22band%22%3A{{band}}%2C%22bandColorizer%22%3A%7B%22type%22%3A%22linearGradient%22%2C%22breakpoints%22%3A%5B%7B%22value%22%3A{{colorizer_min}}%2C%22color%22%3A%5B0%2C0%2C0%2C255%5D%7D%2C%7B%22value%22%3A{{colorizer_max}}%2C%22color%22%3A%5B255%2C255%2C255%2C255%5D%7D%5D%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%2C%22overColor%22%3A%5B246%2C250%2C254%2C255%5D%2C%22underColor%22%3A%5B247%2C251%2C255%2C255%5D%7D%7D + +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} + + diff --git a/geoengine/test_data/api_calls/stac_provider/test_ndvi_full_sentinel_tile.http b/geoengine/test_data/api_calls/stac_provider/test_ndvi_full_sentinel_tile.http new file mode 100644 index 0000000000..229f16a40f --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/test_ndvi_full_sentinel_tile.http @@ -0,0 +1,50 @@ +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + + +< ndvi-workflow.json + +### + +# Trigger metadata loading for easier debugging +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:32632 +@width = 10980 +@height = 10980 +@time = 2026-01-01T00%3A00%3A00.000Z + +@minx = 399960.0000000000000000 +@miny = 5590200.0000000000000000 + +@maxx = 509760.0000000000000000 +@maxy = 5700000.0000000000000000 + +@styles = custom%3A%7B%22type%22%3A%22singleBand%22%2C%22band%22%3A0%2C%22bandColorizer%22%3A%7B%22type%22%3A%22linearGradient%22%2C%22breakpoints%22%3A%5B%7B%22value%22%3A-0.1%2C%22color%22%3A%5B0%2C0%2C0%2C255%5D%7D%2C%7B%22value%22%3A0.8%2C%22color%22%3A%5B255%2C255%2C255%2C255%5D%7D%5D%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%2C%22overColor%22%3A%5B246%2C250%2C254%2C255%5D%2C%22underColor%22%3A%5B247%2C251%2C255%2C255%5D%7D%7D + +# January 2026 NDVI request based on provider channels B08 and B04 +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} diff --git a/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile.http b/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile.http new file mode 100644 index 0000000000..834b02c067 --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile.http @@ -0,0 +1,48 @@ +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + + +< ndvi-workflow.json + +### + +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:32632 +@width = 512 +@height = 512 +@time = 2020-08-07T00:00:00.000000Z + +@minx = 399960.0000000000000000 +@miny = 5592480.0000000000000000 + +@maxx = 405080.0000000000000000 +@maxy = 5597600.0000000000000000 + +@styles = custom%3A%7B%22type%22%3A%22singleBand%22%2C%22band%22%3A0%2C%22bandColorizer%22%3A%7B%22type%22%3A%22linearGradient%22%2C%22breakpoints%22%3A%5B%7B%22value%22%3A-0.1%2C%22color%22%3A%5B0%2C0%2C0%2C255%5D%7D%2C%7B%22value%22%3A0.8%2C%22color%22%3A%5B0%2C255%2C0%2C255%5D%7D%5D%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%2C%22overColor%22%3A%5B246%2C250%2C254%2C255%5D%2C%22underColor%22%3A%5B247%2C251%2C255%2C255%5D%7D%7D + +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} diff --git a/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile_single_day.http b/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile_single_day.http new file mode 100644 index 0000000000..beb6ba13ca --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/test_ndvi_geoengine_tile_single_day.http @@ -0,0 +1,131 @@ +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +# Register single-day NDVI workflow (no temporal aggregation) +# A: SCL (20m -> interpolated to 10m), B: NIR (B08), C: Red (B04) +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +{ + "type": "Raster", + "operator": { + "type": "Expression", + "params": { + "expression": "if (A == 3 || (A >= 7 && A <= 11)) { NODATA } else { (B - C) / (B + C) }", + "mapNoData": false, + "outputBand": { + "name": "NDVI", + "measurement": { + "type": "continuous", + "measurement": "NDVI", + "unit": "NDVI" + } + }, + "outputType": "F32" + }, + "sources": { + "raster": { + "type": "RasterStacker", + "params": { + "renameBands": { + "type": "default" + } + }, + "sources": { + "rasters": [ + { + "type": "RasterTypeConversion", + "params": { + "outputDataType": "U16" + }, + "sources": { + "raster": { + "type": "Interpolation", + "params": { + "interpolation": "nearestNeighbor", + "outputResolution": { + "type": "fraction", + "x": 2.0, + "y": 2.0 + } + }, + "sources": { + "raster": { + "type": "BandFilter", + "params": { + "bands": [1] + }, + "sources": { + "raster": { + "type": "MultiBandGdalSource", + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u8_20`" + } + } + } + } + } + } + } + }, + { + "type": "BandFilter", + "params": { + "bands": [3, 4] + }, + "sources": { + "raster": { + "type": "MultiBandGdalSource", + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u16_10`" + } + } + } + } + ] + } + } + } + } +} + +### + +# Trigger metadata loading for easier debugging +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:32632 +@width = 512 +@height = 512 +@time = 2020-08-07T00:00:00.000000Z + +@minx = 399960.0000000000000000 +@miny = 5592480.0000000000000000 + +@maxx = 405080.0000000000000000 +@maxy = 5597600.0000000000000000 + +@styles = custom%3A%7B%22type%22%3A%22singleBand%22%2C%22band%22%3A0%2C%22bandColorizer%22%3A%7B%22type%22%3A%22linearGradient%22%2C%22breakpoints%22%3A%5B%7B%22value%22%3A-0.1%2C%22color%22%3A%5B0%2C0%2C0%2C255%5D%7D%2C%7B%22value%22%3A0.8%2C%22color%22%3A%5B0%2C255%2C0%2C255%5D%7D%5D%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%2C%22overColor%22%3A%5B246%2C250%2C254%2C255%5D%2C%22underColor%22%3A%5B247%2C251%2C255%2C255%5D%7D%7D + +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} diff --git a/geoengine/test_data/api_calls/stac_provider/test_rgb_frontend_wms_request.http b/geoengine/test_data/api_calls/stac_provider/test_rgb_frontend_wms_request.http new file mode 100644 index 0000000000..3e0925f07e --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/test_rgb_frontend_wms_request.http @@ -0,0 +1,64 @@ +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +{ + "type": "Raster", + "operator": { + "type": "Reprojection", + "params": { + "targetSpatialReference": "EPSG:3857" + }, + "sources": { + "source": { + "type": "MultiBandGdalSource", + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u8_10`" + } + } + } + } +} + +### + +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:3857 +@width = 256 +@height = 256 +@time = 2025-08-06T00:00:00.000Z + +@minx = 0 +@miny = 5009377.085697312 + +@maxx = 2504688.5428486555 +@maxy = 7514065.628545968 + +# RGB colorizer uses the 3-band true-color U8 10m stack: B02/B03/B04. +@styles = custom%3A%7B%22type%22%3A%22multiBand%22%2C%22redBand%22%3A2%2C%22greenBand%22%3A1%2C%22blueBand%22%3A0%2C%22redMin%22%3A0%2C%22redMax%22%3A255%2C%22redScale%22%3A1%2C%22greenMin%22%3A0%2C%22greenMax%22%3A255%2C%22greenScale%22%3A1%2C%22blueMin%22%3A0%2C%22blueMax%22%3A255%2C%22blueScale%22%3A1%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%7D + +GET http://localhost:4200/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=TRUE&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} diff --git a/geoengine/test_data/api_calls/stac_provider/test_rgb_geoengine_tile.http b/geoengine/test_data/api_calls/stac_provider/test_rgb_geoengine_tile.http new file mode 100644 index 0000000000..48282fa2fe --- /dev/null +++ b/geoengine/test_data/api_calls/stac_provider/test_rgb_geoengine_tile.http @@ -0,0 +1,67 @@ +### + +# @name anonymousSession +POST http://localhost:3030/api/anonymous +Content-Type: application/json + +### + +# @name provider +POST http://localhost:3030/api/layerDb/providers +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +< ../../provider_defs_api/stac_sentinel2.json + +### + +# Register RGB workflow based on STAC U8 10m dataset and reproject to Web Mercator. +# The 10m stack exposes B02/B03/B04 as bands 1/2/4 in the source. +# @name workflow +POST http://localhost:3030/api/workflow +Authorization: Bearer {{anonymousSession.response.body.$.id}} +Content-Type: application/json + +{ + "type": "Raster", + "operator": { + "type": "Reprojection", + "params": { + "targetSpatialReference": "EPSG:3857" + }, + "sources": { + "source": { + "type": "MultiBandGdalSource", + "params": { + "data": "_:b274275c-373d-4a3f-8b45-9b48e9614329:`dataset/epsg32632_u8_10`" + } + } + } + } +} + +### + +# Trigger metadata loading for easier debugging +GET http://localhost:3030/api/workflow/{{workflow.response.body.$.id}}/metadata +Authorization: Bearer {{anonymousSession.response.body.$.id}} + +### + +@workflowId = {{workflow.response.body.$.id}} +@crs = EPSG:3857 +@width = 512 +@height = 512 +@time = 2020-08-07T00:00:00.000000Z + +@minx = 950000.0000000000000000 +@miny = 6525000.0000000000000000 + +@maxx = 970000.0000000000000000 +@maxy = 6545000.0000000000000000 + +# RGB colorizer uses the already stacked B04/B03/B02 true-color bands from the U8 10m source. +@styles = custom%3A%7B%22type%22%3A%22multiBand%22%2C%22redBand%22%3A2%2C%22greenBand%22%3A1%2C%22blueBand%22%3A0%2C%22redMin%22%3A0%2C%22redMax%22%3A255%2C%22redScale%22%3A1%2C%22greenMin%22%3A0%2C%22greenMax%22%3A255%2C%22greenScale%22%3A1%2C%22blueMin%22%3A0%2C%22blueMax%22%3A255%2C%22blueScale%22%3A1%2C%22noDataColor%22%3A%5B0%2C0%2C0%2C0%5D%7D + +GET http://localhost:3030/api/wms/{{workflowId}}?REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES={{styles}}&TRANSPARENT=true&layers={{workflowId}}&time={{time}}&EXCEPTIONS=application%2Fjson&WIDTH={{width}}&HEIGHT={{height}}&CRS={{crs}}&BBOX={{minx}}%2C{{miny}}%2C{{maxx}}%2C{{maxy}} +Authorization: Bearer {{anonymousSession.response.body.$.id}} diff --git a/geoengine/test_data/provider_defs_api/stac_sentinel2.json b/geoengine/test_data/provider_defs_api/stac_sentinel2.json new file mode 100644 index 0000000000..1b1c2e42d3 --- /dev/null +++ b/geoengine/test_data/provider_defs_api/stac_sentinel2.json @@ -0,0 +1,416 @@ +{ + "type": "StacProviderDefinition", + "id": "b274275c-373d-4a3f-8b45-9b48e9614329", + "name": "Sentinel 2 L2A from STAC", + "description": "", + "priority": 50, + "apiUrl": "https://stac.nsiscloud.polsa.gov.pl/v1", + "collectionName": "sentinel-2-l2a", + "s3Config": { + "endpoint": "eodata.nsiscloud.polsa.gov.pl", + "accessKey": "XXX", + "secretKey": "XXX" + }, + "timeDimension": { + "type": "regular", + "origin": 0, + "step": { + "granularity": "days", + "step": 1 + } + }, + "datasets": [ + { + "name": "Sentinel-2 L2A EPSG:32632 U16 10m", + "description": "", + "dataType": "U16", + "resolution": { + "x": 10, + "y": 10 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 10, + "yPixelSize": -10 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 10979, + "yIdx": 10979 + } + } + } + }, + "bands": [ + { + "assetTitle": "Aerosol optical thickness (AOT) - 10m", + "bandName": "AOT" + }, + { + "assetTitle": "Blue (band 2) - 10m", + "bandName": "B02" + }, + { + "assetTitle": "Green (band 3) - 10m", + "bandName": "B03" + }, + { + "assetTitle": "NIR 1 (band 8) - 10m", + "bandName": "B08" + }, + { + "assetTitle": "Red (band 4) - 10m", + "bandName": "B04" + }, + { + "assetTitle": "Water vapour (WVP) - 10m", + "bandName": "WVP" + } + ] + }, + { + "name": "Sentinel-2 L2A EPSG:32632 U16 20m", + "description": "", + "dataType": "U16", + "resolution": { + "x": 20, + "y": 20 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 20, + "yPixelSize": -20 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 5489, + "yIdx": 5489 + } + } + } + }, + "bands": [ + { + "assetTitle": "Aerosol optical thickness (AOT) - 20m", + "bandName": "AOT" + }, + { + "assetTitle": "Blue (band 2) - 20m", + "bandName": "B02" + }, + { + "assetTitle": "Coastal aerosol (band 1) - 20m", + "bandName": "B01" + }, + { + "assetTitle": "Green (band 3) - 20m", + "bandName": "B03" + }, + { + "assetTitle": "NIR 2 (band 8A) - 20m", + "bandName": "B8A" + }, + { + "assetTitle": "Red (band 4) - 20m", + "bandName": "B04" + }, + { + "assetTitle": "Red edge 1 (band 5) - 20m", + "bandName": "B05" + }, + { + "assetTitle": "Red edge 2 (band 6) - 20m", + "bandName": "B06" + }, + { + "assetTitle": "Red edge 3 (band 7) - 20m", + "bandName": "B07" + }, + { + "assetTitle": "SWIR 1 (band 11) - 20m", + "bandName": "B11" + }, + { + "assetTitle": "SWIR 2 (band 12) - 20m", + "bandName": "B12" + }, + { + "assetTitle": "Water vapour (WVP) - 20m", + "bandName": "WVP" + } + ] + }, + { + "name": "Sentinel-2 L2A EPSG:32632 U16 60m", + "description": "", + "dataType": "U16", + "resolution": { + "x": 60, + "y": 60 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 60, + "yPixelSize": -60 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 1829, + "yIdx": 1829 + } + } + } + }, + "bands": [ + { + "assetTitle": "Aerosol optical thickness (AOT) - 60m", + "bandName": "AOT" + }, + { + "assetTitle": "Blue (band 2) - 60m", + "bandName": "B02" + }, + { + "assetTitle": "Coastal aerosol (band 1) - 60m", + "bandName": "B01" + }, + { + "assetTitle": "Green (band 3) - 60m", + "bandName": "B03" + }, + { + "assetTitle": "NIR 2 (band 8A) - 60m", + "bandName": "B8A" + }, + { + "assetTitle": "NIR 3 (band 9) - 60m", + "bandName": "B09" + }, + { + "assetTitle": "Red (band 4) - 60m", + "bandName": "B04" + }, + { + "assetTitle": "Red edge 1 (band 5) - 60m", + "bandName": "B05" + }, + { + "assetTitle": "Red edge 2 (band 6) - 60m", + "bandName": "B06" + }, + { + "assetTitle": "Red edge 3 (band 7) - 60m", + "bandName": "B07" + }, + { + "assetTitle": "SWIR 1 (band 11) - 60m", + "bandName": "B11" + }, + { + "assetTitle": "SWIR 2 (band 12) - 60m", + "bandName": "B12" + }, + { + "assetTitle": "Water vapour (WVP) - 60m", + "bandName": "WVP" + } + ] + }, + { + "name": "Sentinel-2 L2A EPSG:32632 U8 10m", + "description": "", + "dataType": "U8", + "resolution": { + "x": 10, + "y": 10 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 10, + "yPixelSize": -10 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 10979, + "yIdx": 10979 + } + } + } + }, + "bands": [ + { + "assetTitle": "True color image", + "bandName": "B02" + }, + { + "assetTitle": "True color image", + "bandName": "B03" + }, + { + "assetTitle": "True color image", + "bandName": "B04" + } + ] + }, + { + "name": "Sentinel-2 L2A EPSG:32632 U8 20m", + "description": "", + "dataType": "U8", + "resolution": { + "x": 20, + "y": 20 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 20, + "yPixelSize": -20 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 5489, + "yIdx": 5489 + } + } + } + }, + "bands": [ + { + "assetTitle": "Cloud probability (CLD) - 20m", + "bandName": "CLD" + }, + { + "assetTitle": "Scene classification map (SCL) - 20m" + }, + { + "assetTitle": "Snow probability (SNW) - 20m", + "bandName": "SNW" + }, + { + "assetTitle": "True color image", + "bandName": "B02" + }, + { + "assetTitle": "True color image", + "bandName": "B03" + }, + { + "assetTitle": "True color image", + "bandName": "B04" + } + ] + }, + { + "name": "Sentinel-2 L2A EPSG:32632 U8 60m", + "description": "", + "dataType": "U8", + "resolution": { + "x": 60, + "y": 60 + }, + "projection": "EPSG:32632", + "spatialGrid": { + "descriptor": "source", + "spatialGrid": { + "geoTransform": { + "originCoordinate": { + "x": 399960, + "y": 5700000 + }, + "xPixelSize": 60, + "yPixelSize": -60 + }, + "gridBounds": { + "topLeftIdx": { + "xIdx": 0, + "yIdx": 0 + }, + "bottomRightIdx": { + "xIdx": 1829, + "yIdx": 1829 + } + } + } + }, + "bands": [ + { + "assetTitle": "Cloud probability (CLD) - 60m", + "bandName": "CLD" + }, + { + "assetTitle": "Scene classification map (SCL) - 60m" + }, + { + "assetTitle": "Snow probability (SNW) - 60m", + "bandName": "SNW" + }, + { + "assetTitle": "True color image", + "bandName": "B02" + }, + { + "assetTitle": "True color image", + "bandName": "B03" + }, + { + "assetTitle": "True color image", + "bandName": "B04" + } + ] + } + ] +} diff --git a/geoengine/test_data/stac_responses/collections/code-de.json b/geoengine/test_data/stac_responses/collections/code-de.json new file mode 100644 index 0000000000..3ca11cc59a --- /dev/null +++ b/geoengine/test_data/stac_responses/collections/code-de.json @@ -0,0 +1,1229 @@ +{ + "id": "sentinel-2-l2a", + "description": "The Sentinel-2 Level-2A Collection 1 product provides orthorectified Surface Reflectance (Bottom-Of-Atmosphere: BOA), with sub-pixel multispectral and multitemporal registration accuracy. Scene Classification (including Clouds and Cloud Shadows), AOT (Aerosol Optical Thickness) and WV (Water Vapour) maps are included in the product.", + "stac_version": "1.1.0", + "links": [ + { + "rel": "aggregate", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/aggregate" + }, + { + "rel": "aggregations", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/aggregations" + }, + { + "rel": "items", + "type": "application/geo+json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/items" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/" + }, + { + "rel": "queryables", + "type": "application/schema+json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/queryables" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + }, + { + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice", + "rel": "license", + "type": "application/pdf", + "title": "Legal notice on the use of Copernicus Sentinel Data and Service Information" + }, + { + "href": "https://ceos.org/ard/files/PFS/SR/v5.0.1/CEOS-ARD_Product_Family_Specification_Surface_Reflectance-v5.0.1.docx", + "rel": "ceos-ard-specification", + "type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "title": "CEOS-ARD Product Specification for Surface Reflectance (Word)" + }, + { + "href": "https://ceos.org/ard/files/PFS/SR/v5.0.1/CEOS-ARD_Product_Family_Specification_Surface_Reflectance-v5.0.1.pdf", + "rel": "ceos-ard-specification", + "type": "application/pdf", + "title": "CEOS-ARD Product Specification for Surface Reflectance (PDF)" + } + ], + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/processing/v1.2.0/schema.json", + "https://stac-extensions.github.io/product/v0.1.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", + "https://stac-extensions.github.io/raster/v2.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.1.0/schema.json", + "https://stac-extensions.github.io/classification/v2.0.0/schema.json", + "https://stac-extensions.github.io/ceos-ard/v0.2.0/schema.json", + "https://stac-extensions.github.io/storage/v2.0.0/schema.json" + ], + "title": "Sentinel-2 Level-2A", + "type": "Collection", + "assets": { + "thumbnail": { + "href": "https://s3.waw3-2.cloudferro.com/swift/v1/stac-png/S2_L2A.jpg", + "type": "image/jpeg", + "title": "Sentinel-2 Level-2A", + "roles": ["thumbnail"], + "proj:code": null, + "proj:shape": [360, 640] + } + }, + "license": "other", + "extent": { + "spatial": { "bbox": [[-180, -90, 180, 90]] }, + "temporal": { "interval": [["2015-06-27T10:25:31Z", null]] } + }, + "keywords": [ + "10m", + "Copernicus", + "ESA", + "EU", + "Global", + "Imagery", + "MSI", + "Optical", + "PT24H", + "Reflectance", + "S2MSI2A", + "Satellite", + "Sentinel", + "Sentinel-2" + ], + "providers": [ + { + "name": "ESA", + "roles": ["producer"], + "url": "https://sentinel.esa.int/web/sentinel/missions/sentinel-2" + }, + { + "name": "European Commission", + "roles": ["licensor"], + "url": "https://commission.europa.eu/" + }, + { + "name": "CloudFerro", + "roles": ["host", "processor"], + "url": "https://cloudferro.com/" + } + ], + "summaries": { + "gsd": [10], + "platform": ["sentinel-2a", "sentinel-2b", "sentinel-2c"], + "instruments": ["msi"], + "constellation": ["sentinel-2"], + "sat:platform_international_designator": [ + "2015-028A", + "2017-013A", + "2024-157A" + ], + "processing:level": ["L2"], + "product:type": ["S2MSI2A"], + "product:timeliness": ["PT24H"], + "product:timeliness_category": ["NRT"], + "statistics": { + "type": "object", + "properties": { + "nodata": { "type": "number", "minimum": 0, "maximum": 100 }, + "saturated_defective": { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "dark_area": { "type": "number", "minimum": 0, "maximum": 100 }, + "cloud_shadow": { "type": "number", "minimum": 0, "maximum": 100 }, + "vegetation": { "type": "number", "minimum": 0, "maximum": 100 }, + "not_vegetated": { "type": "number", "minimum": 0, "maximum": 100 }, + "water": { "type": "number", "minimum": 0, "maximum": 100 }, + "unclassified": { "type": "number", "minimum": 0, "maximum": 100 }, + "medium_proba_clouds": { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "high_proba_clouds": { "type": "number", "minimum": 0, "maximum": 100 }, + "thin_cirrus": { "type": "number", "minimum": 0, "maximum": 100 } + } + } + }, + "ceosard:type": "optical", + "ceosard:specification": "SR", + "ceosard:specification_version": "5.0.1", + "sci:doi": "10.5270/S2_-znk9xsj", + "sci:citation": "Copernicus Sentinel data [Year]", + "auth:schemes": { + "s3": { "type": "s3" }, + "oidc-nsis": { + "type": "openIdConnect", + "openIdConnectUrl": "https://identity.nsiscloud.polsa.gov.pl/auth/realms/NSIS-CLOUD/.well-known/openid-configuration" + } + }, + "storage:schemes": { + "s3-nsis": { + "type": "custom-s3", + "title": "NSIS S3", + "platform": "https://eodata.nsiscloud.polsa.gov.pl", + "description": "Earth Observation archive of the National Satellite Information System (NSIS) coordinated by the Polish Space Agency (POLSA).", + "requester_pays": false + } + }, + "bands": [ + { + "gsd": 60, + "name": "B01", + "eo:common_name": "coastal", + "description": "Coastal aerosol (band 1)" + }, + { + "gsd": 10, + "name": "B02", + "eo:common_name": "blue", + "description": "Blue (band 2)" + }, + { + "gsd": 10, + "name": "B03", + "eo:common_name": "green", + "description": "Green (band 3)" + }, + { + "gsd": 10, + "name": "B04", + "eo:common_name": "red", + "description": "Red (band 4)" + }, + { + "gsd": 20, + "name": "B05", + "eo:common_name": "rededge071", + "description": "Red edge 1 (band 5)" + }, + { + "gsd": 20, + "name": "B06", + "eo:common_name": "rededge075", + "description": "Red edge 2 (band 6)" + }, + { + "gsd": 20, + "name": "B07", + "eo:common_name": "rededge078", + "description": "Red edge 3 (band 7)" + }, + { + "gsd": 10, + "name": "B08", + "eo:common_name": "nir", + "description": "NIR 1 (band 8)" + }, + { + "gsd": 20, + "name": "B8A", + "eo:common_name": "nir08", + "description": "NIR 2 (band 8A)" + }, + { + "gsd": 60, + "name": "B09", + "eo:common_name": "nir09", + "description": "NIR 3 (band 9)" + }, + { + "gsd": 20, + "name": "B11", + "eo:common_name": "swir16", + "description": "SWIR 1 (band 11)" + }, + { + "gsd": 20, + "name": "B12", + "eo:common_name": "swir22", + "description": "SWIR 2 (band 12)" + } + ], + "item_assets": { + "Product": { + "title": "Zipped product", + "type": "application/zip", + "auth:refs": ["oidc"], + "roles": ["data", "metadata", "archive"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + } + }, + "B01_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Coastal aerosol (band 1) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B01", "eo:common_name": "coastal" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:original", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B01_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Coastal aerosol (band 1) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B01", "eo:common_name": "coastal" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:upsampled", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B02_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Blue (band 2) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B02", "eo:common_name": "blue" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "B02_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Blue (band 2) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B02", "eo:common_name": "blue" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B02_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Blue (band 2) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B02", "eo:common_name": "blue" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B03_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Green (band 3) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B03", "eo:common_name": "green" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "B03_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Green (band 3) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B03", "eo:common_name": "green" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B03_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Green (band 3) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B03", "eo:common_name": "green" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B04_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red (band 4) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B04", "eo:common_name": "red" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "B04_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red (band 4) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B04", "eo:common_name": "red" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B04_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red (band 4) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B04", "eo:common_name": "red" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B05_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 1 (band 5) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B05", "eo:common_name": "rededge071" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B05_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 1 (band 5) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B05", "eo:common_name": "rededge071" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B06_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 2 (band 6) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B06", "eo:common_name": "rededge075" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B06_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 2 (band 6) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B06", "eo:common_name": "rededge075" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B07_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 3 (band 7) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B07", "eo:common_name": "rededge078" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B07_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Red edge 3 (band 7) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B07", "eo:common_name": "rededge078" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B08_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "NIR 1 (band 8) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B08", "eo:common_name": "nir" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "B8A_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "NIR 2 (band 8A) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B8A", "eo:common_name": "nir08" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B8A_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "NIR 2 (band 8A) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B8A", "eo:common_name": "nir08" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B09_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "NIR 3 (band 9) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B09", "eo:common_name": "nir09" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:original", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B11_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "SWIR 1 (band 11) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B11", "eo:common_name": "swir16" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B11_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "SWIR 1 (band 11) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B11", "eo:common_name": "swir16" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "B12_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "SWIR 2 (band 12) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B12", "eo:common_name": "swir22" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "B12_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "SWIR 2 (band 12) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [{ "name": "B12", "eo:common_name": "swir22" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "TCI_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "True color image", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [ + { + "name": "B04", + "eo:common_name": "red", + "description": "Red (band 4)" + }, + { + "name": "B03", + "eo:common_name": "green", + "description": "Green (band 3)" + }, + { + "name": "B02", + "eo:common_name": "blue", + "description": "Blue (band 2)" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["visual", "sampling:original", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "TCI_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "True color image", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [ + { + "name": "B04", + "eo:common_name": "red", + "description": "Red (band 4)" + }, + { + "name": "B03", + "eo:common_name": "green", + "description": "Green (band 3)" + }, + { + "name": "B02", + "eo:common_name": "blue", + "description": "Blue (band 2)" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["visual", "sampling:downsampled", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "TCI_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "True color image", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "bands": [ + { + "name": "B04", + "eo:common_name": "red", + "description": "Red (band 4)" + }, + { + "name": "B03", + "eo:common_name": "green", + "description": "Green (band 3)" + }, + { + "name": "B02", + "eo:common_name": "blue", + "description": "Blue (band 2)" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["visual", "sampling:downsampled", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "AOT_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Aerosol optical thickness (AOT) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "AOT_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Aerosol optical thickness (AOT) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "AOT_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Aerosol optical thickness (AOT) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "SCL_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Scene classfication map (SCL) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint8", + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "sampling:original", "gsd:20m"], + "classification:classes": [ + { + "value": 0, + "name": "no_data", + "nodata": true, + "percentage": 25.002557 + }, + { "value": 1, "name": "saturated_or_defective", "percentage": 0 }, + { "value": 2, "name": "dark_area_pixels", "percentage": 6.666911 }, + { "value": 3, "name": "cloud_shadows", "percentage": 0.012537 }, + { "value": 4, "name": "vegetation", "percentage": 7.376051 }, + { "value": 5, "name": "not_vegetated", "percentage": 56.82134 }, + { "value": 6, "name": "water", "percentage": 1.142773 }, + { "value": 7, "name": "unclassified", "percentage": 0.126051 }, + { + "value": 8, + "name": "cloud_medium_probability", + "percentage": 0.028017 + }, + { + "value": 9, + "name": "cloud_high_probability", + "percentage": 0.002491 + }, + { "value": 10, "name": "thin_cirrus", "percentage": 0.135881 }, + { "value": 11, "name": "snow", "percentage": 27.687943 } + ], + "storage:refs": ["s3-nsis"] + }, + "SCL_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Scene classfication map (SCL) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint8", + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "sampling:downsampled", "gsd:60m"], + "classification:classes": [ + { "value": 0, "name": "no_data", "nodata": true }, + { "value": 1, "name": "saturated_or_defective" }, + { "value": 2, "name": "dark_area_pixels" }, + { "value": 3, "name": "cloud_shadows" }, + { "value": 4, "name": "vegetation" }, + { "value": 5, "name": "not_vegetated" }, + { "value": 6, "name": "water" }, + { "value": 7, "name": "unclassified" }, + { "value": 8, "name": "cloud_medium_probability" }, + { "value": 9, "name": "cloud_high_probability" }, + { "value": 10, "name": "thin_cirrus" }, + { "value": 11, "name": "snow" } + ], + "storage:refs": ["s3-nsis"] + }, + "WVP_10m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Water vapour (WVP) - 10m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:shape": [10980, 10980], + "roles": ["data", "gsd:10m"], + "storage:refs": ["s3-nsis"] + }, + "WVP_20m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Water vapour (WVP) - 20m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:shape": [5490, 5490], + "roles": ["data", "gsd:20m"], + "storage:refs": ["s3-nsis"] + }, + "WVP_60m": { + "type": "image/jp2", + "auth:refs": ["s3"], + "alternate:name": "S3", + "title": "Water vapour (WVP) - 60m", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "nodata": 0, + "data_type": "uint16", + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:shape": [1830, 1830], + "roles": ["data", "gsd:60m"], + "storage:refs": ["s3-nsis"] + }, + "thumbnail": { + "type": "image/jpeg", + "roles": ["thumbnail", "overview"], + "title": "Quicklook", + "proj:code": null, + "proj:shape": [343, 343], + "alternate:name": "HTTPS", + "data_type": "uint8", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + }, + "safe_manifest": { + "type": "application/xml", + "auth:refs": ["s3"], + "roles": ["metadata"], + "title": "manifest.safe", + "alternate:name": "S3", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + }, + "product_metadata": { + "type": "application/xml", + "auth:refs": ["s3"], + "roles": ["metadata"], + "title": "MTD_MSIL2A.xml", + "alternate:name": "S3", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + }, + "granule_metadata": { + "type": "application/xml", + "auth:refs": ["s3"], + "roles": ["metadata"], + "title": "MTD_TL.xml", + "alternate:name": "S3", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + }, + "inspire_metadata": { + "type": "application/xml", + "auth:refs": ["s3"], + "roles": ["metadata"], + "title": "INSPIRE.xml", + "alternate:name": "S3", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + }, + "datastrip_metadata": { + "type": "application/xml", + "auth:refs": ["s3"], + "roles": ["metadata"], + "title": "MTD_DS.xml", + "alternate:name": "S3", + "alternate": { + "https": { + "auth:refs": ["oidc-nsis"], + "storage:refs": [], + "alternate:name": "HTTPS" + } + }, + "storage:refs": ["s3-nsis"] + } + } +} diff --git a/geoengine/test_data/stac_responses/collections/element84.json b/geoengine/test_data/stac_responses/collections/element84.json new file mode 100644 index 0000000000..cc99a4e60f --- /dev/null +++ b/geoengine/test_data/stac_responses/collections/element84.json @@ -0,0 +1,998 @@ +{ + "type": "Collection", + "id": "sentinel-2-l2a", + "stac_version": "1.0.0", + "title": "Sentinel-2 Level-2A", + "description": "Global Sentinel-2 data from the Multispectral Instrument (MSI) onboard Sentinel-2", + "links": [ + { + "rel": "self", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + }, + { + "rel": "cite-as", + "href": "https://doi.org/10.5270/S2_-742ikth", + "title": "Copernicus Sentinel-2 MSI Level-2A (L2A) Bottom-of-Atmosphere Radiance" + }, + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice", + "title": "proprietary" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1" + }, + { + "rel": "items", + "type": "application/geo+json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items" + }, + { + "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type": "application/schema+json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/queryables" + }, + { + "rel": "aggregate", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/aggregate", + "method": "GET" + }, + { + "rel": "aggregations", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/aggregations" + } + ], + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.1.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "item_assets": { + "aot": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Aerosol optical thickness (AOT)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "blue": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "coastal": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "granule_metadata": { "type": "application/xml", "roles": ["metadata"] }, + "green": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir08": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir09": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "red": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge1": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge2": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge3": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "scl": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Scene classification map (SCL)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data", "reflectance"] + }, + "swir16": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "swir22": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "thumbnail": { + "type": "image/jpeg", + "title": "Thumbnail image", + "roles": ["thumbnail"] + }, + "tileinfo_metadata": { "type": "application/json", "roles": ["metadata"] }, + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "roles": ["visual"] + }, + "wvp": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Water vapour (WVP)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "cm", + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "aot-jp2": { + "type": "image/jp2", + "title": "Aerosol optical thickness (AOT)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "blue-jp2": { + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "coastal-jp2": { + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "green-jp2": { + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir-jp2": { + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir08-jp2": { + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "nir09-jp2": { + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "red-jp2": { + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge1-jp2": { + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge2-jp2": { + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge3-jp2": { + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "scl-jp2": { + "type": "image/jp2", + "title": "Scene classification map (SCL)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data", "reflectance"] + }, + "swir16-jp2": { + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "swir22-jp2": { + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "scale": 0.0001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + }, + "visual-jp2": { + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4900020], + "roles": ["visual"] + }, + "wvp-jp2": { + "type": "image/jp2", + "title": "Water vapour (WVP)", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4900020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "cm", + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data", "reflectance"] + } + }, + "extent": { + "spatial": { "bbox": [[-180, -90, 180, 90]] }, + "temporal": { "interval": [["2015-06-27T10:25:31.456000Z", null]] } + }, + "license": "proprietary", + "keywords": ["sentinel", "earth observation", "esa"], + "providers": [ + { + "name": "ESA", + "roles": ["producer"], + "url": "https://earth.esa.int/web/guest/home" + }, + { + "name": "Sinergise", + "roles": ["processor"], + "url": "https://registry.opendata.aws/sentinel-2/" + }, + { + "name": "AWS", + "roles": ["host"], + "url": "http://sentinel-pds.s3-website.eu-central-1.amazonaws.com/" + }, + { + "name": "Element 84", + "roles": ["processor"], + "url": "https://element84.com" + } + ], + "summaries": { + "platform": ["sentinel-2a", "sentinel-2b"], + "constellation": ["sentinel-2"], + "instruments": ["msi"], + "gsd": [10, 20, 60], + "view:off_nadir": [0], + "sci:doi": ["10.5270/s2_-znk9xsj"], + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + }, + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + }, + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + }, + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + }, + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + }, + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + }, + { + "name": "cirrus", + "common_name": "cirrus", + "description": "Cirrus (band 10)", + "center_wavelength": 1.3735, + "full_width_half_max": 0.075 + }, + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + }, + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ] + } +} diff --git a/geoengine/test_data/stac_responses/items/code-de-marburg.json b/geoengine/test_data/stac_responses/items/code-de-marburg.json new file mode 100644 index 0000000000..a761c9ef2c --- /dev/null +++ b/geoengine/test_data/stac_responses/items/code-de-marburg.json @@ -0,0 +1,6059 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(b496b0ce-147a-4e5e-9213-c54b8829c789)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R20m/T32UNC_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5800020] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R10m/T32UNC_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5800020] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/IMG_DATA/R60m/T32UNC_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5800020] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(47c91c67-1f14-43b9-8f1d-4aa23f70ac7f)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/GRANULE/L2A_T32UNC_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNC_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.095261Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(d13af8a9-8774-408a-b192-e74dcac86935)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R20m/T32UNB_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5700000] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R10m/T32UNB_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5700000] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/IMG_DATA/R60m/T32UNB_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5700000] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(596763b2-44eb-4eb5-b69f-b8aac7d53acb)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/GRANULE/L2A_T32UNB_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNB_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:44.071183Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(dd77902b-9fb7-4704-b2f4-e63407acf1f6)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R20m/T32UNA_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 499980, 0, -20, 5600040] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R10m/T32UNA_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 499980, 0, -10, 5600040] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/IMG_DATA/R60m/T32UNA_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 499980, 0, -60, 5600040] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(cf9bd1f8-9f1c-479d-b957-b2c5e11e7efd)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/GRANULE/L2A_T32UNA_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UNA_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.725266Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(efa139e0-24c9-4452-98b2-776b81344faf)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R20m/T32UMC_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5800020] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R10m/T32UMC_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5800020] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/IMG_DATA/R60m/T32UMC_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5800020] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2d202376-ffa9-4a12-ac97-294a6a733c99)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/GRANULE/L2A_T32UMC_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMC_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.427993Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(8bc95b30-aa13-46d9-aedd-3981c0b3d66e)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R20m/T32UMB_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5700000] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R10m/T32UMB_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/IMG_DATA/R60m/T32UMB_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5700000] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(06aafedb-7211-4507-99b9-002093e8266f)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/GRANULE/L2A_T32UMB_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMB_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.262600Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(032ff41d-0731-4cf2-903e-3828807e8ec1)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R20m/T32UMA_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 399960, 0, -20, 5600040] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R10m/T32UMA_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 399960, 0, -10, 5600040] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/IMG_DATA/R60m/T32UMA_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 399960, 0, -60, 5600040] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(e70ecdfa-8319-4bfb-a749-9776af4039bf)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/GRANULE/L2A_T32UMA_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32UMA_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:45.142875Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(a5360e85-8630-4b71-8328-4be798b5bed8)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R20m/T32ULC_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5800020] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R10m/T32ULC_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5800020] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/IMG_DATA/R60m/T32ULC_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5800020] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(148c3fb2-c233-4499-b007-6fe54800a3ef)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/GRANULE/L2A_T32ULC_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULC_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:45.960866Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(069361d7-52f3-4e63-90c1-391e35666e9a)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R20m/T32ULB_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5700000] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R10m/T32ULB_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5700000] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/IMG_DATA/R60m/T32ULB_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5700000] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(62c13415-bcc6-440f-b96a-b8b5ffb5c7f3)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/GRANULE/L2A_T32ULB_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULB_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.506407Z" + }, + "stac_version": "1.1.0" + }, + { + "assets": { + "B11_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B11_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_B02_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "B02_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B02_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B11_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.841, + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:common_name": "swir16", + "eo:center_wavelength": 1.614 + } + ], + "title": "SWIR 1 (band 11) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B11_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B03_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B03_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B02_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "Blue (band 2) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B02_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B03_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_B03_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "B03_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + } + ], + "title": "Green (band 3) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B03_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B07_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B07_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B07_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.399, + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:common_name": "rededge078", + "eo:center_wavelength": 0.783 + } + ], + "title": "Red edge 3 (band 7) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B07_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B06_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B06_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B01_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B01_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B8A_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B8A_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "WVP_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_WVP_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "WVP_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_WVP_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B06_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.374, + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:common_name": "rededge075", + "eo:center_wavelength": 0.741 + } + ], + "title": "Red edge 2 (band 6) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B06_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B01_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.228, + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:common_name": "coastal", + "eo:center_wavelength": 0.443 + } + ], + "title": "Coastal aerosol (band 1) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B01_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B8A_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.441, + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:common_name": "nir08", + "eo:center_wavelength": 0.865 + } + ], + "title": "NIR 2 (band 8A) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B8A_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "AOT_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_AOT_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "WVP_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "title": "Water vapour (WVP) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_WVP_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "AOT_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_AOT_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "AOT_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "title": "Aerosol optical thickness (AOT) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_AOT_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "B05_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B05_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "thumbnail": { + "data_type": "uint8", + "proj:shape": [343, 343], + "proj:code": null, + "title": "Quicklook", + "href": "https://datahub.creodias.eu/odata/v1/Assets(e4869900-ec44-4148-91e0-0113310d969f)/$value" + }, + "SCL_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_SCL_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "SNW_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B09_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.479, + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:common_name": "nir09", + "eo:center_wavelength": 0.945 + } + ], + "title": "NIR 3 (band 9) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B09_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B05_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.357, + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:common_name": "rededge071", + "eo:center_wavelength": 0.704 + } + ], + "title": "Red edge 1 (band 5) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B05_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B12_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B12_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "CLD_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B04_60m": { + "data_type": "uint16", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 60m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_B04_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "B12_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 1.16, + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:common_name": "swir22", + "eo:center_wavelength": 2.202 + } + ], + "title": "SWIR 2 (band 12) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B12_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "CLD_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "title": "Cloud probability (CLD) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/QI_DATA/MSK_CLDPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "SCL_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "title": "Scene classification map (SCL) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_SCL_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B04_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_B04_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "TCI_10m": { + "data_type": "uint8", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_TCI_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "SNW_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "title": "Snow probability (SNW) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/QI_DATA/MSK_SNWPRB_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B04_20m": { + "data_type": "uint16", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + } + ], + "title": "Red (band 4) - 20m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_B04_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "TCI_20m": { + "data_type": "uint8", + "proj:shape": [5490, 5490], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R20m/T32ULA_20260103T103441_TCI_20m.jp2", + "proj:transform": [20, 0, 300000, 0, -20, 5600040] + }, + "B08_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.454, + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:common_name": "nir", + "eo:center_wavelength": 0.833 + } + ], + "title": "NIR 1 (band 8) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R10m/T32ULA_20260103T103441_B08_10m.jp2", + "proj:transform": [10, 0, 300000, 0, -10, 5600040] + }, + "TCI_60m": { + "data_type": "uint8", + "proj:shape": [1830, 1830], + "proj:code": "EPSG:32632", + "bands": [ + { + "eo:full_width_half_max": 0.342, + "name": "B04", + "description": "Red (band 4)", + "eo:common_name": "red", + "eo:center_wavelength": 0.665 + }, + { + "eo:full_width_half_max": 0.291, + "name": "B03", + "description": "Green (band 3)", + "eo:common_name": "green", + "eo:center_wavelength": 0.56 + }, + { + "eo:full_width_half_max": 0.267, + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue", + "eo:center_wavelength": 0.493 + } + ], + "title": "True color image", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/IMG_DATA/R60m/T32ULA_20260103T103441_TCI_60m.jp2", + "proj:transform": [60, 0, 300000, 0, -60, 5600040] + }, + "Product": { + "title": "Zipped product", + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(cf85793b-762b-4e1e-9c8c-d94908ff9181)/$value" + }, + "granule_metadata": { + "title": "MTD_TL.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/GRANULE/L2A_T32ULA_A006942_20260103T103622/MTD_TL.xml" + }, + "datastrip_metadata": { + "title": "MTD_DS.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/DATASTRIP/DS_2CPS_20260103T142711_S20260103T103622/MTD_DS.xml" + }, + "safe_manifest": { + "title": "manifest.safe", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/manifest.safe" + }, + "inspire_metadata": { + "title": "INSPIRE.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/INSPIRE.xml" + }, + "product_metadata": { + "title": "MTD_MSIL2A.xml", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/03/S2C_MSIL2A_20260103T103441_N0511_R108_T32ULA_20260103T142711.SAFE/MTD_MSIL2A.xml" + } + }, + "properties": { + "datetime": "2026-01-03T10:34:41.025000Z", + "updated": "2026-03-10T20:57:46.235099Z" + }, + "stac_version": "1.1.0" + } + ], + "links": [ + { + "rel": "root", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/items?bbox=7.560547850100084,50.45526533913283,9.140457957690465,51.45116832125808&datetime=2026-01-03T00:00:00.000Z/2026-01-04T00:00:00.000Z&limit=100&fields=stac_version,properties.datetime,properties.updated,assets.*.title,assets.*.href,assets.*.data_type,assets.*.bands,assets.*.proj:code,assets.*.proj:shape,assets.*.proj:transform" + }, + { + "rel": "collection", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + } + ], + "numberReturned": 9, + "numberMatched": 9 +} diff --git a/geoengine/test_data/stac_responses/items/code-de.json b/geoengine/test_data/stac_responses/items/code-de.json new file mode 100644 index 0000000000..f9b1444e20 --- /dev/null +++ b/geoengine/test_data/stac_responses/items/code-de.json @@ -0,0 +1,1858 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "stac_version": "1.1.0", + "stac_extensions": [ + "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", + "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", + "https://stac-extensions.github.io/classification/v2.0.0/schema.json", + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/file/v2.1.0/schema.json", + "https://stac-extensions.github.io/grid/v1.1.0/schema.json", + "https://stac-extensions.github.io/processing/v1.2.0/schema.json", + "https://stac-extensions.github.io/product/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/raster/v2.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.1.0/schema.json", + "https://stac-extensions.github.io/storage/v2.0.0/schema.json", + "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", + "https://stac-extensions.github.io/view/v1.1.0/schema.json" + ], + "id": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755", + "collection": "sentinel-2-l2a", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [99.0537157026846, 81.29104718150722], + [99.3071609430051, 80.92925761838922], + [99.74923134449617, 80.93234050606884], + [99.39469658449919, 81.12757263766429], + [99.38157739570931, 81.12696360248792], + [99.38135193773752, 81.12707768805082], + [99.37823914450131, 81.12693341294806], + [99.0537157026846, 81.29104718150722] + ] + ] + }, + "bbox": [ + 99.0537157026846, 80.92925761838922, 99.74923134449617, + 81.29104718150722 + ], + "properties": { + "datetime": "2026-03-19T08:45:59.024000Z", + "created": "2026-03-19T11:37:33Z", + "updated": "2026-03-19T12:25:24.103102Z", + "start_datetime": "2026-03-19T08:45:59.024000Z", + "end_datetime": "2026-03-19T08:45:59.024000Z", + "platform": "sentinel-2b", + "instruments": ["msi"], + "constellation": "sentinel-2", + "gsd": 10.0, + "published": "2026-03-19T11:52:28.296536Z", + "eopf:origin_datetime": "2026-03-19T11:37:33.000000Z", + "auth:schemes": { + "s3": { "type": "s3" }, + "oidc-nsis": { + "type": "openIdConnect", + "openIdConnectUrl": "https://identity.nsiscloud.polsa.gov.pl/auth/realms/NSIS-CLOUD/.well-known/openid-configuration" + } + }, + "storage:schemes": { + "s3-nsis": { + "type": "custom-s3", + "title": "NSIS S3", + "platform": "https://eodata.nsiscloud.polsa.gov.pl", + "description": "Earth Observation archive of the National Satellite Information System (NSIS) coordinated by the Polish Space Agency (POLSA).", + "requester_pays": false + } + }, + "processing:level": "L2", + "processing:datetime": "2026-03-19T11:07:55.000000Z", + "product:type": "S2MSI2A", + "product:timeliness": "PT24H", + "product:timeliness_category": "NRT", + "eo:cloud_cover": 100.0, + "eo:snow_cover": 0.0, + "sat:orbit_state": "descending", + "sat:relative_orbit": 107, + "sat:absolute_orbit": 47180, + "sat:platform_international_designator": "2017-013A", + "grid:code": "MGRS-48XVR", + "view:sun_azimuth": 232.058873907605, + "view:sun_elevation": 4.780452252935305, + "eopf:datastrip_id": "S2B_OPER_MSI_L2A_DS_2BPS_20260319T110755_S20260319T084555_N05.12", + "eopf:datatake_id": "GS2B_20260319T084559_047180_N05.12", + "eopf:instrument_mode": "INS-NOBS", + "statistics": { + "nodata": 98.720229, + "saturated_defective": 0.0, + "dark_area": 0.0, + "cloud_shadow": 0.0, + "vegetation": 0.0, + "not_vegetated": 0.0, + "water": 0.0, + "unclassified": 0.0, + "medium_proba_clouds": 5.769654, + "high_proba_clouds": 94.230348, + "thin_cirrus": 0.0 + }, + "processing:version": "05.12", + "view:azimuth": 224.5716176529944, + "view:incidence_angle": 1.6426142459615778, + "processing:software": { "eometadatatool": "260316154030+dirty" }, + "processing:facility": "ESA", + "expires": "2262-01-01T00:00:00.000000Z" + }, + "links": [ + { + "rel": "self", + "type": "application/geo+json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/items/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + }, + { + "rel": "collection", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/" + }, + { + "href": "https://trace.dataspace.copernicus.eu/api/v1/traces/name/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE.zip", + "rel": "version-history", + "type": "application/json", + "title": "Product history record from the CDSE traceability service" + }, + { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/", + "rel": "enclosure", + "type": "application/x-directory", + "title": "S3 path to source directory", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"] + } + ], + "assets": { + "safe_manifest": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/manifest.safe", + "type": "application/xml", + "title": "manifest.safe", + "roles": ["metadata"], + "file:checksum": "d501105a105ce6f70dce0e93ececdee0c47982", + "file:size": 68920, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/manifest.safe", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(manifest.safe)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "product_metadata": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/MTD_MSIL2A.xml", + "type": "application/xml", + "title": "MTD_MSIL2A.xml", + "roles": ["metadata"], + "file:checksum": "d50120bee20afb37e369df626ee41a60465ca17bc17797e505dda4acfcb914a0355b5c", + "file:size": 54531, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/MTD_MSIL2A.xml", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(MTD_MSIL2A.xml)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "granule_metadata": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/MTD_TL.xml", + "type": "application/xml", + "title": "MTD_TL.xml", + "roles": ["metadata"], + "file:checksum": "d50120342f7d089666f0851333705cd7cf7ff7e9e941d87948d2cd67b516d738448b81", + "file:size": 179307, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/MTD_TL.xml", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(MTD_TL.xml)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "inspire_metadata": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/INSPIRE.xml", + "type": "application/xml", + "title": "INSPIRE.xml", + "roles": ["metadata"], + "file:checksum": "d5012058dfd5f833975bdcc781374a92b744c3228bc23ba93d965d9f58ebe5eb428661", + "file:size": 18745, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/INSPIRE.xml", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(INSPIRE.xml)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "datastrip_metadata": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/DATASTRIP/DS_2BPS_20260319T110755_S20260319T084555/MTD_DS.xml", + "type": "application/xml", + "title": "MTD_DS.xml", + "roles": ["metadata"], + "file:checksum": "d5012012a0d9dadea77b3ca007433934d447f029f7c470489924b3234899943fb36309", + "file:size": 8417870, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/DATASTRIP/DS_2BPS_20260319T110755_S20260319T084555/MTD_DS.xml", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(DATASTRIP)/Nodes(DS_2BPS_20260319T110755_S20260319T084555)/Nodes(MTD_DS.xml)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "thumbnail": { + "href": "https://datahub.creodias.eu/odata/v1/Assets(58b8d589-02c2-40ba-b242-d6c08b181924)/$value", + "type": "image/jpeg", + "title": "Quicklook", + "roles": ["thumbnail", "overview"], + "data_type": "uint8", + "proj:code": null, + "proj:shape": [343, 343], + "file:checksum": "d501103df47e85991fd71b8fc9e376a320bff4", + "file:size": 2971, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755-ql.jpg", + "alternate:name": "HTTPS", + "alternate": { + "s3": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755-ql.jpg", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"] + } + } + }, + "B01_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B01_20m.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 20m", + "roles": ["data", "reflectance", "sampling:upsampled", "gsd:20m"], + "bands": [ + { + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:center_wavelength": 0.443, + "eo:full_width_half_max": 0.228, + "eo:common_name": "coastal" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 220.985674320575, + "view:incidence_angle": 2.05201253076965, + "file:checksum": "1620d7879397d716ba117751fd94e7b4d5ee29039960129ea7966832d47ef7f89670", + "file:size": 332482, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B01_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B01_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B01_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B01_60m.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "roles": ["data", "reflectance", "sampling:original", "gsd:60m"], + "bands": [ + { + "name": "B01", + "description": "Coastal aerosol (band 1)", + "eo:center_wavelength": 0.443, + "eo:full_width_half_max": 0.228, + "eo:common_name": "coastal" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 220.985674320575, + "view:incidence_angle": 2.05201253076965, + "file:checksum": "1620bd6b4b7086be3a657f3c47c8f3391c1aaebb4f07cbbc2c089500982bb0d31412", + "file:size": 78873, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B01_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B01_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B02_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B02_10m.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "bands": [ + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "view:azimuth": 234.00877302273, + "view:incidence_angle": 1.12037591704729, + "file:checksum": "1620bb0fe5de708223709aa8e25afa29f7a12b828f1746977ba66e404c73fc9be484", + "file:size": 1816247, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B02_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_B02_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B02_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B02_20m.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 20m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "bands": [ + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 234.00877302273, + "view:incidence_angle": 1.12037591704729, + "file:checksum": "162091706c6f7a61530d7b768d407dc3bc29f0b345460f2df67d6d6dfef5db4170b5", + "file:size": 512014, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B02_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B02_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B02_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B02_60m.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 234.00877302273, + "view:incidence_angle": 1.12037591704729, + "file:checksum": "1620ea022572bb192b2d7485e034b262f37347d88b064c86cf4df4ca581e8ddd7b00", + "file:size": 81886, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B02_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B02_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B03_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B03_10m.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "bands": [ + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "view:azimuth": 225.535583355159, + "view:incidence_angle": 1.3020815602771, + "file:checksum": "162019197233561651bc8f6f70e9235842877e95c4abc543251e28a593f1f1a4f84e", + "file:size": 1754691, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B03_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_B03_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B03_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B03_20m.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 20m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "bands": [ + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 225.535583355159, + "view:incidence_angle": 1.3020815602771, + "file:checksum": "162015b88723301765e8339a033370570a289511b6a1f200614f18154b6506b40246", + "file:size": 497075, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B03_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B03_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B03_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B03_60m.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 225.535583355159, + "view:incidence_angle": 1.3020815602771, + "file:checksum": "162009cc3d911e8583eeb9f53dc9e65ae7c1d313d96709ce0f6c5a720f963a92ac32", + "file:size": 79952, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B03_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B03_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B04_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B04_10m.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "view:azimuth": 227.105990090539, + "view:incidence_angle": 1.46098812343093, + "file:checksum": "1620855d325c4cbf1ad4e2c21fe323aa7d39fe2fa94f8bfe5bd4e7796c383711bc75", + "file:size": 1751572, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B04_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_B04_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B04_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B04_20m.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 20m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:20m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 227.105990090539, + "view:incidence_angle": 1.46098812343093, + "file:checksum": "162042e36a5ec65a69344cd69126e94f9e6e261d200a21985494fc346508894ead23", + "file:size": 498463, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B04_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B04_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B04_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B04_60m.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 227.105990090539, + "view:incidence_angle": 1.46098812343093, + "file:checksum": "1620c064ca122384efab996c9726b0265cab9c08274a69c8d0911db5c36f9256481b", + "file:size": 80648, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B04_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B04_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B05_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B05_20m.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:center_wavelength": 0.704, + "eo:full_width_half_max": 0.357, + "eo:common_name": "rededge071" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 224.546700714363, + "view:incidence_angle": 1.57474196734165, + "file:checksum": "16202ab8be543cb28893b555548c19eaf3ebf08f14b60d504930852266e36ee94f9a", + "file:size": 501239, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B05_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B05_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B05_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B05_60m.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B05", + "description": "Red edge 1 (band 5)", + "eo:center_wavelength": 0.704, + "eo:full_width_half_max": 0.357, + "eo:common_name": "rededge071" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 224.546700714363, + "view:incidence_angle": 1.57474196734165, + "file:checksum": "1620514853b96ede5ffc728881a163ace6849f215f9419e4b79d3c5f463a4fde2439", + "file:size": 81119, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B05_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B05_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B06_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B06_20m.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:center_wavelength": 0.741, + "eo:full_width_half_max": 0.374, + "eo:common_name": "rededge075" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 222.322814148439, + "view:incidence_angle": 1.69167775673831, + "file:checksum": "1620a5037e99bb5e8128e25709b29100b0eba4767c392fefb91779cfa74d721c21a2", + "file:size": 508331, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B06_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B06_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B06_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B06_60m.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B06", + "description": "Red edge 2 (band 6)", + "eo:center_wavelength": 0.741, + "eo:full_width_half_max": 0.374, + "eo:common_name": "rededge075" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 222.322814148439, + "view:incidence_angle": 1.69167775673831, + "file:checksum": "1620d7c8662cd138f1763dfa80d5959bc9908bd7352a0fb103e31f192110f074c1b9", + "file:size": 81635, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B06_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B06_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B07_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B07_20m.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:center_wavelength": 0.783, + "eo:full_width_half_max": 0.399, + "eo:common_name": "rededge078" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 220.359752366298, + "view:incidence_angle": 1.8120977137764, + "file:checksum": "1620e83a1abaac2822763a56b5d16fcf22f8f30d91bd8e9efe6c35355678b68119f6", + "file:size": 507793, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B07_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B07_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B07_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B07_60m.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B07", + "description": "Red edge 3 (band 7)", + "eo:center_wavelength": 0.783, + "eo:full_width_half_max": 0.399, + "eo:common_name": "rededge078" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 220.359752366298, + "view:incidence_angle": 1.8120977137764, + "file:checksum": "16206a7accaecff014f04dfbf23e7089cda696b7bdd6e5fff6499b4d5f6159e891a6", + "file:size": 81540, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B07_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B07_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B08_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B08_10m.jp2", + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "roles": ["data", "reflectance", "sampling:original", "gsd:10m"], + "bands": [ + { + "name": "B08", + "description": "NIR 1 (band 8)", + "eo:center_wavelength": 0.833, + "eo:full_width_half_max": 0.454, + "eo:common_name": "nir" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "view:azimuth": 229.454644128316, + "view:incidence_angle": 1.20702509227639, + "file:checksum": "16209dfbe755609f8e55c40d80a2caed9125d0c9e792d6d6d23c04d154e21fabae5e", + "file:size": 1791116, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_B08_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_B08_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B8A_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B8A_20m.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:center_wavelength": 0.865, + "eo:full_width_half_max": 0.441, + "eo:common_name": "nir08" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 218.635266070521, + "view:incidence_angle": 1.93450016938485, + "file:checksum": "16200de3f1e14660991d2b488e4db0ccde817b6daba3038019e86517000452fc929f", + "file:size": 516604, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B8A_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B8A_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B8A_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B8A_60m.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B8A", + "description": "NIR 2 (band 8A)", + "eo:center_wavelength": 0.865, + "eo:full_width_half_max": 0.441, + "eo:common_name": "nir08" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 218.635266070521, + "view:incidence_angle": 1.93450016938485, + "file:checksum": "1620a27280c00b0f792056067e7c4a67cf68cfcd77428c22bf2121e62d5c82c51ed5", + "file:size": 81846, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B8A_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B8A_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B09_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B09_60m.jp2", + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "roles": ["data", "reflectance", "sampling:original", "gsd:60m"], + "bands": [ + { + "name": "B09", + "description": "NIR 3 (band 9)", + "eo:center_wavelength": 0.945, + "eo:full_width_half_max": 0.479, + "eo:common_name": "nir09" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 215.652199613161, + "view:incidence_angle": 2.19263848829018, + "file:checksum": "16207c35dd03df5cdc531507af1db770305c9ed798a5ee44a062bd58ef5b4deecedd", + "file:size": 69998, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B09_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B09_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B11_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B11_20m.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:center_wavelength": 1.614, + "eo:full_width_half_max": 0.841, + "eo:common_name": "swir16" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 226.407316072289, + "view:incidence_angle": 1.662802095303, + "file:checksum": "162084adf59b29e6dfb19cc4ff821e82f665656a0abc93f42121fa36d8b3e33cddec", + "file:size": 424877, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B11_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B11_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B11_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B11_60m.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B11", + "description": "SWIR 1 (band 11)", + "eo:center_wavelength": 1.614, + "eo:full_width_half_max": 0.841, + "eo:common_name": "swir16" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 226.407316072289, + "view:incidence_angle": 1.662802095303, + "file:checksum": "16206cfdcdf3cf2767166e68d2c3e532ada21d33b0dc6b013bd37dcac94283b5becc", + "file:size": 67973, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B11_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B11_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B12_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B12_20m.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "roles": ["data", "reflectance", "sampling:original", "gsd:20m"], + "bands": [ + { + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:center_wavelength": 2.202, + "eo:full_width_half_max": 1.16, + "eo:common_name": "swir22" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "view:azimuth": 222.123283487889, + "view:incidence_angle": 1.95466182439327, + "file:checksum": "16200cad5bf8b84aba807a14c5f65964f376796f8cdfb5e57ea3b6115d9515e454df", + "file:size": 426621, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_B12_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_B12_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "B12_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B12_60m.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 60m", + "roles": ["data", "reflectance", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B12", + "description": "SWIR 2 (band 12)", + "eo:center_wavelength": 2.202, + "eo:full_width_half_max": 1.16, + "eo:common_name": "swir22" + } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.0001, + "raster:offset": -0.1, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "view:azimuth": 222.123283487889, + "view:incidence_angle": 1.95466182439327, + "file:checksum": "162006e71efd871dc7aefcfcb5c4d62d8ff3f8967858d34e2d58ee0a61f005473883", + "file:size": 68313, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_B12_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_B12_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "TCI_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_TCI_10m.jp2", + "type": "image/jp2", + "title": "True color image", + "roles": ["visual", "sampling:original", "gsd:10m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + }, + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + }, + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "file:checksum": "1620e466290a5b8a1dfb49f65a44c383bc7b781a1f39a3070b4461fb6505fdb7376e", + "file:size": 71295, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_TCI_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_TCI_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "TCI_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_TCI_20m.jp2", + "type": "image/jp2", + "title": "True color image", + "roles": ["visual", "sampling:downsampled", "gsd:20m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + }, + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + }, + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "file:checksum": "16207da5f35405a2a650d4091a216853a043de4be28eded04722245b01dfbf5e9a4d", + "file:size": 33790, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_TCI_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_TCI_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "TCI_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_TCI_60m.jp2", + "type": "image/jp2", + "title": "True color image", + "roles": ["visual", "sampling:downsampled", "gsd:60m"], + "bands": [ + { + "name": "B04", + "description": "Red (band 4)", + "eo:center_wavelength": 0.665, + "eo:full_width_half_max": 0.342, + "eo:common_name": "red" + }, + { + "name": "B03", + "description": "Green (band 3)", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 0.291, + "eo:common_name": "green" + }, + { + "name": "B02", + "description": "Blue (band 2)", + "eo:center_wavelength": 0.493, + "eo:full_width_half_max": 0.267, + "eo:common_name": "blue" + } + ], + "data_type": "uint8", + "nodata": 0, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "file:checksum": "16204f3830fc86684d14b312b93ecf4f49e291f58df8081c64ac6466b298ca5bcf1e", + "file:size": 21202, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_TCI_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_TCI_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "AOT_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_AOT_10m.jp2", + "type": "image/jp2", + "title": "Aerosol optical thickness (AOT) - 10m", + "roles": ["data", "gsd:10m"], + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "file:checksum": "162039f2c2e2273aa7d85baf1b40875cf5a9e7324602c40e2da23d567eec331a81ae", + "file:size": 41424, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_AOT_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_AOT_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "AOT_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_AOT_20m.jp2", + "type": "image/jp2", + "title": "Aerosol optical thickness (AOT) - 20m", + "roles": ["data", "gsd:20m"], + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "file:checksum": "1620fb28b8c1ed77b3a183f40cc66ddd3e05d785bb01f7814394ea84cb88dacef15c", + "file:size": 47070, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_AOT_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_AOT_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "AOT_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_AOT_60m.jp2", + "type": "image/jp2", + "title": "Aerosol optical thickness (AOT) - 60m", + "roles": ["data", "gsd:60m"], + "bands": [ + { "name": "AOT", "description": "Aerosol optical thickness" } + ], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "file:checksum": "1620096d3d2ce214f4ee3ce33448af3cb604b8b965719d7b2a9e1a564971229b4a24", + "file:size": 23088, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_AOT_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_AOT_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "SCL_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_SCL_20m.jp2", + "type": "image/jp2", + "title": "Scene classification map (SCL) - 20m", + "roles": ["data", "sampling:original", "gsd:20m"], + "data_type": "uint8", + "nodata": 0, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "classification:classes": [ + { + "value": 0, + "name": "no_data", + "nodata": true, + "percentage": 98.720229 + }, + { "value": 1, "name": "saturated_or_defective", "percentage": 0.0 }, + { "value": 2, "name": "dark_area_pixels", "percentage": 0.0 }, + { "value": 3, "name": "cloud_shadows", "percentage": 0.0 }, + { "value": 4, "name": "vegetation", "percentage": 0.0 }, + { "value": 5, "name": "not_vegetated", "percentage": 0.0 }, + { "value": 6, "name": "water", "percentage": 0.0 }, + { "value": 7, "name": "unclassified", "percentage": 0.0 }, + { + "value": 8, + "name": "cloud_medium_probability", + "percentage": 5.769654 + }, + { + "value": 9, + "name": "cloud_high_probability", + "percentage": 94.230348 + }, + { "value": 10, "name": "thin_cirrus", "percentage": 0.0 }, + { "value": 11, "name": "snow", "percentage": 0.0 } + ], + "file:checksum": "1620bee4557a74c5de19e67f149808ff7a6de65501ecf0a6e2a253ee537e12d38b44", + "file:size": 40775, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_SCL_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_SCL_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "SCL_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_SCL_60m.jp2", + "type": "image/jp2", + "title": "Scene classification map (SCL) - 60m", + "roles": ["data", "sampling:downsampled", "gsd:60m"], + "data_type": "uint8", + "nodata": 0, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "classification:classes": [ + { "value": 0, "name": "no_data", "nodata": true }, + { "value": 1, "name": "saturated_or_defective" }, + { "value": 2, "name": "dark_area_pixels" }, + { "value": 3, "name": "cloud_shadows" }, + { "value": 4, "name": "vegetation" }, + { "value": 5, "name": "not_vegetated" }, + { "value": 6, "name": "water" }, + { "value": 7, "name": "unclassified" }, + { "value": 8, "name": "cloud_medium_probability" }, + { "value": 9, "name": "cloud_high_probability" }, + { "value": 10, "name": "thin_cirrus" }, + { "value": 11, "name": "snow" } + ], + "file:checksum": "162016c95a718c4a1a53e5e189cc044801e97060e3d60fa7d5dd4358ae54d6d00b48", + "file:size": 18262, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_SCL_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_SCL_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "WVP_10m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_WVP_10m.jp2", + "type": "image/jp2", + "title": "Water vapour (WVP) - 10m", + "roles": ["data", "gsd:10m"], + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 10, + "proj:code": "EPSG:32648", + "proj:shape": [10980, 10980], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [10, 0, 399960, 0, -10, 9100020], + "file:checksum": "1620da73a112b4a128420e95d8fecce34e4f863eb1ae2fc11f06f38f529f0a958daa", + "file:size": 12543, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R10m/T48XVR_20260319T084559_WVP_10m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R10m)/Nodes(T48XVR_20260319T084559_WVP_10m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "WVP_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_WVP_20m.jp2", + "type": "image/jp2", + "title": "Water vapour (WVP) - 20m", + "roles": ["data", "gsd:20m"], + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "file:checksum": "16206efe7dcb3a5f317f21216e6e75ff676bfd1fb3731d97357c4f513748ebbe2328", + "file:size": 26777, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R20m/T48XVR_20260319T084559_WVP_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R20m)/Nodes(T48XVR_20260319T084559_WVP_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "WVP_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_WVP_60m.jp2", + "type": "image/jp2", + "title": "Water vapour (WVP) - 60m", + "roles": ["data", "gsd:60m"], + "bands": [{ "name": "WVP", "description": "Water vapour" }], + "data_type": "uint16", + "nodata": 0, + "raster:scale": 0.001, + "raster:offset": 0.0, + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "file:checksum": "1620a9b7ff03ce08bd320db1a1773b6e9e2b4ed9cabd3acef1aa18dedc268db816e8", + "file:size": 14879, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/IMG_DATA/R60m/T48XVR_20260319T084559_WVP_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(IMG_DATA)/Nodes(R60m)/Nodes(T48XVR_20260319T084559_WVP_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "SNW_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_SNWPRB_20m.jp2", + "type": "image/jp2", + "title": "Snow probability (SNW) - 20m", + "roles": ["data", "gsd:20m"], + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "data_type": "uint8", + "statistics": { "minimum": 0, "maximum": 100 }, + "unit": "%", + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "file:checksum": "1620e21a64bc28edca4520dbae75a016ffa98878bb66f0d206b6730cc3888a51eefb", + "file:size": 24753, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_SNWPRB_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(QI_DATA)/Nodes(MSK_SNWPRB_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "SNW_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_SNWPRB_60m.jp2", + "type": "image/jp2", + "title": "Snow probability (SNW) - 60m", + "roles": ["data", "gsd:60m"], + "bands": [{ "name": "SNW", "description": "Snow probability" }], + "data_type": "uint8", + "statistics": { "minimum": 0, "maximum": 100 }, + "unit": "%", + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "file:checksum": "1620929aa31218ae8ca9aa6d376642d40a5b6f236753eba040b060d124049b511d50", + "file:size": 13970, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_SNWPRB_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(QI_DATA)/Nodes(MSK_SNWPRB_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "CLD_20m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_CLDPRB_20m.jp2", + "type": "image/jp2", + "title": "Cloud probability (CLD) - 20m", + "roles": ["data", "gsd:20m"], + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "data_type": "uint8", + "statistics": { "minimum": 0, "maximum": 100 }, + "unit": "%", + "gsd": 20, + "proj:code": "EPSG:32648", + "proj:shape": [5490, 5490], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [20, 0, 399960, 0, -20, 9100020], + "file:checksum": "1620388b5d349073b004704da934138a472cb1206d83b029a741ca227e88104b4de2", + "file:size": 215790, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_CLDPRB_20m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(QI_DATA)/Nodes(MSK_CLDPRB_20m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "CLD_60m": { + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/03/19/S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_CLDPRB_60m.jp2", + "type": "image/jp2", + "title": "Cloud probability (CLD) - 60m", + "roles": ["data", "gsd:60m"], + "bands": [{ "name": "CLD", "description": "Cloud probability" }], + "data_type": "uint8", + "statistics": { "minimum": 0, "maximum": 100 }, + "unit": "%", + "gsd": 60, + "proj:code": "EPSG:32648", + "proj:shape": [1830, 1830], + "proj:bbox": [399960, 8990220, 509760, 9100020], + "proj:transform": [60, 0, 399960, 0, -60, 9100020], + "file:checksum": "16204389a8930c96a28a9a34aed4ff6b228dbe40d3a4602afd694742d5547c5ee1c6", + "file:size": 41272, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE/GRANULE/L2A_T48XVR_A047180_20260319T084555/QI_DATA/MSK_CLDPRB_60m.jp2", + "alternate:name": "S3", + "auth:refs": ["s3"], + "storage:refs": ["s3-nsis"], + "alternate": { + "https": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/Nodes(S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE)/Nodes(GRANULE)/Nodes(L2A_T48XVR_A047180_20260319T084555)/Nodes(QI_DATA)/Nodes(MSK_CLDPRB_60m.jp2)/$value", + "alternate:name": "HTTPS", + "auth:refs": ["oidc-nsis"], + "storage:refs": [] + } + } + }, + "Product": { + "href": "https://download.nsiscloud.polsa.gov.pl/odata/v1/Products(2ff73481-f8b1-46d0-b7bf-905a38827c3f)/$value", + "type": "application/zip", + "title": "Zipped product", + "roles": ["data", "metadata", "archive"], + "file:checksum": "d50110ded3296733bd83e445a1d242e78b50e9", + "file:size": 22964604, + "file:local_path": "S2B_MSIL2A_20260319T084559_N0512_R107_T48XVR_20260319T110755.SAFE.zip", + "auth:refs": ["oidc"] + } + } + } + ], + "links": [ + { + "rel": "next", + "type": "application/json", + "method": "GET", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/items?limit=1&token=WzE3NzM5MDk5NTkwMjQwMDAwMDAsIlMyQl9NU0lMMkFfMjAyNjAzMTlUMDg0NTU5X04wNTEyX1IxMDdfVDQ4WFZSXzIwMjYwMzE5VDExMDc1NSIsInNlbnRpbmVsLTItbDJhIl0%3D" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a/items?limit=1" + }, + { + "rel": "collection", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.nsiscloud.polsa.gov.pl/v1/collections/sentinel-2-l2a" + } + ], + "numberReturned": 1, + "numberMatched": 75121804 +} diff --git a/geoengine/test_data/stac_responses/items/element84-marburg-minimal.json b/geoengine/test_data/stac_responses/items/element84-marburg-minimal.json new file mode 100644 index 0000000000..47a330b991 --- /dev/null +++ b/geoengine/test_data/stac_responses/items/element84-marburg-minimal.json @@ -0,0 +1,75 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/projection/v1.1.0/schema.json", + "https://stac-extensions.github.io/raster/v1.1.0/schema.json", + "https://stac-extensions.github.io/eo/v1.1.0/schema.json" + ], + "id": "S2B_32UMB_20260128_0_L2A", + "bbox": [8.766, 50.802, 8.767, 50.803], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [8.766, 50.802], + [8.767, 50.802], + [8.767, 50.803], + [8.766, 50.803], + [8.766, 50.802] + ] + ] + }, + "properties": { + "datetime": "2026-01-28T10:36:43.609000Z", + "updated": "2026-01-29T21:55:06.896Z", + "proj:epsg": 32632 + }, + "assets": { + "blue": { + "title": "Blue - 10m", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/32/U/MB/2026/1/S2B_32UMB_20260128_0_L2A/B02.tif", + "eo:bands": [ + { + "name": "B02", + "common_name": "blue" + } + ], + "raster:bands": [ + { + "data_type": "uint16" + } + ], + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + }, + "blue-jp2": { + "title": "Blue - 10m", + "type": "image/jp2", + "href": "s3://sentinel-s2-l2a/tiles/32/U/MB/2026/1/28/0/R10m/B02.jp2", + "eo:bands": [ + { + "name": "B02", + "common_name": "blue" + } + ], + "raster:bands": [ + { + "data_type": "uint16" + } + ], + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 5700000] + } + }, + "links": [] + } + ], + "links": [], + "numberReturned": 1, + "numberMatched": 1 +} diff --git a/geoengine/test_data/stac_responses/items/element84.json b/geoengine/test_data/stac_responses/items/element84.json new file mode 100644 index 0000000000..be2032df00 --- /dev/null +++ b/geoengine/test_data/stac_responses/items/element84.json @@ -0,0 +1,984 @@ +{ + "type": "FeatureCollection", + "stac_version": "1.0.0", + "stac_extensions": [], + "context": { "limit": 1, "matched": 44626419, "returned": 1 }, + "numberMatched": 44626419, + "numberReturned": 1, + "features": [ + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json", + "https://stac-extensions.github.io/grid/v1.1.0/schema.json", + "https://stac-extensions.github.io/sentinel-2/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.1.0/schema.json", + "https://stac-extensions.github.io/projection/v1.1.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/processing/v1.1.0/schema.json", + "https://stac-extensions.github.io/eo/v1.1.0/schema.json" + ], + "id": "S2B_30VVJ_20260327_0_L2A", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-3.3251764940446265, 57.741871881805025], + [-3.0259938501815213, 57.692499684285906], + [-2.8364486237901474, 57.65943244106549], + [-2.836075251505463, 57.74218312679223], + [-3.3251764940446265, 57.741871881805025] + ] + ] + }, + "bbox": [-3.325176, 57.659432, -2.836075, 57.742183], + "properties": { + "created": "2026-03-27T14:50:58.239Z", + "platform": "sentinel-2b", + "constellation": "sentinel-2", + "instruments": ["msi"], + "eo:cloud_cover": 48.887998, + "proj:epsg": 32630, + "proj:centroid": { "lat": 57.71472, "lon": -2.99839 }, + "mgrs:utm_zone": 30, + "mgrs:latitude_band": "V", + "mgrs:grid_square": "VJ", + "grid:code": "MGRS-30VVJ", + "view:azimuth": 100.354737505716, + "view:incidence_angle": 2.44446989540211, + "view:sun_azimuth": 166.30204781652, + "view:sun_elevation": 34.7328430809259, + "s2:tile_id": "S2B_OPER_MSI_L2A_TL_2BPS_20260327T140638_A047296_T30VVJ_N05.12", + "s2:degraded_msi_data_percentage": 0, + "s2:nodata_pixel_percentage": 98.859793, + "s2:saturated_defective_pixel_percentage": 0, + "s2:cloud_shadow_percentage": 0.416692, + "s2:vegetation_percentage": 5.594499, + "s2:not_vegetated_percentage": 3.030621, + "s2:water_percentage": 41.373861, + "s2:unclassified_percentage": 0.694584, + "s2:medium_proba_clouds_percentage": 11.794832, + "s2:high_proba_clouds_percentage": 22.849977, + "s2:thin_cirrus_percentage": 14.243189, + "s2:snow_ice_percentage": 0.001746, + "s2:product_type": "S2MSI2A", + "s2:processing_baseline": "05.12", + "s2:product_uri": "S2B_MSIL2A_20260327T113319_N0512_R080_T30VVJ_20260327T140638.SAFE", + "s2:generation_time": "2026-03-27T14:06:38.000000Z", + "s2:datatake_id": "GS2B_20260327T113319_047296_N05.12", + "s2:datatake_type": "INS-NOBS", + "s2:datastrip_id": "S2B_OPER_MSI_L2A_DS_2BPS_20260327T140638_S20260327T113315_N05.12", + "s2:reflectance_conversion_factor": 1.00703812798266, + "datetime": "2026-03-27T11:35:13.618000Z", + "s2:sequence": "0", + "earthsearch:s3_path": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A", + "earthsearch:payload_id": "roda-sentinel2/workflow-sentinel2-to-stac/5b4e3cc49243f1142b927c708cbcde45", + "earthsearch:boa_offset_applied": true, + "processing:software": { "sentinel2-to-stac": "2025.03.06" }, + "updated": "2026-03-27T14:50:58.239Z" + }, + "links": [ + { + "rel": "self", + "type": "application/geo+json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2B_30VVJ_20260327_0_L2A" + }, + { + "rel": "canonical", + "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/S2B_30VVJ_20260327_0_L2A.json", + "type": "application/json" + }, + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice" + }, + { + "rel": "derived_from", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l1c/items/S2B_30VVJ_20260327_0_L1C", + "type": "application/geo+json" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + }, + { + "rel": "collection", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1" + }, + { + "rel": "thumbnail", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2B_30VVJ_20260327_0_L2A/thumbnail" + } + ], + "assets": { + "aot": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/AOT.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Aerosol optical thickness (AOT)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data"] + }, + "blue": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B02.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Blue - 10m", + "eo:bands": [ + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "cloud": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/qi/CLD_20m.jp2", + "type": "image/jp2", + "title": "Cloud Probabilities", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data", "cloud"] + }, + "coastal": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B01.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Coastal - 60m", + "eo:bands": [ + { + "name": "B01", + "common_name": "coastal", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 60, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "granule_metadata": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/granule_metadata.xml", + "type": "application/xml", + "roles": ["metadata"] + }, + "green": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B03.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Green - 10m", + "eo:bands": [ + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B08.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 1 - 10m", + "eo:bands": [ + { + "name": "B08", + "common_name": "nir", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir08": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B8A.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 2 - 20m", + "eo:bands": [ + { + "name": "B8A", + "common_name": "nir08", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir09": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B09.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "NIR 3 - 60m", + "eo:bands": [ + { + "name": "B09", + "common_name": "nir09", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 60, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "product_metadata": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/product_metadata.xml", + "type": "application/xml", + "roles": ["metadata"] + }, + "red": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B04.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red - 10m", + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge1": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B05.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red Edge 1 - 20m", + "eo:bands": [ + { + "name": "B05", + "common_name": "rededge", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge2": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B06.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red Edge 2 - 20m", + "eo:bands": [ + { + "name": "B06", + "common_name": "rededge", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge3": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B07.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red Edge 3 - 20m", + "eo:bands": [ + { + "name": "B07", + "common_name": "rededge", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "scl": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/SCL.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Scene classification map (SCL)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data"] + }, + "snow": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/qi/SNW_20m.jp2", + "type": "image/jp2", + "title": "Snow Probabilities", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data", "snow-ice"] + }, + "swir16": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B11.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "SWIR 1.6μm - 20m", + "eo:bands": [ + { + "name": "B11", + "common_name": "swir16", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "swir22": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/B12.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "SWIR 2.2μm - 20m", + "eo:bands": [ + { + "name": "B12", + "common_name": "swir22", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "tileinfo_metadata": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/tileinfo_metadata.json", + "type": "application/json", + "roles": ["metadata"] + }, + "visual": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/TCI.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "True color image", + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 }, + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 }, + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "roles": ["visual"] + }, + "wvp": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/WVP.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Water Vapour (WVP)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "unit": "cm", + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data"] + }, + "thumbnail": { + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/VJ/2026/3/S2B_30VVJ_20260327_0_L2A/preview.jpg", + "type": "image/jpeg", + "title": "Thumbnail of preview image", + "roles": ["thumbnail"] + }, + "aot-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/AOT.jp2", + "type": "image/jp2", + "title": "Aerosol optical thickness (AOT)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data"] + }, + "blue-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R10m/B02.jp2", + "type": "image/jp2", + "title": "Blue - 10m", + "eo:bands": [ + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "coastal-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R60m/B01.jp2", + "type": "image/jp2", + "title": "Coastal - 60m", + "eo:bands": [ + { + "name": "B01", + "common_name": "coastal", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 60, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "green-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R10m/B03.jp2", + "type": "image/jp2", + "title": "Green - 10m", + "eo:bands": [ + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R10m/B08.jp2", + "type": "image/jp2", + "title": "NIR 1 - 10m", + "eo:bands": [ + { + "name": "B08", + "common_name": "nir", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir08-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B8A.jp2", + "type": "image/jp2", + "title": "NIR 2 - 20m", + "eo:bands": [ + { + "name": "B8A", + "common_name": "nir08", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "nir09-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R60m/B09.jp2", + "type": "image/jp2", + "title": "NIR 3 - 60m", + "eo:bands": [ + { + "name": "B09", + "common_name": "nir09", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 60, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "red-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R10m/B04.jp2", + "type": "image/jp2", + "title": "Red - 10m", + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 10, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge1-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B05.jp2", + "type": "image/jp2", + "title": "Red Edge 1 - 20m", + "eo:bands": [ + { + "name": "B05", + "common_name": "rededge", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge2-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B06.jp2", + "type": "image/jp2", + "title": "Red Edge 2 - 20m", + "eo:bands": [ + { + "name": "B06", + "common_name": "rededge", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "rededge3-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B07.jp2", + "type": "image/jp2", + "title": "Red Edge 3 - 20m", + "eo:bands": [ + { + "name": "B07", + "common_name": "rededge", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "scl-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/SCL.jp2", + "type": "image/jp2", + "title": "Scene classification map (SCL)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 20 } + ], + "roles": ["data"] + }, + "swir16-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B11.jp2", + "type": "image/jp2", + "title": "SWIR 1.6μm - 20m", + "eo:bands": [ + { + "name": "B11", + "common_name": "swir16", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "swir22-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/B12.jp2", + "type": "image/jp2", + "title": "SWIR 2.2μm - 20m", + "eo:bands": [ + { + "name": "B12", + "common_name": "swir22", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "scale": 0.0001, + "offset": -0.1 + } + ], + "roles": ["data", "reflectance"] + }, + "visual-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R10m/TCI.jp2", + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "raster:bands": [ + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 }, + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 }, + { "nodata": 0, "data_type": "uint8", "spatial_resolution": 10 } + ], + "gsd": 10, + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 6400020], + "roles": ["visual"] + }, + "wvp-jp2": { + "href": "s3://sentinel-s2-l2a/tiles/30/V/VJ/2026/3/27/0/R20m/WVP.jp2", + "type": "image/jp2", + "title": "Water Vapour (WVP)", + "gsd": 20, + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 6400020], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 20, + "unit": "cm", + "scale": 0.001, + "offset": 0 + } + ], + "roles": ["data"] + } + }, + "collection": "sentinel-2-l2a" + } + ], + "links": [ + { + "rel": "next", + "title": "Next page of Items", + "method": "GET", + "type": "application/geo+json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items?collections=sentinel-2-l2a&limit=1&next=2026-03-27T11%3A35%3A13.618000Z%2CS2B_30VVJ_20260327_0_L2A%2Csentinel-2-l2a" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1" + }, + { + "rel": "self", + "type": "application/geo+json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items" + }, + { + "rel": "collection", + "type": "application/json", + "href": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + } + ] +} diff --git a/geoengine/test_data/stac_responses/items/polsa-marburg-minimal.json b/geoengine/test_data/stac_responses/items/polsa-marburg-minimal.json new file mode 100644 index 0000000000..984111f634 --- /dev/null +++ b/geoengine/test_data/stac_responses/items/polsa-marburg-minimal.json @@ -0,0 +1,56 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/raster/v2.0.0/schema.json" + ], + "id": "S2B_MSIL2A_20260128T103209_N0511_R108_T32UMB_20260128T141930", + "bbox": [8.766, 50.802, 8.767, 50.803], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [8.766, 50.802], + [8.767, 50.802], + [8.767, 50.803], + [8.766, 50.803], + [8.766, 50.802] + ] + ] + }, + "properties": { + "datetime": "2026-01-28T10:32:09Z", + "updated": "2026-01-29T14:19:30Z", + "proj:code": "EPSG:32632" + }, + "assets": { + "B02_10m": { + "data_type": "uint16", + "proj:shape": [10980, 10980], + "proj:code": "EPSG:32632", + "proj:transform": [10, 0, 399960, 0, -10, 5700000], + "bands": [ + { + "name": "B02", + "description": "Blue (band 2)", + "eo:common_name": "blue" + } + ], + "title": "Blue (band 2) - 10m", + "href": "s3://eodata/Sentinel-2/MSI/L2A/2026/01/28/example/B02_10m.jp2", + "type": "image/jp2", + "gsd": 10 + } + }, + "links": [] + } + ], + "links": [], + "numberReturned": 1, + "numberMatched": 1 +} diff --git a/openapi.json b/openapi.json index de96f605c7..bb5b80d104 100644 --- a/openapi.json +++ b/openapi.json @@ -10895,6 +10895,24 @@ } } }, + "SpatialResolution": { + "type": "object", + "description": "The spatial resolution in SRS units", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + } + } + }, "StacApiRetries": { "type": "object", "required": [ @@ -10918,6 +10936,146 @@ } } }, + "StacDataProviderDefinition": { + "type": "object", + "required": [ + "type", + "name", + "id", + "description", + "apiUrl", + "collectionName", + "timeDimension", + "datasets" + ], + "properties": { + "apiUrl": { + "type": "string" + }, + "collectionName": { + "type": "string" + }, + "datasets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StacProviderDataset" + } + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/DataProviderId" + }, + "name": { + "type": "string" + }, + "priority": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "s3Config": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StacProviderS3Config" + } + ] + }, + "timeDimension": { + "$ref": "#/components/schemas/TimeDimension" + }, + "type": { + "type": "string", + "enum": [ + "StacProviderDefinition" + ] + } + } + }, + "StacProviderDataset": { + "type": "object", + "required": [ + "name", + "description", + "dataType", + "resolution", + "projection", + "spatialGrid", + "bands" + ], + "properties": { + "bands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StacProviderDatasetBand" + } + }, + "dataType": { + "$ref": "#/components/schemas/RasterDataType" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projection": { + "type": "string" + }, + "resolution": { + "$ref": "#/components/schemas/SpatialResolution" + }, + "spatialGrid": { + "$ref": "#/components/schemas/SpatialGridDescriptor" + } + } + }, + "StacProviderDatasetBand": { + "type": "object", + "required": [ + "assetTitle" + ], + "properties": { + "assetTitle": { + "type": "string" + }, + "bandName": { + "type": [ + "string", + "null" + ] + } + } + }, + "StacProviderS3Config": { + "type": "object", + "required": [ + "endpoint" + ], + "properties": { + "accessKey": { + "type": [ + "string", + "null" + ] + }, + "endpoint": { + "type": "string" + }, + "secretKey": { + "type": [ + "string", + "null" + ] + } + } + }, "StacQueryBuffer": { "type": "object", "description": "A struct that represents buffers to apply to stac requests", @@ -11572,6 +11730,9 @@ { "$ref": "#/components/schemas/SentinelS2L2ACogsProviderDefinition" }, + { + "$ref": "#/components/schemas/StacDataProviderDefinition" + }, { "$ref": "#/components/schemas/WildliveDataConnectorDefinition" } @@ -11590,6 +11751,7 @@ "NetCdfCf": "#/components/schemas/NetCdfCfDataProviderDefinition", "Pangaea": "#/components/schemas/PangaeaDataProviderDefinition", "SentinelS2L2ACogs": "#/components/schemas/SentinelS2L2ACogsProviderDefinition", + "StacProviderDefinition": "#/components/schemas/StacDataProviderDefinition", "WildLIVE!": "#/components/schemas/WildliveDataConnectorDefinition" } } diff --git a/ui/projects/enhanced-data-viewer/src/app/main/main.component.html b/ui/projects/enhanced-data-viewer/src/app/main/main.component.html index 11ddeacf8d..ecdda6d4cf 100644 --- a/ui/projects/enhanced-data-viewer/src/app/main/main.component.html +++ b/ui/projects/enhanced-data-viewer/src/app/main/main.component.html @@ -114,22 +114,20 @@

Time Selection

Vizualization Presets

- - RGB - RGB - - - NDVI - NDVI - - - False Color - False Color - - - SWI - SWI - + @for (preset of visualizationPresets; track preset.preset) { + + + {{ preset.label }} + + }
@@ -164,6 +162,10 @@

Vizualization Presets

} + @if (isLoading()) { + + } +
['preset']; +const DEFAULT_VISUALIZATION_PRESET: EnabledVisualizationPreset = 'rgb'; @Component({ selector: 'geoengine-main', @@ -24,6 +63,7 @@ import {MatDatepickerInputEvent, MatDatepickerModule} from '@angular/material/da MatButtonToggleModule, MatDatepickerModule, MatIconModule, + MatProgressBarModule, MatRadioModule, MatSidenavModule, MatToolbarModule, @@ -38,13 +78,30 @@ export class MainComponent implements OnInit { readonly config = inject(AppConfig); readonly projectService = inject(ProjectService); readonly userService = inject(UserService); - private readonly layerService = inject(LayersService); private readonly mapService = inject(MapService); + private readonly notificationService = inject(NotificationService); readonly topToolbar = viewChild.required>('topToolbar', {read: ElementRef}); readonly mapComponent = viewChild.required(MapContainerComponent); - readonly layersReverse = signal>([]); + readonly layersReverse = toSignal(this.projectService.getLayerStream().pipe(map((layers) => layers.slice().reverse())), { + initialValue: [], + }); + + readonly isLoading = toSignal( + this.projectService + .getLayerStream() + .pipe( + switchMap((layers) => + layers.length === 0 + ? of(false) + : combineLatest(layers.map((l) => this.projectService.getLayerStatusStream(l))).pipe( + map((statuses) => statuses.some((s) => s === LoadingState.LOADING)), + ), + ), + ), + {initialValue: false}, + ); readonly totalHeight = signal(window.innerHeight); readonly topToolbarHeight = signal(64); @@ -66,12 +123,20 @@ export class MainComponent implements OnInit { return time.start.toDate(); }); + readonly visualizationPresets = VISUALIZATION_PRESETS; + readonly activePreset = signal(DEFAULT_VISUALIZATION_PRESET); + + private readonly presetWorkflowIds = new Map(); + private activeLayer: RasterLayer | undefined; + ngOnInit(): void { this.mapService.registerMapComponent(this.mapComponent()); this.onToolbarResize(); const topToolbarObserver = new ResizeObserver(() => this.onToolbarResize()); topToolbarObserver.observe(this.topToolbar().nativeElement); + + void this.activatePreset(DEFAULT_VISUALIZATION_PRESET); } onResize(): void { @@ -86,6 +151,55 @@ export class MainComponent implements OnInit { return layer.id; } + isPresetEnabled(preset: VisualizationPreset): preset is EnabledVisualizationPreset { + return preset in PRESET_DEFINITIONS; + } + + async activatePreset(preset: VisualizationPreset): Promise { + if (!this.isPresetEnabled(preset)) { + return; + } + + if (this.activePreset() === preset && this.activeLayer) { + return; + } + + try { + const workflowId = await this.getOrRegisterWorkflowId(preset); + const layer = new RasterLayer({ + name: preset.toUpperCase(), + workflowId, + isVisible: true, + isLegendVisible: false, + symbology: PRESET_DEFINITIONS[preset].symbology, + }); + + await this.replaceActiveLayer(layer); + this.activePreset.set(preset); + } catch (error) { + console.error('Failed to activate visualization preset', preset, error); + this.notificationService.error('Could not load selected visualization preset'); + } + } + + private async getOrRegisterWorkflowId(preset: EnabledVisualizationPreset): Promise { + const existingWorkflowId = this.presetWorkflowIds.get(preset); + if (existingWorkflowId) { + return existingWorkflowId; + } + + const workflowId = await firstValueFrom(this.projectService.registerWorkflow(PRESET_DEFINITIONS[preset].workflow)); + this.presetWorkflowIds.set(preset, workflowId); + return workflowId; + } + + private async replaceActiveLayer(nextLayer: RasterLayer): Promise { + await firstValueFrom(this.projectService.clearLayers()); + + await firstValueFrom(this.projectService.addLayer(nextLayer)); + this.activeLayer = nextLayer; + } + async timeForward(): Promise { const time = this.currentTime(); const timeStepDuration = this.timeStepDuration(); @@ -114,3 +228,155 @@ export class MainComponent implements OnInit { await this.projectService.setTime(time); } } + +const STAC_PROVIDER_ID = 'b274275c-373d-4a3f-8b45-9b48e9614329'; + +const RGB_WORKFLOW: Workflow = { + type: 'Raster', + operator: { + type: 'MultiBandGdalSource', + params: { + data: `_:${STAC_PROVIDER_ID}:\`dataset/epsg32632_u8_10\``, + }, + }, +}; + +const NDVI_WORKFLOW: Workflow = { + type: 'Raster', + operator: { + type: 'Expression', + params: { + expression: 'if (A == 3 || (A >= 7 && A <= 11)) { NODATA } else { (B - C) / (B + C) }', + mapNoData: false, + outputBand: { + name: 'NDVI', + measurement: { + type: 'continuous', + measurement: 'NDVI', + unit: 'NDVI', + }, + }, + outputType: 'F32', + }, + sources: { + raster: { + type: 'RasterStacker', + params: { + renameBands: { + type: 'default', + }, + }, + sources: { + rasters: [ + { + type: 'RasterTypeConversion', + params: { + outputDataType: 'U16', + }, + sources: { + raster: { + type: 'Interpolation', + params: { + interpolation: 'nearestNeighbor', + outputResolution: { + type: 'fraction', + x: 2.0, + y: 2.0, + }, + }, + sources: { + raster: { + type: 'BandFilter', + params: { + bands: [1], + }, + sources: { + raster: { + type: 'MultiBandGdalSource', + params: { + data: `_:${STAC_PROVIDER_ID}:\`dataset/epsg32632_u8_20\``, + }, + }, + }, + }, + }, + }, + }, + }, + { + type: 'BandFilter', + params: { + bands: [3, 4], + }, + sources: { + raster: { + type: 'MultiBandGdalSource', + params: { + data: `_:${STAC_PROVIDER_ID}:\`dataset/epsg32632_u16_10\``, + }, + }, + }, + }, + ], + }, + }, + }, + }, +}; + +const RGB_SYMBOLOGY = RasterSymbology.fromRasterSymbologyDict({ + type: 'raster', + opacity: 1.0, + rasterColorizer: { + type: 'multiBand', + redBand: 2, + greenBand: 1, + blueBand: 0, + redMin: 0, + redMax: 255, + redScale: 1, + greenMin: 0, + greenMax: 255, + greenScale: 1, + blueMin: 0, + blueMax: 255, + blueScale: 1, + noDataColor: [0, 0, 0, 0], + }, +}); + +const NDVI_SYMBOLOGY = RasterSymbology.fromRasterSymbologyDict({ + type: 'raster', + opacity: 1.0, + rasterColorizer: { + type: 'singleBand', + band: 0, + bandColorizer: { + type: 'linearGradient', + breakpoints: [ + { + value: -0.1, + color: [0, 0, 0, 255], + }, + { + value: 0.8, + color: [0, 255, 0, 255], + }, + ], + noDataColor: [0, 0, 0, 0], + overColor: [246, 250, 254, 255], + underColor: [247, 251, 255, 255], + }, + }, +}); + +const PRESET_DEFINITIONS: Record = { + rgb: { + workflow: RGB_WORKFLOW, + symbology: RGB_SYMBOLOGY, + }, + ndvi: { + workflow: NDVI_WORKFLOW, + symbology: NDVI_SYMBOLOGY, + }, +};