From 72499c34c19323e0df65b0aac75fa8c30e68d5df Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:17:48 -0400 Subject: [PATCH 01/10] Initial implementation of render extension --- pystac/extensions/ext.py | 11 ++ pystac/extensions/render.py | 273 ++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 pystac/extensions/render.py diff --git a/pystac/extensions/ext.py b/pystac/extensions/ext.py index 2488b1c42..a1f1c9c0a 100644 --- a/pystac/extensions/ext.py +++ b/pystac/extensions/ext.py @@ -12,6 +12,7 @@ from pystac.extensions.pointcloud import PointcloudExtension from pystac.extensions.projection import ProjectionExtension from pystac.extensions.raster import RasterExtension +from pystac.extensions.render import Render, RenderExtension from pystac.extensions.sar import SarExtension from pystac.extensions.sat import SatExtension from pystac.extensions.scientific import ScientificExtension @@ -36,6 +37,7 @@ "pc", "proj", "raster", + "render", "sar", "sat", "sci", @@ -58,6 +60,7 @@ PointcloudExtension.name: PointcloudExtension, ProjectionExtension.name: ProjectionExtension, RasterExtension.name: RasterExtension, + RenderExtension.name: RenderExtension, SarExtension.name: SarExtension, SatExtension.name: SatExtension, ScientificExtension.name: ScientificExtension, @@ -110,6 +113,10 @@ def cube(self) -> DatacubeExtension[Collection]: def item_assets(self) -> dict[str, AssetDefinition]: return ItemAssetsExtension.ext(self.stac_object).item_assets + @property + def render(self) -> dict[str, Render]: + return RenderExtension.ext(self.stac_object).renders + @property def sci(self) -> ScientificExtension[Collection]: return ScientificExtension.ext(self.stac_object) @@ -164,6 +171,10 @@ def pc(self) -> PointcloudExtension[Item]: def proj(self) -> ProjectionExtension[Item]: return ProjectionExtension.ext(self.stac_object) + @property + def render(self) -> RenderExtension[Item]: + return RenderExtension.ext(self.stac_object) + @property def sar(self) -> SarExtension[Item]: return SarExtension.ext(self.stac_object) diff --git a/pystac/extensions/render.py b/pystac/extensions/render.py new file mode 100644 index 000000000..94dd60c08 --- /dev/null +++ b/pystac/extensions/render.py @@ -0,0 +1,273 @@ +"""Implements the :stac-ext:`Render Extension `.""" + +from __future__ import annotations + +from typing import Any, Generic, Literal, TypeVar + +import pystac +from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension +from pystac.extensions.hooks import ExtensionHooks +from pystac.utils import get_required, map_opt + +T = TypeVar("T", pystac.Collection, pystac.Item) + +SCHEMA_URI_PATTERN: str = ( + "https://stac-extensions.github.io/render/v{version}/schema.json" +) +DEFAULT_VERSION: str = "1.0.0" + +SUPPORTED_VERSIONS = [DEFAULT_VERSION] + +RENDERS_PROP = "renders" + + +class Render: + properties: dict[str, Any] + + def __init__(self, properties: dict[str, Any]) -> None: + self.properties = properties + + @property + def assets(self) -> list[str]: + return get_required(self.properties.get("assets"), self, "assets") + + @assets.setter + def assets(self, v: list[str]) -> None: + self.properties["assets"] = v + + @property + def title(self) -> str | None: + return self.properties.get("title") + + @title.setter + def title(self, v: str | None) -> None: + if v is not None: + self.properties["title"] = v + else: + self.properties.pop("title", None) + + @property + def rescale(self) -> list[list[float]] | None: + return self.properties.get("rescale") + + @rescale.setter + def rescale(self, v: list[list[float]] | None) -> None: + if v is not None: + self.properties["rescale"] = v + else: + self.properties.pop("rescale", None) + + @property + def nodata(self) -> float | str | None: + return self.properties.get("nodata") + + @nodata.setter + def nodata(self, v: float | str | None) -> None: + if v is not None: + self.properties["nodata"] = v + else: + self.properties.pop("nodata", None) + + @property + def colormap_name(self) -> str | None: + return self.properties.get("colormap_name") + + @colormap_name.setter + def colormap_name(self, v: str | None) -> None: + if v is not None: + self.properties["colormap_name"] = v + else: + self.properties.pop("colormap_name", None) + + @property + def colormap(self) -> dict[str, Any] | None: + return self.properties.get("colormap") + + @colormap.setter + def colormap(self, v: dict[str, Any] | None) -> None: + if v is not None: + self.properties["colormap"] = v + else: + self.properties.pop("colormap", None) + + @property + def color_formula(self) -> str | None: + return self.properties.get("color_formula") + + @color_formula.setter + def color_formula(self, v: str | None) -> None: + if v is not None: + self.properties["color_formula"] = v + else: + self.properties.pop("color_formula", None) + + @property + def resampling(self) -> str | None: + return self.properties.get("resampling") + + @resampling.setter + def resampling(self, v: str | None) -> None: + if v is not None: + self.properties["resampling"] = v + else: + self.properties.pop("resampling", None) + + @property + def expression(self) -> str | None: + return self.properties.get("expression") + + @expression.setter + def expression(self, v: str | None) -> None: + if v is not None: + self.properties["expression"] = v + else: + self.properties.pop("expression", None) + + @property + def minmax_zoom(self) -> list[int] | None: + return self.properties.get("minmax_zoom") + + @minmax_zoom.setter + def minmax_zoom(self, v: list[int] | None) -> None: + if v is not None: + self.properties["minmax_zoom"] = v + else: + self.properties.pop("minmax_zoom", None) + + def apply( + self, + assets: list[str], + title: str | None = None, + rescale: list[list[float]] | None = None, + nodata: float | str | None = None, + colormap_name: str | None = None, + colormap: dict[str, Any] | None = None, + color_formula: str | None = None, + resampling: str | None = None, + expression: str | None = None, + minmax_zoom: list[int] | None = None, + ) -> None: + self.assets = assets + self.title = title + self.rescale = rescale + self.nodata = nodata + self.colormap_name = colormap_name + self.colormap = colormap + self.color_formula = color_formula + self.resampling = resampling + self.expression = expression + self.minmax_zoom = minmax_zoom + + @classmethod + def create( + cls, + assets: list[str], + title: str | None = None, + rescale: list[list[float]] | None = None, + nodata: float | str | None = None, + colormap_name: str | None = None, + colormap: dict[str, Any] | None = None, + color_formula: str | None = None, + resampling: str | None = None, + expression: str | None = None, + minmax_zoom: list[int] | None = None, + ) -> Render: + c = cls({}) + c.apply( + assets=assets, + title=title, + rescale=rescale, + nodata=nodata, + colormap_name=colormap_name, + colormap=colormap, + color_formula=color_formula, + resampling=resampling, + expression=expression, + minmax_zoom=minmax_zoom, + ) + return c + + def to_dict(self) -> dict[str, Any]: + return self.properties + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Render): + raise NotImplementedError + return self.properties == other.properties + + def __repr__(self) -> str: + props = " ".join( + [ + f"{key}={value}" + for key, value in self.properties.items() + if value is not None + ] + ) + return f" None: + self.renders = renders + + @property + def renders(self) -> dict[str, Render]: + return get_required( + self._get_property(RENDERS_PROP, dict[str, Render]), self, RENDERS_PROP + ) + + @renders.setter + def renders(self, v: dict[str, Render]) -> None: + self._set_property( + RENDERS_PROP, + map_opt(lambda renders: {k: r.to_dict() for k, r in renders.items()}, v), + pop_if_none=False, + ) + + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + + @classmethod + def ext(cls, obj: T, add_if_missing: bool = False) -> RenderExtension[T]: + if isinstance(obj, pystac.Collection): + cls.ensure_has_extension(obj, add_if_missing) + if isinstance(obj, pystac.Item): + cls.ensure_has_extension(obj, add_if_missing) + return ItemRenderExtension(obj) + else: + raise pystac.ExtensionTypeError( + f"RenderExtension does not apply to type '{type(obj).__name__}" + ) + + +class CollectionRenderExtension(RenderExtension[pystac.Collection]): + def __init__(self, collection: pystac.Collection): + self.collection = collection + self.properties = collection.extra_fields + + def __repr__(self) -> str: + return f"" + + +class ItemRenderExtension(RenderExtension[pystac.Item]): + def __init__(self, item: pystac.Item): + self.item = item + self.properties = item.properties + + def __repr__(self) -> str: + return f"" + + +class RenderExtensionHooks(ExtensionHooks): + schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + stac_object_types = {pystac.STACObjectType.COLLECTION, pystac.STACObjectType.ITEM} From b3718d4962ad854952b0ffe7e157ee1a62913c60 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Sun, 27 Oct 2024 23:17:49 -0400 Subject: [PATCH 02/10] Return Render objs from renders getter --- pystac/extensions/render.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pystac/extensions/render.py b/pystac/extensions/render.py index 94dd60c08..8727ee5c6 100644 --- a/pystac/extensions/render.py +++ b/pystac/extensions/render.py @@ -221,9 +221,12 @@ def apply( @property def renders(self) -> dict[str, Render]: - return get_required( - self._get_property(RENDERS_PROP, dict[str, Render]), self, RENDERS_PROP + renders: dict[str, dict[str, Any]] = get_required( + self._get_property(RENDERS_PROP, dict[str, dict[str, Any]]), + self, + RENDERS_PROP, ) + return {k: Render(v) for k, v in renders.items()} @renders.setter def renders(self, v: dict[str, Render]) -> None: @@ -246,7 +249,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> RenderExtension[T]: return ItemRenderExtension(obj) else: raise pystac.ExtensionTypeError( - f"RenderExtension does not apply to type '{type(obj).__name__}" + f"RenderExtension does not apply to type '{type(obj).__name__}'" ) From d1b333da47e288a428d4ee465dc5e9b0216bc3f1 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:07:09 -0400 Subject: [PATCH 03/10] Add closing greater than symbol to Render repr --- pystac/extensions/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystac/extensions/render.py b/pystac/extensions/render.py index 8727ee5c6..a47cb5e56 100644 --- a/pystac/extensions/render.py +++ b/pystac/extensions/render.py @@ -203,7 +203,7 @@ def __repr__(self) -> str: if value is not None ] ) - return f"" class RenderExtension( From 74a7dd0a49642f086eae3fe54a4a068d83dbfd1b Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:09:32 -0400 Subject: [PATCH 04/10] Fix instance type check logic --- pystac/extensions/render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pystac/extensions/render.py b/pystac/extensions/render.py index a47cb5e56..49fea3d6c 100644 --- a/pystac/extensions/render.py +++ b/pystac/extensions/render.py @@ -244,7 +244,8 @@ def get_schema_uri(cls) -> str: def ext(cls, obj: T, add_if_missing: bool = False) -> RenderExtension[T]: if isinstance(obj, pystac.Collection): cls.ensure_has_extension(obj, add_if_missing) - if isinstance(obj, pystac.Item): + return CollectionRenderExtension(obj) + elif isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return ItemRenderExtension(obj) else: From 97687488b2b1e98463c3dc915082e9efbb04ee3b Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:12:02 -0400 Subject: [PATCH 05/10] Add render extension tests --- tests/data-files/render/collection.json | 66 +++++ .../render/render-landsat-example.json | 248 ++++++++++++++++++ tests/extensions/test_render.py | 235 +++++++++++++++++ 3 files changed, 549 insertions(+) create mode 100644 tests/data-files/render/collection.json create mode 100644 tests/data-files/render/render-landsat-example.json create mode 100644 tests/extensions/test_render.py diff --git a/tests/data-files/render/collection.json b/tests/data-files/render/collection.json new file mode 100644 index 000000000..f82fcd6cf --- /dev/null +++ b/tests/data-files/render/collection.json @@ -0,0 +1,66 @@ +{ + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v1.0.0/schema.json", + "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json" + ], + "type": "Collection", + "id": "senitnel2-l2a", + "title": "Sentinel-2 L2A", + "description": "Sentinel-2 L2A data", + "license": "Apache-2.0", + "extent": { + "spatial": { + "bbox": [[172.9, 1.3, 173, 1.4]] + }, + "temporal": { + "interval": [["2015-06-23T00:00:00Z", null]] + } + }, + "assets": {}, + "item_assets": { + "ndvi": { + "roles": ["virtual", "data", "index"], + "type": "image/vnd.stac.geotiff; cloud-optimized=true", + "vrt:hrefs": [ + { + "key": "red", + "href": "#/assets/red" + }, + { + "key": "nir", + "href": "#/assets/nir" + } + ], + "title": "Normalized Difference Vegetation Index", + "vrt:algorithm": "band_arithmetic", + "vrt:algorithm_opts": { + "expression": "(B05-B04)/(B05+B04)", + "rescale": [[-1, 1]] + } + } + }, + "renders": { + "ndvi": { + "title": "Normalized Difference Vegetation Index", + "assets": ["ndvi"], + "resampling": "average", + "colormap_name": "ylgn" + } + }, + "summaries": { + "datetime": { + "minimum": "2015-06-23T00:00:00Z", + "maximum": "2019-07-10T13:44:56Z" + } + }, + "links": [ + { + "href": "./collection.json", + "rel": "root", + "title": "Sentinel-2 L2A", + "type": "application/json" + } + ] +} diff --git a/tests/data-files/render/render-landsat-example.json b/tests/data-files/render/render-landsat-example.json new file mode 100644 index 000000000..a42d91389 --- /dev/null +++ b/tests/data-files/render/render-landsat-example.json @@ -0,0 +1,248 @@ +{ + "type": "Feature", + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/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/render/v1.0.0/schema.json", + "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/web-map-links/v1.2.0/schema.json" + ], + "id": "LC08_L1TP_044033_20210305_20210312_01_T1", + "properties": { + "gsd": 30, + "platform": "LANDSAT_8", + "instruments": ["OLI", "TIRS"], + "eo:cloud_cover": 7.41, + "proj:epsg": 32610, + "view:sun_azimuth": 149.10910644, + "view:sun_elevation": 40.48243563, + "view:off_nadir": 0.001, + "landsat:scene_id": "LC80440332021064LGN00", + "landsat:processing_level": "L1TP", + "landsat:collection_number": "01", + "landsat:collection_category": "T1", + "landsat:cloud_cover_land": 7.4, + "landsat:wrs_path": "44", + "landsat:wrs_row": "33", + "datetime": "2021-03-05T18:45:37.619485Z", + "created": "2021-03-16T01:40:56.703Z", + "updated": "2021-03-16T01:40:56.703Z", + "renders": { + "thumbnail": { + "title": "Thumbnail", + "assets": ["B4", "B3", "B2"], + "rescale": [[0, 150]], + "colormap_name": "rainbow", + "resampling": "bilinear", + "bidx": [1], + "width": 1024, + "height": 1024, + "bands": ["B4", "B3", "B2"] + }, + "ndvi": { + "title": "Normalized Difference Vegetation Index", + "assets": ["ndvi"], + "resampling": "average", + "colormap_name": "ylgn" + } + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-122.49680286164214, 39.958062660227306], + [-120.31547276090922, 39.578858170656], + [-120.82135075676177, 37.82701417652536], + [-122.9993441554352, 38.2150173967007], + [-122.49680286164214, 39.958062660227306] + ] + ] + }, + "links": [], + "assets": { + "index": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/index.html", + "type": "application/html", + "title": "HTML Page" + }, + "ANG": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_ANG.txt", + "type": "text/plain", + "title": "ANG Metadata", + "roles": ["metadata"] + }, + "MTL": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_MTL.txt", + "type": "text/plain", + "title": "MTL Metadata", + "roles": ["metadata"] + }, + "BQA": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_BQA.TIF", + "type": "image/tiff; application=geotiff", + "title": "Quality Band", + "roles": ["quality"] + }, + "B1": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B1.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.48, + "full_width_half_max": 0.02 + } + ] + }, + "B2": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B2.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B2", + "common_name": "blue", + "center_wavelength": 0.44, + "full_width_half_max": 0.06 + } + ] + }, + "B3": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B3.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B3", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.06 + } + ] + }, + "B4": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B4.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B4", + "common_name": "red", + "center_wavelength": 0.65, + "full_width_half_max": 0.04 + } + ] + }, + "B5": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B5.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B5", + "common_name": "nir", + "center_wavelength": 0.86, + "full_width_half_max": 0.03 + } + ] + }, + "B6": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B6.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B6", + "common_name": "swir16", + "center_wavelength": 1.6, + "full_width_half_max": 0.08 + } + ] + }, + "B7": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B7.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B7", + "common_name": "swir22", + "center_wavelength": 2.2, + "full_width_half_max": 0.2 + } + ] + }, + "B8": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B8.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B8", + "common_name": "pan", + "center_wavelength": 0.59, + "full_width_half_max": 0.18 + } + ], + "gsd": 15 + }, + "B9": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B9.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B9", + "common_name": "cirrus", + "center_wavelength": 1.37, + "full_width_half_max": 0.02 + } + ] + }, + "B10": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B10.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B10", + "common_name": "lwir11", + "center_wavelength": 10.9, + "full_width_half_max": 0.8 + } + ], + "gsd": 100 + }, + "B11": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B11.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B11", + "common_name": "lwir12", + "center_wavelength": 12, + "full_width_half_max": 1 + } + ], + "gsd": 100 + }, + "ndvi": { + "roles": ["virtual", "data", "index"], + "type": "image/vnd.stac.geotiff; cloud-optimized=true", + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1#/assets/NDVI", + "vrt:hrefs": [ + { + "key": "B4", + "href": "#/assets/B4" + }, + { + "key": "B5", + "href": "#/assets/B5" + } + ], + "title": "Normalized Difference Vegetation Index", + "vrt:algorithm": "band_arithmetic", + "vrt:algorithm_opts": { + "expression": "(B05-B04)/(B05+B04)", + "rescale": [[-1, 1]] + } + } + }, + "bbox": [-123.00234, 37.82405, -120.31321, 39.95894], + "collection": "landsat-8-l1-c1" +} diff --git a/tests/extensions/test_render.py b/tests/extensions/test_render.py new file mode 100644 index 000000000..e93ec7ea5 --- /dev/null +++ b/tests/extensions/test_render.py @@ -0,0 +1,235 @@ +"""Tests for pystac.tests.extensions.render""" + +import json + +import pytest + +import pystac +import pystac.errors +from pystac.extensions.render import Render, RenderExtension +from tests.conftest import get_data_file + + +@pytest.fixture +def ext_collection_uri() -> str: + return get_data_file("render/collection.json") + + +@pytest.fixture +def ext_collection(ext_collection_uri: str) -> pystac.Collection: + return pystac.Collection.from_file(ext_collection_uri) + + +@pytest.fixture +def ext_item_uri() -> str: + return get_data_file("render/render-landsat-example.json") + + +@pytest.fixture +def ext_item(ext_item_uri: str) -> pystac.Item: + return pystac.Item.from_file(ext_item_uri) + + +@pytest.fixture +def render() -> Render: + return Render.create( + assets=["B4", "B3", "B2"], + title="RGB", + rescale=[[0, 1000], [0, 1000], [0, 1000]], + nodata=-9999, + colormap_name="viridis", + colormap={ + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + }, + color_formula="gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5", + resampling="bilinear", + expression="(B08-B04)/(B08+B04)", + minmax_zoom=[2, 18], + ) + + +@pytest.fixture +def thumbnail_render(ext_item: pystac.Item) -> Render: + return RenderExtension.ext(ext_item).renders["thumbnail"] + + +def test_collection_stac_extensions(ext_collection: pystac.Collection) -> None: + assert RenderExtension.has_extension(ext_collection) + + +def test_item_stac_extensions(ext_item: pystac.Item) -> None: + assert RenderExtension.has_extension(ext_item) + + +def test_ext_raises_if_item_does_not_conform(item: pystac.Item) -> None: + with pytest.raises(pystac.errors.ExtensionNotImplemented): + RenderExtension.ext(item) + + +def test_ext_raises_if_collection_does_not_conform( + collection: pystac.Collection, +) -> None: + with pytest.raises(pystac.errors.ExtensionNotImplemented): + RenderExtension.ext(collection) + + +def test_ext_raises_on_catalog(catalog: pystac.Catalog) -> None: + with pytest.raises( + pystac.errors.ExtensionTypeError, + match="RenderExtension does not apply to type 'Catalog'", + ): + RenderExtension.ext(catalog) # type: ignore + + +def test_item_to_from_dict(ext_item_uri: str, ext_item: pystac.Item) -> None: + with open(ext_item_uri) as f: + d = json.load(f) + actual = ext_item.to_dict(include_self_link=False) + assert actual == d + + +def test_collection_to_from_dict( + ext_collection_uri: str, ext_collection: pystac.Item +) -> None: + with open(ext_collection_uri) as f: + d = json.load(f) + actual = ext_collection.to_dict(include_self_link=False) + assert actual == d + + +def test_add_to_item(item: pystac.Item) -> None: + assert not RenderExtension.has_extension(item) + RenderExtension.add_to(item) + + assert RenderExtension.has_extension(item) + + +def test_add_to_collection(collection: pystac.Collection) -> None: + assert not RenderExtension.has_extension(collection) + RenderExtension.add_to(collection) + + assert RenderExtension.has_extension(collection) + + +# TODO: re-enable, record cassette after schema is corrected +# @pytest.mark.vcr +# def test_item_validate(ext_item: pystac.Item) -> None: +# assert ext_item.validate() + + +# @pytest.mark.vcr +# def test_collection_validate(ext_collection: pystac.Collection) -> None: +# assert ext_collection.validate() + + +def test_get_render_values(thumbnail_render: Render) -> None: + assert thumbnail_render.title == "Thumbnail" + assert thumbnail_render.assets == ["B4", "B3", "B2"] + assert thumbnail_render.rescale == [[0, 150]] + assert thumbnail_render.colormap_name == "rainbow" + assert thumbnail_render.resampling == "bilinear" + # assert thumbnail_render.bidx == [1] + # assert thumbnail_render.width == 1024 + # assert thumbnail_render.height == 1024 + # assert thumbnail_render.bands == ["B4", "B3", "B2"] + + +def test_apply_renders_to_item(item: pystac.Item, render: Render) -> None: + RenderExtension.add_to(item) + + RenderExtension.ext(item).apply({"render": render}) + assert item.ext.render.renders["render"].assets == ["B4", "B3", "B2"] + assert item.ext.render.renders["render"].title == "RGB" + assert item.ext.render.renders["render"].rescale == [ + [0, 1000], + [0, 1000], + [0, 1000], + ] + assert item.ext.render.renders["render"].nodata == -9999 + assert item.ext.render.renders["render"].colormap_name == "viridis" + assert item.ext.render.renders["render"].colormap == { + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + } + assert ( + item.ext.render.renders["render"].color_formula + == "gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5" + ) + assert item.ext.render.renders["render"].resampling == "bilinear" + assert item.ext.render.renders["render"].expression == "(B08-B04)/(B08+B04)" + assert item.ext.render.renders["render"].minmax_zoom == [2, 18] + + assert item.ext.render.renders["render"] == render + + +def test_get_unset_properties(item: pystac.Item) -> None: + RenderExtension.add_to(item) + RenderExtension.ext(item).apply( + { + "render": Render.create( + assets=["B4", "B3", "B2"], + ) + } + ) + + assert item.ext.render.renders["render"].title is None + assert item.ext.render.renders["render"].rescale is None + assert item.ext.render.renders["render"].nodata is None + assert item.ext.render.renders["render"].colormap_name is None + assert item.ext.render.renders["render"].colormap is None + assert item.ext.render.renders["render"].color_formula is None + assert item.ext.render.renders["render"].resampling is None + assert item.ext.render.renders["render"].expression is None + assert item.ext.render.renders["render"].minmax_zoom is None + + +def test_equality_check_with_unexpected_type_raises_notimplemented_error() -> None: + render = Render.create( + assets=["B4", "B3", "B2"], + ) + with pytest.raises(NotImplementedError): + _ = render == 1 + + +def test_item_repr(ext_item: pystac.Item) -> None: + ext = RenderExtension.ext(ext_item) + assert ext.__repr__() == f"" + + +def test_collection_repr(ext_collection: pystac.Collection) -> None: + ext = RenderExtension.ext(ext_collection) + assert ( + ext.__repr__() + == f"" + ) + + +def test_render_repr() -> None: + render = Render.create( + assets=["B4", "B3", "B2"], + title="RGB", + rescale=[[0, 1000], [0, 1000], [0, 1000]], + nodata=-9999, + colormap_name="viridis", + colormap={ + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + }, + color_formula="gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5", + resampling="bilinear", + expression="(B08-B04)/(B08+B04)", + minmax_zoom=[2, 18], + ) + + assert render.__repr__() == ( + "" + ) From a29411d3bb4eb006581e26f9bb1ecc0c7c1b461c Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:22:27 -0400 Subject: [PATCH 06/10] Remove item-assets and virtual assets extensions --- tests/data-files/render/collection.json | 27 +------------------------ tests/extensions/test_render.py | 2 +- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/tests/data-files/render/collection.json b/tests/data-files/render/collection.json index f82fcd6cf..ae8541218 100644 --- a/tests/data-files/render/collection.json +++ b/tests/data-files/render/collection.json @@ -1,9 +1,7 @@ { "stac_version": "1.1.0", "stac_extensions": [ - "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", - "https://stac-extensions.github.io/render/v1.0.0/schema.json", - "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json" + "https://stac-extensions.github.io/render/v1.0.0/schema.json" ], "type": "Collection", "id": "senitnel2-l2a", @@ -18,29 +16,6 @@ "interval": [["2015-06-23T00:00:00Z", null]] } }, - "assets": {}, - "item_assets": { - "ndvi": { - "roles": ["virtual", "data", "index"], - "type": "image/vnd.stac.geotiff; cloud-optimized=true", - "vrt:hrefs": [ - { - "key": "red", - "href": "#/assets/red" - }, - { - "key": "nir", - "href": "#/assets/nir" - } - ], - "title": "Normalized Difference Vegetation Index", - "vrt:algorithm": "band_arithmetic", - "vrt:algorithm_opts": { - "expression": "(B05-B04)/(B05+B04)", - "rescale": [[-1, 1]] - } - } - }, "renders": { "ndvi": { "title": "Normalized Difference Vegetation Index", diff --git a/tests/extensions/test_render.py b/tests/extensions/test_render.py index e93ec7ea5..f88c425ac 100644 --- a/tests/extensions/test_render.py +++ b/tests/extensions/test_render.py @@ -91,7 +91,7 @@ def test_item_to_from_dict(ext_item_uri: str, ext_item: pystac.Item) -> None: def test_collection_to_from_dict( - ext_collection_uri: str, ext_collection: pystac.Item + ext_collection_uri: str, ext_collection: pystac.Collection ) -> None: with open(ext_collection_uri) as f: d = json.load(f) From f3a49ea27c1bc06cb9ef8563a761a10029e6f0b3 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:32:48 -0400 Subject: [PATCH 07/10] Add docstrings to render extension --- pystac/extensions/render.py | 183 ++++++++++++++++++++++++++++++++---- 1 file changed, 167 insertions(+), 16 deletions(-) diff --git a/pystac/extensions/render.py b/pystac/extensions/render.py index 49fea3d6c..95c613e12 100644 --- a/pystac/extensions/render.py +++ b/pystac/extensions/render.py @@ -22,6 +22,8 @@ class Render: + """Parameters for creating a rendered view of assets.""" + properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: @@ -29,6 +31,10 @@ def __init__(self, properties: dict[str, Any]) -> None: @property def assets(self) -> list[str]: + """ + List of asset keys referencing the assets that are + used to make the rendering. + """ return get_required(self.properties.get("assets"), self, "assets") @assets.setter @@ -37,6 +43,7 @@ def assets(self, v: list[str]) -> None: @property def title(self) -> str | None: + """Title of the rendering""" return self.properties.get("title") @title.setter @@ -48,6 +55,10 @@ def title(self, v: str | None) -> None: @property def rescale(self) -> list[list[float]] | None: + """A list of min/max value pairs to rescale each asset by, e.g. + `[[0, 5000], [0, 7000], [0, 9000]]`. If not provided, the + assets will not be rescaled. + """ return self.properties.get("rescale") @rescale.setter @@ -59,6 +70,7 @@ def rescale(self, v: list[list[float]] | None) -> None: @property def nodata(self) -> float | str | None: + """Nodata value.""" return self.properties.get("nodata") @nodata.setter @@ -70,6 +82,9 @@ def nodata(self, v: float | str | None) -> None: @property def colormap_name(self) -> str | None: + """Name of color map to apply to the render. + See: https://matplotlib.org/stable/gallery/color/colormap_reference.html + """ return self.properties.get("colormap_name") @colormap_name.setter @@ -81,6 +96,9 @@ def colormap_name(self, v: str | None) -> None: @property def colormap(self) -> dict[str, Any] | None: + """A dictionary containing a custom colormap definition. + See: https://developmentseed.org/titiler/advanced/rendering/#custom-colormaps + """ return self.properties.get("colormap") @colormap.setter @@ -92,6 +110,12 @@ def colormap(self, v: dict[str, Any] | None) -> None: @property def color_formula(self) -> str | None: + """A string containing a color formula to apply + color corrections to images. Useful for reducing + artefacts like atmospheric haze, dark shadows, or + muted colors. + See: https://developmentseed.org/titiler/advanced/rendering/#color-formula + """ return self.properties.get("color_formula") @color_formula.setter @@ -103,6 +127,10 @@ def color_formula(self, v: str | None) -> None: @property def resampling(self) -> str | None: + """Resampling algorithm to apply to the referenced assets. See GDAL + resampling algorithm for some examples. + See: https://gdal.org/en/latest/programs/gdalwarp.html#cmdoption-gdalwarp-r + """ return self.properties.get("resampling") @resampling.setter @@ -114,6 +142,7 @@ def resampling(self, v: str | None) -> None: @property def expression(self) -> str | None: + """Band arithmetic formula to apply to the referenced assets.""" return self.properties.get("expression") @expression.setter @@ -125,6 +154,7 @@ def expression(self, v: str | None) -> None: @property def minmax_zoom(self) -> list[int] | None: + """Zoom level range applicable for the visualization, e.g. `[2, 18]`.""" return self.properties.get("minmax_zoom") @minmax_zoom.setter @@ -147,6 +177,41 @@ def apply( expression: str | None = None, minmax_zoom: list[int] | None = None, ) -> None: + """Set the properties for a new Render. + + Args: + assets: + List of asset keys referencing the assets that are + used to make the rendering. + title: + Title of the rendering. + rescale: + A list of min/max value pairs to rescale each asset by, e.g. + `[[0, 5000], [0, 7000], [0, 9000]]`. If not provided, the + assets will not be rescaled. + nodata: + Nodata value. + colormap_name: + Name of color map to apply to the render. + https://matplotlib.org/stable/gallery/color/colormap_reference.html + colormap: + A dictionary containing a custom colormap definition. + https://developmentseed.org/titiler/advanced/rendering/#custom-colormaps + color_formula: + A string containing a color formula to apply + color corrections to images. Useful for reducing + artefacts like atmospheric haze, dark shadows, or + muted colors. + https://developmentseed.org/titiler/advanced/rendering/#color-formula + resampling: + Resampling algorithm to apply to the referenced assets. See GDAL + resampling algorithm for some examples. + https://gdal.org/en/latest/programs/gdalwarp.html#cmdoption-gdalwarp-r + expression: + Band arithmetic formula to apply to the referenced assets. + minmax_zoom: + Zoom level range applicable for the visualization, e.g. `[2, 18]`. + """ self.assets = assets self.title = title self.rescale = rescale @@ -172,6 +237,41 @@ def create( expression: str | None = None, minmax_zoom: list[int] | None = None, ) -> Render: + """Create a new Render. + + Args: + assets: + List of asset keys referencing the assets that are + used to make the rendering. + title: + Title of the rendering. + rescale: + A list of min/max value pairs to rescale each asset by, e.g. + `[[0, 5000], [0, 7000], [0, 9000]]`. If not provided, the + assets will not be rescaled. + nodata: + Nodata value. + colormap_name: + Name of color map to apply to the render. + https://matplotlib.org/stable/gallery/color/colormap_reference.html + colormap: + A dictionary containing a custom colormap definition. + https://developmentseed.org/titiler/advanced/rendering/#custom-colormaps + color_formula: + A string containing a color formula to apply + color corrections to images. Useful for reducing + artefacts like atmospheric haze, dark shadows, or + muted colors. + https://developmentseed.org/titiler/advanced/rendering/#color-formula + resampling: + Resampling algorithm to apply to the referenced assets. See GDAL + resampling algorithm for some examples. + https://gdal.org/en/latest/programs/gdalwarp.html#cmdoption-gdalwarp-r + expression: + Band arithmetic formula to apply to the referenced assets. + minmax_zoom: + Zoom level range applicable for the visualization, e.g. `[2, 18]`. + """ c = cls({}) c.apply( assets=assets, @@ -211,16 +311,68 @@ class RenderExtension( PropertiesExtension, ExtensionManagementMixin[pystac.Item | pystac.Collection], ): + """An abstract class that can be used to extend the properties of a + :class:`~pystac.Collection` or :class:`~pystac.Item` with + properties from the :stac-ext:`Render Extension `. This class is + generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, + :class:`~pystac.Collection`). + + To create a concrete instance of :class:`RenderExtension`, use the + :meth:`RenderExtension.ext` method. For example: + + .. code-block:: python + + >>> item: pystac.Item = ... + >>> xr_ext = RenderExtension.ext(item) + + """ + name: Literal["render"] = "render" + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + + @classmethod + def ext(cls, obj: T, add_if_missing: bool = False) -> RenderExtension[T]: + """Extend the given STAC Object with properties from the + :stac-ext:`Render Extension `. + + This extension can be applied to instances of :class:`~pystac.Collection` + or :class:`~pystac.Item`. + + Raises: + pystac.ExtensionTypeError : If an invalid object type is passed. + """ + if isinstance(obj, pystac.Collection): + cls.ensure_has_extension(obj, add_if_missing) + return CollectionRenderExtension(obj) + elif isinstance(obj, pystac.Item): + cls.ensure_has_extension(obj, add_if_missing) + return ItemRenderExtension(obj) + else: + raise pystac.ExtensionTypeError( + f"RenderExtension does not apply to type '{type(obj).__name__}'" + ) + def apply( self, renders: dict[str, Render], ) -> None: + """Applies the render extension fields to the extended + object. + + Args: + renders: a dictionary mapping render names to + :class: `~pystac.extension.render.Render` objects. + """ self.renders = renders @property def renders(self) -> dict[str, Render]: + """A dictionary where each key is the name of a render and each + value is a :class:`~Render` object. + """ renders: dict[str, dict[str, Any]] = get_required( self._get_property(RENDERS_PROP, dict[str, dict[str, Any]]), self, @@ -236,25 +388,16 @@ def renders(self, v: dict[str, Render]) -> None: pop_if_none=False, ) - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) - @classmethod - def ext(cls, obj: T, add_if_missing: bool = False) -> RenderExtension[T]: - if isinstance(obj, pystac.Collection): - cls.ensure_has_extension(obj, add_if_missing) - return CollectionRenderExtension(obj) - elif isinstance(obj, pystac.Item): - cls.ensure_has_extension(obj, add_if_missing) - return ItemRenderExtension(obj) - else: - raise pystac.ExtensionTypeError( - f"RenderExtension does not apply to type '{type(obj).__name__}'" - ) +class CollectionRenderExtension(RenderExtension[pystac.Collection]): + """A concrete implementation of :class:`RenderExtension` on a + :class:`~pystac.Collection` that extends the properties of the Collection to include + properties defined in the :stac-ext:`Render Extension `. + This class should generally not be instantiated directly. Instead, call + :meth:`RenderExtension.ext` on an :class:`~pystac.Collection` to extend it. + """ -class CollectionRenderExtension(RenderExtension[pystac.Collection]): def __init__(self, collection: pystac.Collection): self.collection = collection self.properties = collection.extra_fields @@ -264,6 +407,14 @@ def __repr__(self) -> str: class ItemRenderExtension(RenderExtension[pystac.Item]): + """A concrete implementation of :class:`RenderExtension` on a + :class:`~pystac.Item` that extends the properties of the Item to include + properties defined in the :stac-ext:`Render Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`RenderExtension.ext` on an :class:`~pystac.Item` to extend it. + """ + def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties From fb11ad3124b78f70da6560f4e968f8b7e9bbdc9f Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:16:13 -0400 Subject: [PATCH 08/10] Replace Landsat-8 example Item with Sentinel-2 Item --- tests/data-files/render/item.json | 405 ++++++++++++++++++ .../render/render-landsat-example.json | 248 ----------- tests/extensions/test_render.py | 4 +- 3 files changed, 407 insertions(+), 250 deletions(-) create mode 100644 tests/data-files/render/item.json delete mode 100644 tests/data-files/render/render-landsat-example.json diff --git a/tests/data-files/render/item.json b/tests/data-files/render/item.json new file mode 100644 index 000000000..d4dd109c3 --- /dev/null +++ b/tests/data-files/render/item.json @@ -0,0 +1,405 @@ +{ + "type": "Feature", + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.1.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "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/render/v1.0.0/schema.json", + "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json" + ], + "id": "S2B_33SVB_20210221_0_L2A", + "bbox": [ + 13.86148243891681, 36.95257399124932, 15.111074610520053, + 37.94752813015372 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [13.876381589019879, 36.95257399124932], + [13.86148243891681, 37.942072015005024], + [15.111074610520053, 37.94752813015372], + [15.109620666835209, 36.95783951241028], + [13.876381589019879, 36.95257399124932] + ] + ] + }, + "properties": { + "datetime": "2021-02-21T10:00:17Z", + "platform": "sentinel-2b", + "constellation": "sentinel-2", + "instruments": ["msi"], + "gsd": 10, + "view:off_nadir": 0, + "proj:epsg": 32633, + "sentinel:utm_zone": 33, + "sentinel:latitude_band": "S", + "sentinel:grid_square": "VB", + "sentinel:sequence": "0", + "sentinel:product_id": "S2B_MSIL2A_20210221T095029_N0214_R079_T33SVB_20210221T115149", + "sentinel:data_coverage": 100, + "eo:cloud_cover": 21.22, + "sentinel:valid_cloud_cover": true, + "renders": { + "thumbnail": { + "title": "Thumbnail", + "assets": ["B04", "B03", "B02"], + "rescale": [[0, 150]], + "colormap_name": "rainbow", + "resampling": "bilinear", + "bidx": [1], + "width": 1024, + "height": 1024, + "bands": ["B4", "B3", "B2"] + }, + "sir": { + "title": "Shortwave Infra-red", + "assets": ["B12", "B08", "B04"], + "rescale": [ + [0, 5000], + [0, 7000], + [0, 9000] + ], + "resampling": "nearest" + } + } + }, + "collection": "sentinel-s2-l2a-cogs", + "assets": { + "metadata": { + "title": "Original XML metadata", + "type": "application/xml", + "roles": ["metadata"], + "href": "metadata.xml" + }, + "B01": { + "title": "Band 1 (coastal)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 60, + "eo:bands": [ + { + "name": "B01", + "common_name": "coastal", + "center_wavelength": 0.4439, + "full_width_half_max": 0.027 + } + ], + "href": "B01.tif", + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 20567, + "stats_mean": 2339.4759595597, + "stats_stddev": 3026.6973619954, + "stats_valid_percent": 99.83, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B02": { + "title": "Band 2 (blue)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 10, + "eo:bands": [ + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.4966, + "full_width_half_max": 0.098 + } + ], + "href": "B02.tif", + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 19264, + "stats_mean": 2348.069117847, + "stats_stddev": 2916.5446249911, + "stats_valid_percent": 99.99, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B03": { + "title": "Band 3 (green)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 10, + "eo:bands": [ + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "href": "B03.tif", + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 18064, + "stats_mean": 2384.4680007438, + "stats_stddev": 2675.410513295, + "stats_valid_percent": 99.999, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B04": { + "title": "Band 4 (red)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 10, + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.6645, + "full_width_half_max": 0.038 + } + ], + "href": "B04.tif", + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 17200, + "stats_mean": 2273.9667970732, + "stats_stddev": 2618.272802792, + "stats_valid_percent": 99.999, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B05": { + "title": "Band 5", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B05", + "center_wavelength": 0.7039, + "full_width_half_max": 0.019 + } + ], + "href": "B05.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 16842, + "stats_mean": 2634.1490243416, + "stats_stddev": 2634.1490243416, + "stats_valid_percent": 99.999, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B06": { + "title": "Band 6", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B06", + "center_wavelength": 0.7402, + "full_width_half_max": 0.018 + } + ], + "href": "B06.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1], + "raster:bands": [ + { + "data_type": "uint16", + "nodata": 0, + "stats_min": 1, + "stats_max": 16502, + "stats_mean": 3329.8844628619, + "stats_stddev": 2303.0096294469, + "stats_valid_percent": 99.999, + "values": [ + { + "name": "BOA reflectance" + } + ] + } + ] + }, + "B07": { + "title": "Band 7", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B07", + "center_wavelength": 0.7825, + "full_width_half_max": 0.028 + } + ], + "href": "B07.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + }, + "B08": { + "title": "Band 8 (nir)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 10, + "eo:bands": [ + { + "name": "B08", + "common_name": "nir", + "center_wavelength": 0.8351, + "full_width_half_max": 0.145 + } + ], + "href": "B08.tif", + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1] + }, + "B8A": { + "title": "Band 8A", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B8A", + "center_wavelength": 0.8648, + "full_width_half_max": 0.033 + } + ], + "href": "B8A.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + }, + "B09": { + "title": "Band 9", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 60, + "eo:bands": [ + { + "name": "B09", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "href": "B09.tif", + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1] + }, + "B11": { + "title": "Band 11 (swir16)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B11", + "common_name": "swir16", + "center_wavelength": 1.6137, + "full_width_half_max": 0.143 + } + ], + "href": "B11.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + }, + "B12": { + "title": "Band 12 (swir22)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "gsd": 20, + "eo:bands": [ + { + "name": "B12", + "common_name": "swir22", + "center_wavelength": 2.22024, + "full_width_half_max": 0.242 + } + ], + "href": "B12.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + }, + "AOT": { + "title": "Aerosol Optical Thickness (AOT)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "href": "AOT.tif", + "proj:shape": [1830, 1830], + "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1] + }, + "WVP": { + "title": "Water Vapour (WVP)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "href": "WVP.tif", + "proj:shape": [10980, 10980], + "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1] + }, + "SCL": { + "title": "Scene Classification Map (SCL)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": ["data"], + "href": "SCL.tif", + "proj:shape": [5490, 5490], + "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + } + }, + "links": [ + { + "rel": "collection", + "href": "./collection.json", + "type": "application/json", + "title": "Sentinel-2 L2A Cogs Collection" + } + ] +} diff --git a/tests/data-files/render/render-landsat-example.json b/tests/data-files/render/render-landsat-example.json deleted file mode 100644 index a42d91389..000000000 --- a/tests/data-files/render/render-landsat-example.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "type": "Feature", - "stac_version": "1.1.0", - "stac_extensions": [ - "https://stac-extensions.github.io/eo/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/render/v1.0.0/schema.json", - "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json", - "https://stac-extensions.github.io/web-map-links/v1.2.0/schema.json" - ], - "id": "LC08_L1TP_044033_20210305_20210312_01_T1", - "properties": { - "gsd": 30, - "platform": "LANDSAT_8", - "instruments": ["OLI", "TIRS"], - "eo:cloud_cover": 7.41, - "proj:epsg": 32610, - "view:sun_azimuth": 149.10910644, - "view:sun_elevation": 40.48243563, - "view:off_nadir": 0.001, - "landsat:scene_id": "LC80440332021064LGN00", - "landsat:processing_level": "L1TP", - "landsat:collection_number": "01", - "landsat:collection_category": "T1", - "landsat:cloud_cover_land": 7.4, - "landsat:wrs_path": "44", - "landsat:wrs_row": "33", - "datetime": "2021-03-05T18:45:37.619485Z", - "created": "2021-03-16T01:40:56.703Z", - "updated": "2021-03-16T01:40:56.703Z", - "renders": { - "thumbnail": { - "title": "Thumbnail", - "assets": ["B4", "B3", "B2"], - "rescale": [[0, 150]], - "colormap_name": "rainbow", - "resampling": "bilinear", - "bidx": [1], - "width": 1024, - "height": 1024, - "bands": ["B4", "B3", "B2"] - }, - "ndvi": { - "title": "Normalized Difference Vegetation Index", - "assets": ["ndvi"], - "resampling": "average", - "colormap_name": "ylgn" - } - } - }, - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [-122.49680286164214, 39.958062660227306], - [-120.31547276090922, 39.578858170656], - [-120.82135075676177, 37.82701417652536], - [-122.9993441554352, 38.2150173967007], - [-122.49680286164214, 39.958062660227306] - ] - ] - }, - "links": [], - "assets": { - "index": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/index.html", - "type": "application/html", - "title": "HTML Page" - }, - "ANG": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_ANG.txt", - "type": "text/plain", - "title": "ANG Metadata", - "roles": ["metadata"] - }, - "MTL": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_MTL.txt", - "type": "text/plain", - "title": "MTL Metadata", - "roles": ["metadata"] - }, - "BQA": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_BQA.TIF", - "type": "image/tiff; application=geotiff", - "title": "Quality Band", - "roles": ["quality"] - }, - "B1": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B1.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B1", - "common_name": "coastal", - "center_wavelength": 0.48, - "full_width_half_max": 0.02 - } - ] - }, - "B2": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B2.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B2", - "common_name": "blue", - "center_wavelength": 0.44, - "full_width_half_max": 0.06 - } - ] - }, - "B3": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B3.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B3", - "common_name": "green", - "center_wavelength": 0.56, - "full_width_half_max": 0.06 - } - ] - }, - "B4": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B4.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B4", - "common_name": "red", - "center_wavelength": 0.65, - "full_width_half_max": 0.04 - } - ] - }, - "B5": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B5.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B5", - "common_name": "nir", - "center_wavelength": 0.86, - "full_width_half_max": 0.03 - } - ] - }, - "B6": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B6.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B6", - "common_name": "swir16", - "center_wavelength": 1.6, - "full_width_half_max": 0.08 - } - ] - }, - "B7": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B7.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B7", - "common_name": "swir22", - "center_wavelength": 2.2, - "full_width_half_max": 0.2 - } - ] - }, - "B8": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B8.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B8", - "common_name": "pan", - "center_wavelength": 0.59, - "full_width_half_max": 0.18 - } - ], - "gsd": 15 - }, - "B9": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B9.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B9", - "common_name": "cirrus", - "center_wavelength": 1.37, - "full_width_half_max": 0.02 - } - ] - }, - "B10": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B10.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B10", - "common_name": "lwir11", - "center_wavelength": 10.9, - "full_width_half_max": 0.8 - } - ], - "gsd": 100 - }, - "B11": { - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B11.TIF", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", - "eo:bands": [ - { - "name": "B11", - "common_name": "lwir12", - "center_wavelength": 12, - "full_width_half_max": 1 - } - ], - "gsd": 100 - }, - "ndvi": { - "roles": ["virtual", "data", "index"], - "type": "image/vnd.stac.geotiff; cloud-optimized=true", - "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1#/assets/NDVI", - "vrt:hrefs": [ - { - "key": "B4", - "href": "#/assets/B4" - }, - { - "key": "B5", - "href": "#/assets/B5" - } - ], - "title": "Normalized Difference Vegetation Index", - "vrt:algorithm": "band_arithmetic", - "vrt:algorithm_opts": { - "expression": "(B05-B04)/(B05+B04)", - "rescale": [[-1, 1]] - } - } - }, - "bbox": [-123.00234, 37.82405, -120.31321, 39.95894], - "collection": "landsat-8-l1-c1" -} diff --git a/tests/extensions/test_render.py b/tests/extensions/test_render.py index f88c425ac..c2d1e4c9c 100644 --- a/tests/extensions/test_render.py +++ b/tests/extensions/test_render.py @@ -22,7 +22,7 @@ def ext_collection(ext_collection_uri: str) -> pystac.Collection: @pytest.fixture def ext_item_uri() -> str: - return get_data_file("render/render-landsat-example.json") + return get_data_file("render/item.json") @pytest.fixture @@ -126,7 +126,7 @@ def test_add_to_collection(collection: pystac.Collection) -> None: def test_get_render_values(thumbnail_render: Render) -> None: assert thumbnail_render.title == "Thumbnail" - assert thumbnail_render.assets == ["B4", "B3", "B2"] + assert thumbnail_render.assets == ["B04", "B03", "B02"] assert thumbnail_render.rescale == [[0, 150]] assert thumbnail_render.colormap_name == "rainbow" assert thumbnail_render.resampling == "bilinear" From ef68e1c016f683aaa9833dcfd9d3023da972ad55 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:16:51 -0400 Subject: [PATCH 09/10] Remove validation tests --- tests/extensions/test_render.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/extensions/test_render.py b/tests/extensions/test_render.py index c2d1e4c9c..759be0018 100644 --- a/tests/extensions/test_render.py +++ b/tests/extensions/test_render.py @@ -113,17 +113,6 @@ def test_add_to_collection(collection: pystac.Collection) -> None: assert RenderExtension.has_extension(collection) -# TODO: re-enable, record cassette after schema is corrected -# @pytest.mark.vcr -# def test_item_validate(ext_item: pystac.Item) -> None: -# assert ext_item.validate() - - -# @pytest.mark.vcr -# def test_collection_validate(ext_collection: pystac.Collection) -> None: -# assert ext_collection.validate() - - def test_get_render_values(thumbnail_render: Render) -> None: assert thumbnail_render.title == "Thumbnail" assert thumbnail_render.assets == ["B04", "B03", "B02"] From 2d65be179b44afee7bdf16df31712357e3a3152e Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:15:24 -0400 Subject: [PATCH 10/10] Remove other extensions, simplify Item --- tests/data-files/render/item.json | 262 ++---------------------------- 1 file changed, 16 insertions(+), 246 deletions(-) diff --git a/tests/data-files/render/item.json b/tests/data-files/render/item.json index d4dd109c3..59b008e69 100644 --- a/tests/data-files/render/item.json +++ b/tests/data-files/render/item.json @@ -2,12 +2,7 @@ "type": "Feature", "stac_version": "1.1.0", "stac_extensions": [ - "https://stac-extensions.github.io/eo/v1.1.0/schema.json", - "https://stac-extensions.github.io/view/v1.0.0/schema.json", - "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/render/v1.0.0/schema.json", - "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json" + "https://stac-extensions.github.io/render/v1.0.0/schema.json" ], "id": "S2B_33SVB_20210221_0_L2A", "bbox": [ @@ -32,16 +27,6 @@ "constellation": "sentinel-2", "instruments": ["msi"], "gsd": 10, - "view:off_nadir": 0, - "proj:epsg": 32633, - "sentinel:utm_zone": 33, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "VB", - "sentinel:sequence": "0", - "sentinel:product_id": "S2B_MSIL2A_20210221T095029_N0214_R079_T33SVB_20210221T115149", - "sentinel:data_coverage": 100, - "eo:cloud_cover": 21.22, - "sentinel:valid_cloud_cover": true, "renders": { "thumbnail": { "title": "Thumbnail", @@ -66,7 +51,7 @@ } } }, - "collection": "sentinel-s2-l2a-cogs", + "collection": "sentinel-s2-l2a", "assets": { "metadata": { "title": "Original XML metadata", @@ -79,303 +64,90 @@ "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 60, - "eo:bands": [ - { - "name": "B01", - "common_name": "coastal", - "center_wavelength": 0.4439, - "full_width_half_max": 0.027 - } - ], - "href": "B01.tif", - "proj:shape": [1830, 1830], - "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 20567, - "stats_mean": 2339.4759595597, - "stats_stddev": 3026.6973619954, - "stats_valid_percent": 99.83, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B01.tif" }, "B02": { "title": "Band 2 (blue)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 10, - "eo:bands": [ - { - "name": "B02", - "common_name": "blue", - "center_wavelength": 0.4966, - "full_width_half_max": 0.098 - } - ], - "href": "B02.tif", - "proj:shape": [10980, 10980], - "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 19264, - "stats_mean": 2348.069117847, - "stats_stddev": 2916.5446249911, - "stats_valid_percent": 99.99, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B02.tif" }, "B03": { "title": "Band 3 (green)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 10, - "eo:bands": [ - { - "name": "B03", - "common_name": "green", - "center_wavelength": 0.56, - "full_width_half_max": 0.045 - } - ], - "href": "B03.tif", - "proj:shape": [10980, 10980], - "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 18064, - "stats_mean": 2384.4680007438, - "stats_stddev": 2675.410513295, - "stats_valid_percent": 99.999, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B03.tif" }, "B04": { "title": "Band 4 (red)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 10, - "eo:bands": [ - { - "name": "B04", - "common_name": "red", - "center_wavelength": 0.6645, - "full_width_half_max": 0.038 - } - ], - "href": "B04.tif", - "proj:shape": [10980, 10980], - "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 17200, - "stats_mean": 2273.9667970732, - "stats_stddev": 2618.272802792, - "stats_valid_percent": 99.999, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B04.tif" }, "B05": { "title": "Band 5", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B05", - "center_wavelength": 0.7039, - "full_width_half_max": 0.019 - } - ], - "href": "B05.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 16842, - "stats_mean": 2634.1490243416, - "stats_stddev": 2634.1490243416, - "stats_valid_percent": 99.999, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B05.tif" }, "B06": { "title": "Band 6", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B06", - "center_wavelength": 0.7402, - "full_width_half_max": 0.018 - } - ], - "href": "B06.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1], - "raster:bands": [ - { - "data_type": "uint16", - "nodata": 0, - "stats_min": 1, - "stats_max": 16502, - "stats_mean": 3329.8844628619, - "stats_stddev": 2303.0096294469, - "stats_valid_percent": 99.999, - "values": [ - { - "name": "BOA reflectance" - } - ] - } - ] + "href": "B06.tif" }, "B07": { "title": "Band 7", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B07", - "center_wavelength": 0.7825, - "full_width_half_max": 0.028 - } - ], - "href": "B07.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + "href": "B07.tif" }, "B08": { "title": "Band 8 (nir)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 10, - "eo:bands": [ - { - "name": "B08", - "common_name": "nir", - "center_wavelength": 0.8351, - "full_width_half_max": 0.145 - } - ], - "href": "B08.tif", - "proj:shape": [10980, 10980], - "proj:transform": [10, 0, 399960, 0, -10, 4200000, 0, 0, 1] + "href": "B08.tif" }, "B8A": { "title": "Band 8A", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B8A", - "center_wavelength": 0.8648, - "full_width_half_max": 0.033 - } - ], - "href": "B8A.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + "href": "B8A.tif" }, "B09": { "title": "Band 9", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 60, - "eo:bands": [ - { - "name": "B09", - "center_wavelength": 0.945, - "full_width_half_max": 0.026 - } - ], - "href": "B09.tif", - "proj:shape": [1830, 1830], - "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1] + "href": "B09.tif" }, "B11": { "title": "Band 11 (swir16)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B11", - "common_name": "swir16", - "center_wavelength": 1.6137, - "full_width_half_max": 0.143 - } - ], - "href": "B11.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + "href": "B11.tif" }, "B12": { "title": "Band 12 (swir22)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "gsd": 20, - "eo:bands": [ - { - "name": "B12", - "common_name": "swir22", - "center_wavelength": 2.22024, - "full_width_half_max": 0.242 - } - ], - "href": "B12.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + "href": "B12.tif" }, "AOT": { "title": "Aerosol Optical Thickness (AOT)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], - "href": "AOT.tif", - "proj:shape": [1830, 1830], - "proj:transform": [60, 0, 399960, 0, -60, 4200000, 0, 0, 1] + "href": "AOT.tif" }, "WVP": { "title": "Water Vapour (WVP)", @@ -389,9 +161,7 @@ "title": "Scene Classification Map (SCL)", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], - "href": "SCL.tif", - "proj:shape": [5490, 5490], - "proj:transform": [20, 0, 399960, 0, -20, 4200000, 0, 0, 1] + "href": "SCL.tif" } }, "links": [