Create a Dynamic STAC backend¶
By default cogeo-mosaic backends were meant to handle writing and reading mosaicjson either from a file or from a database.
While this is fine for most use cases, some users could want something more dynamic
. In this Notebook we will show how to create a Dynamic mosaic backend based on STAC api.
In [ ]:
Copied!
# Uncomment this line if you need to install the dependencies
# !pip install cogeo-mosaic
# Uncomment this line if you need to install the dependencies
# !pip install cogeo-mosaic
In [3]:
Copied!
from typing import Dict, Tuple, Type, Optional, List
import attr
import morecantile
from rasterio.crs import CRS
from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
from rio_tiler.io import BaseReader
from rio_tiler.io import STACReader
from cogeo_mosaic.backends.base import BaseBackend
from cogeo_mosaic.backends.stac import _fetch, default_stac_accessor
from cogeo_mosaic.mosaic import MosaicJSON
@attr.s
class DynamicStacBackend(BaseBackend):
"""Like a STAC backend but dynamic"""
# input should be the STAC-API url
input: str = attr.ib()
# Addition required attribute (STAC Query)
query: Dict = attr.ib(factory=dict)
minzoom: int = attr.ib()
maxzoom: int = attr.ib()
tms: morecantile.TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
reader: Type[BaseReader] = attr.ib(default=STACReader)
reader_options: Dict = attr.ib(factory=dict)
bounds: Tuple[float, float, float, float] = attr.ib(
default=(-180, -90, 180, 90)
)
crs: CRS = attr.ib(default=WGS84_CRS)
# STAC API related options
# max_items | next_link_key | limit
stac_api_options: Dict = attr.ib(factory=dict)
# The reader is read-only, we can't pass mosaic_def to the init method
mosaic_def: MosaicJSON = attr.ib(init=False)
_backend_name = "DynamicSTAC"
@minzoom.default
def _minzoom(self):
return self.tms.minzoom
@maxzoom.default
def _maxzoom(self):
return self.tms.maxzoom
def __attrs_post_init__(self):
"""Post Init."""
# Construct a FAKE/Empty mosaicJSON
# mosaic_def has to be defined. As we do for the DynamoDB and SQLite backend
self.mosaic_def = MosaicJSON(
mosaicjson="0.0.3",
name="it's fake but it's ok",
bounds=self.bounds,
minzoom=self.minzoom,
maxzoom=self.maxzoom,
tiles=[] # we set `tiles` to an empty list.
)
def write(self, overwrite: bool = True):
"""This method is not used but is required by the abstract class."""
raise NotImplementedError
def update(self):
"""We overwrite the default method."""
raise NotImplementedError
def _read(self) -> MosaicJSON:
"""This method is not used but is required by the abstract class."""
pass
def assets_for_tile(self, x: int, y: int, z: int) -> List[str]:
"""Retrieve assets for tile."""
bounds = self.tms.bounds(x, y, z)
geom = {
"type": "Polygon",
"coordinates": [
[
[bounds[0], bounds[3]],
[bounds[0], bounds[1]],
[bounds[2], bounds[1]],
[bounds[2], bounds[3]],
[bounds[0], bounds[3]],
]
],
}
return self.get_assets(geom)
def assets_for_point(self, lng: float, lat: float) -> List[str]:
"""Retrieve assets for point.
Note: some API only accept Polygon.
"""
EPSILON = 1e-14
geom = {
"type": "Polygon",
"coordinates": [
[
[lng - EPSILON, lat + EPSILON],
[lng - EPSILON, lat - EPSILON],
[lng + EPSILON, lat - EPSILON],
[lng + EPSILON, lat + EPSILON],
[lng - EPSILON, lat + EPSILON],
]
],
}
return self.get_assets(geom)
def get_assets(self, geom) -> List[str]:
"""Send query to the STAC-API and retrieve assets."""
query = self.query.copy()
query["intersects"] = geom
features = _fetch(
self.input,
query,
**self.stac_api_options,
)
return [default_stac_accessor(f) for f in features]
@property
def _quadkeys(self) -> List[str]:
return []
from typing import Dict, Tuple, Type, Optional, List
import attr
import morecantile
from rasterio.crs import CRS
from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
from rio_tiler.io import BaseReader
from rio_tiler.io import STACReader
from cogeo_mosaic.backends.base import BaseBackend
from cogeo_mosaic.backends.stac import _fetch, default_stac_accessor
from cogeo_mosaic.mosaic import MosaicJSON
@attr.s
class DynamicStacBackend(BaseBackend):
"""Like a STAC backend but dynamic"""
# input should be the STAC-API url
input: str = attr.ib()
# Addition required attribute (STAC Query)
query: Dict = attr.ib(factory=dict)
minzoom: int = attr.ib()
maxzoom: int = attr.ib()
tms: morecantile.TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
reader: Type[BaseReader] = attr.ib(default=STACReader)
reader_options: Dict = attr.ib(factory=dict)
bounds: Tuple[float, float, float, float] = attr.ib(
default=(-180, -90, 180, 90)
)
crs: CRS = attr.ib(default=WGS84_CRS)
# STAC API related options
# max_items | next_link_key | limit
stac_api_options: Dict = attr.ib(factory=dict)
# The reader is read-only, we can't pass mosaic_def to the init method
mosaic_def: MosaicJSON = attr.ib(init=False)
_backend_name = "DynamicSTAC"
@minzoom.default
def _minzoom(self):
return self.tms.minzoom
@maxzoom.default
def _maxzoom(self):
return self.tms.maxzoom
def __attrs_post_init__(self):
"""Post Init."""
# Construct a FAKE/Empty mosaicJSON
# mosaic_def has to be defined. As we do for the DynamoDB and SQLite backend
self.mosaic_def = MosaicJSON(
mosaicjson="0.0.3",
name="it's fake but it's ok",
bounds=self.bounds,
minzoom=self.minzoom,
maxzoom=self.maxzoom,
tiles=[] # we set `tiles` to an empty list.
)
def write(self, overwrite: bool = True):
"""This method is not used but is required by the abstract class."""
raise NotImplementedError
def update(self):
"""We overwrite the default method."""
raise NotImplementedError
def _read(self) -> MosaicJSON:
"""This method is not used but is required by the abstract class."""
pass
def assets_for_tile(self, x: int, y: int, z: int) -> List[str]:
"""Retrieve assets for tile."""
bounds = self.tms.bounds(x, y, z)
geom = {
"type": "Polygon",
"coordinates": [
[
[bounds[0], bounds[3]],
[bounds[0], bounds[1]],
[bounds[2], bounds[1]],
[bounds[2], bounds[3]],
[bounds[0], bounds[3]],
]
],
}
return self.get_assets(geom)
def assets_for_point(self, lng: float, lat: float) -> List[str]:
"""Retrieve assets for point.
Note: some API only accept Polygon.
"""
EPSILON = 1e-14
geom = {
"type": "Polygon",
"coordinates": [
[
[lng - EPSILON, lat + EPSILON],
[lng - EPSILON, lat - EPSILON],
[lng + EPSILON, lat - EPSILON],
[lng + EPSILON, lat + EPSILON],
[lng - EPSILON, lat + EPSILON],
]
],
}
return self.get_assets(geom)
def get_assets(self, geom) -> List[str]:
"""Send query to the STAC-API and retrieve assets."""
query = self.query.copy()
query["intersects"] = geom
features = _fetch(
self.input,
query,
**self.stac_api_options,
)
return [default_stac_accessor(f) for f in features]
@property
def _quadkeys(self) -> List[str]:
return []
In [4]:
Copied!
## Base Query for sat-api
# - limit of 5 items per page (we will stop at page 1)
# - less than 25% of cloud
# - more than 75% of data coverage
# - `sentinel-s2-l2a-cogs` collection
query = {
"collections": ["sentinel-s2-l2a-cogs"],
"limit": 5,
"query": {
"eo:cloud_cover": {
"lt": 25
},
"sentinel:data_coverage": {
"gt": 75
}
},
"fields": {
'include': ['id'],
'exclude': ['assets', 'geometry']
}
}
## Base Query for sat-api
# - limit of 5 items per page (we will stop at page 1)
# - less than 25% of cloud
# - more than 75% of data coverage
# - `sentinel-s2-l2a-cogs` collection
query = {
"collections": ["sentinel-s2-l2a-cogs"],
"limit": 5,
"query": {
"eo:cloud_cover": {
"lt": 25
},
"sentinel:data_coverage": {
"gt": 75
}
},
"fields": {
'include': ['id'],
'exclude': ['assets', 'geometry']
}
}
In [12]:
Copied!
# Read Tile
with DynamicStacBackend(
"https://earth-search.aws.element84.com/v0/search",
query=query,
stac_api_options={"max_items": 5},
minzoom=8, # we know this by analysing the sentinel 2 data
maxzoom=12, # we know this by analysing the sentinel 2 data
) as mosaic:
print(mosaic.info())
print(len(mosaic.assets_for_tile(535, 335, 10)))
img, _ = mosaic.tile(535, 335, 10, assets="B01", tilesize=128, threads=0)
print(img)
# Read Tile
with DynamicStacBackend(
"https://earth-search.aws.element84.com/v0/search",
query=query,
stac_api_options={"max_items": 5},
minzoom=8, # we know this by analysing the sentinel 2 data
maxzoom=12, # we know this by analysing the sentinel 2 data
) as mosaic:
print(mosaic.info())
print(len(mosaic.assets_for_tile(535, 335, 10)))
img, _ = mosaic.tile(535, 335, 10, assets="B01", tilesize=128, threads=0)
print(img)
bounds=(-180.0, -90.0, 180.0, 90.0) center=(0.0, 0.0, 8) minzoom=8 maxzoom=12 name="it's fake but it's ok" quadkeys=[] 5 ImageData(array=masked_array( data=[[[283, 350, 339, ..., 536, 439, 386], [317, 315, 320, ..., 334, 311, 406], [362, 393, 250, ..., 402, 303, 419], ..., [711, 618, 495, ..., 504, 313, 373], [746, 666, 407, ..., 532, 372, 381], [273, 569, 506, ..., 546, 606, 418]]], mask=[[[False, False, False, ..., False, False, False], [False, False, False, ..., False, False, False], [False, False, False, ..., False, False, False], ..., [False, False, False, ..., False, False, False], [False, False, False, ..., False, False, False], [False, False, False, ..., False, False, False]]], fill_value=999999, dtype=uint16), assets=['https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_32UMD_20230419_1_L2A'], bounds=BoundingBox(left=900122.4450862072, bottom=6887893.492833819, right=939258.2035682201, top=6927029.25131583), crs=CRS.from_epsg(3857), metadata={}, band_names=['B01_b1'], dataset_statistics=None, cutline_mask=None)
In [14]:
Copied!
# Read Point values
with DynamicStacBackend(
"https://earth-search.aws.element84.com/v0/search",
query=query,
stac_api_options={"max_items": 5},
minzoom=8, # we know this by analysing the sentinel 2 data
maxzoom=12, # we know this by analysing the sentinel 2 data
) as mosaic:
print(len(mosaic.assets_for_point(-1.0546875, 51.99)))
values = mosaic.point(-1.0546875, 51.99, assets="B01")
for (f, v) in values:
print(f.split("/")[-1], v.data)
# Read Point values
with DynamicStacBackend(
"https://earth-search.aws.element84.com/v0/search",
query=query,
stac_api_options={"max_items": 5},
minzoom=8, # we know this by analysing the sentinel 2 data
maxzoom=12, # we know this by analysing the sentinel 2 data
) as mosaic:
print(len(mosaic.assets_for_point(-1.0546875, 51.99)))
values = mosaic.point(-1.0546875, 51.99, assets="B01")
for (f, v) in values:
print(f.split("/")[-1], v.data)
5 S2B_30UXC_20230426_0_L2A [6112] S2A_30UXC_20230322_0_L2A [442] S2A_30UXC_20230302_1_L2A [2866] S2B_30UXC_20230215_0_L2A [409] S2A_30UXC_20230121_0_L2A [491]
In [ ]:
Copied!