"""Implements the :stac-ext:`Render Extension <render>`."""
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
#: Generalized version of :class:`~pystac.Collection` or :class:`~pystac.Item`
T = TypeVar("T", pystac.Collection, pystac.Item)
SCHEMA_URI_PATTERN: str = (
"https://stac-extensions.github.io/render/v{version}/schema.json"
)
DEFAULT_VERSION: str = "2.0.0"
SUPPORTED_VERSIONS = [DEFAULT_VERSION]
RENDERS_PROP = "renders"
[docs]
class Render:
"""Parameters for creating a rendered view of assets."""
properties: dict[str, Any]
def __init__(self, properties: dict[str, Any]) -> None:
self.properties = properties
@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
def assets(self, v: list[str]) -> None:
self.properties["assets"] = v
@property
def title(self) -> str | None:
"""Title of the rendering"""
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:
"""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
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:
"""Nodata value."""
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:
"""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
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:
"""A dictionary containing a custom colormap definition.
See: https://developmentseed.org/titiler/advanced/rendering/#custom-colormaps
"""
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:
"""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
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:
"""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
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:
"""Band arithmetic formula to apply to the referenced assets."""
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:
"""Zoom level range applicable for the visualization, e.g. ``[2, 18]``."""
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)
[docs]
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:
"""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
artifacts 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
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
[docs]
@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:
"""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
artifacts 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,
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
[docs]
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"<Render {props}>"
[docs]
class RenderExtension(
Generic[T],
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 <render>`. 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"
[docs]
@classmethod
def get_schema_uri(cls) -> str:
return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION)
[docs]
@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 <render>`.
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__}'"
)
[docs]
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.extensions.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,
RENDERS_PROP,
)
return {k: Render(v) for k, v in renders.items()}
@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,
)
[docs]
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 <render>`.
This class should generally not be instantiated directly. Instead, call
:meth:`RenderExtension.ext` on an :class:`~pystac.Collection` to extend it.
"""
def __init__(self, collection: pystac.Collection):
self.collection = collection
self.properties = collection.extra_fields
def __repr__(self) -> str:
return f"<CollectionRenderExtension Collection id={self.collection.id}>"
[docs]
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 <render>`.
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
def __repr__(self) -> str:
return f"<ItemRenderExtension Item id={self.item.id}>"
[docs]
class RenderExtensionHooks(ExtensionHooks):
schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION)
stac_object_types = {pystac.STACObjectType.COLLECTION, pystac.STACObjectType.ITEM}