Cropped Tiles #376
-
I love the new What are your thoughts on supporting cropped tile requests for a GeoJSON [multi]polygon? I can imagine a few obstacles:
To work around the above complications, I've previously created cropped COGs such that the cropping is done ahead of the tiler. This works fine, but doing it dynamically at request time would offer architectural elegance and some new opportunities. Thanks for your thoughts, |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
👋 @DanSchoppe While I'm not really sure we want to support this directly in TiTiler, I think it will be pretty basic to do it on your own. here is how you could do it: Let's first assume you have the geometry up front stored in a db or a local file (you could add an endpoint for a user to add its own geometry 🤷♂️) """routes.
app/routes.py
"""
from dataclasses import dataclass
from typing import Callable, Dict, Type
from urllib.parse import urlencode
from fastapi import Depends, Path
from starlette.requests import Request
from starlette.responses import Response
from morecantile import TileMatrixSet
from rio_tiler.io import BaseReader, COGReader
from titiler.core.factory import BaseTilerFactory, img_endpoint_params
from titiler.core.dependencies import ImageParams, MetadataParams, TMSParams
from titiler.core.models.mapbox import TileJSON
from titiler.core.resources.enums import ImageType
from geojson_pydantic.features import Feature
def get_aoi(aoi: str) -> Feature:
"""Return Feature."""
# here you either call a database or access a local file
return feat
@dataclass
class TilerFactory(BaseTilerFactory):
# Default reader is set to COGReader
reader: Type[BaseReader] = COGReader
# Endpoint Dependencies
metadata_dependency: Type[DefaultDependency] = MetadataParams
img_dependency: Type[DefaultDependency] = ImageParams
# TileMatrixSet dependency
tms_dependency: Callable[..., TileMatrixSet] = TMSParams
def register_routes(self):
"""This Method register routes to the router."""
self.tile()
self.tilejson()
def tile(self):
"""Register /tiles endpoint."""
@self.router.get(r"/tiles/{aoi}/{z}/{x}/{y}", **img_endpoint_params)
@self.router.get(r"/tiles/{aoi}/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params)
def tile(
aoi: str = Path(..., description="name of the AOI to use to crop tiles"),
z: int = Path(..., ge=0, le=30, description="Tiles's zoom level"),
x: int = Path(..., description="Tiles's column"),
y: int = Path(..., description="Tiles's row"),
tms: TileMatrixSet = Depends(self.tms_dependency),
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
render_params=Depends(self.render_dependency),
colormap=Depends(self.colormap_dependency),
kwargs: Dict = Depends(self.additional_dependency),
):
"""Create map tile from a dataset."""
feat = get_aoi(aoi)
with self.reader(src_path, tms=tms, **self.reader_options) as src_dst:
# THIS WILL ONLY WORK FOR COGReader
cutline = create_cutline(src_dst.dataset, feat.dict(exclude_none=True), geometry_crs="epsg:4326")
data = src_dst.tile(
x,
y,
z,
**layer_params.kwargs,
**dataset_params.kwargs,
vrt_options={'cutline': cutline},
**kwargs,
)
dst_colormap = getattr(src_dst, "colormap", None)
format = ImageType.jpeg if data.mask.all() else ImageType.png
image = data.post_process(
in_range=render_params.rescale_range,
color_formula=render_params.color_formula,
)
content = image.render(
add_mask=render_params.return_mask,
img_format=format.driver,
colormap=colormap or dst_colormap,
**format.profile,
**render_params.kwargs,
)
return Response(content, media_type=format.mediatype)
def tilejson(self):
"""Register /tilejson.json endpoint."""
@self.router.get(
"/{aoi}/tilejson.json",
response_model=TileJSON,
responses={200: {"description": "Return a tilejson"}},
response_model_exclude_none=True,
)
@self.router.get(
"/{aoi}/{TileMatrixSetId}/tilejson.json",
response_model=TileJSON,
responses={200: {"description": "Return a tilejson"}},
response_model_exclude_none=True,
)
def tilejson(
request: Request,
aoi: str = Path(..., description="name of the AOI to use to crop tiles"),
tms: TileMatrixSet = Depends(self.tms_dependency),
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
render_params=Depends(self.render_dependency),
colormap=Depends(self.colormap_dependency),
kwargs: Dict = Depends(self.additional_dependency),
):
"""Return TileJSON document for a dataset."""
route_params = {
"aoi": aoi,
"z": "{z}",
"x": "{x}",
"y": "{y}",
"TileMatrixSetId": tms.identifier,
}
tiles_url = self.url_for(request, "tile", **route_params)
q = dict(request.query_params)
q.pop("aoi", None)
q.pop("TileMatrixSetId", None)
qs = urlencode(list(q.items()))
tiles_url += f"?{qs}"
with self.reader(src_path, tms=tms, **self.reader_options) as src_dst:
return {
"bounds": src_dst.bounds,
"minzoom": src_dst.minzoom,
"maxzoom": src_dst.maxzoom,
"name": "cogeotif",
"tiles": [tiles_url],
}
cog = TilerFactory() OR another way would be to have the geometry encoded in the URL directly (though about this after writing ☝️) """routes.
app/routes.py
"""
from dataclasses import dataclass
from typing import Callable, Dict, Type
from urllib.parse import urlencode
from fastapi import Depends, Path
from starlette.requests import Request
from starlette.responses import Response
from morecantile import TileMatrixSet
from rio_tiler.io import BaseReader, COGReader
from titiler.core.factory import BaseTilerFactory, img_endpoint_params
from titiler.core.dependencies import ImageParams, MetadataParams, TMSParams
from titiler.core.models.mapbox import TileJSON
from titiler.core.resources.enums import ImageType
from geojson_pydantic.features import Feature
import json
from base64 import b64decode
def get_aoi(aoi: str = Path(..., description="b64 encoded GeoJSON feature")) -> Feature:
"""Return Feature."""
return json.loads(b64decode(aoi))
@dataclass
class TilerFactory(BaseTilerFactory):
# Default reader is set to COGReader
reader: Type[BaseReader] = COGReader
# Endpoint Dependencies
metadata_dependency: Type[DefaultDependency] = MetadataParams
img_dependency: Type[DefaultDependency] = ImageParams
# TileMatrixSet dependency
tms_dependency: Callable[..., TileMatrixSet] = TMSParams
def register_routes(self):
"""This Method register routes to the router."""
self.tile()
self.tilejson()
def tile(self):
"""Register /tiles endpoint."""
@self.router.get(r"/tiles/{aoi}/{z}/{x}/{y}", **img_endpoint_params)
@self.router.get(r"/tiles/{aoi}/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params)
def tile(
aoi: Feature = Depends(get_aoi),
z: int = Path(..., ge=0, le=30, description="Tiles's zoom level"),
x: int = Path(..., description="Tiles's column"),
y: int = Path(..., description="Tiles's row"),
tms: TileMatrixSet = Depends(self.tms_dependency),
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
render_params=Depends(self.render_dependency),
colormap=Depends(self.colormap_dependency),
kwargs: Dict = Depends(self.additional_dependency),
):
"""Create map tile from a dataset."""
with self.reader(src_path, tms=tms, **self.reader_options) as src_dst:
# THIS WILL ONLY WORK FOR COGReader
cutline = create_cutline(src_dst.dataset, aoi.dict(exclude_none=True), geometry_crs="epsg:4326")
data = src_dst.tile(
x,
y,
z,
**layer_params.kwargs,
**dataset_params.kwargs,
vrt_options={'cutline': cutline},
**kwargs,
)
dst_colormap = getattr(src_dst, "colormap", None)
format = ImageType.jpeg if data.mask.all() else ImageType.png
image = data.post_process(
in_range=render_params.rescale_range,
color_formula=render_params.color_formula,
)
content = image.render(
add_mask=render_params.return_mask,
img_format=format.driver,
colormap=colormap or dst_colormap,
**format.profile,
**render_params.kwargs,
)
return Response(content, media_type=format.mediatype)
def tilejson(self):
"""Register /tilejson.json endpoint."""
@self.router.get(
"/{aoi}/tilejson.json",
response_model=TileJSON,
responses={200: {"description": "Return a tilejson"}},
response_model_exclude_none=True,
)
@self.router.get(
"/{aoi}/{TileMatrixSetId}/tilejson.json",
response_model=TileJSON,
responses={200: {"description": "Return a tilejson"}},
response_model_exclude_none=True,
)
def tilejson(
request: Request,
aoi: str = Path(..., description="b64 encoded GeoJSON feature"),
tms: TileMatrixSet = Depends(self.tms_dependency),
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
render_params=Depends(self.render_dependency),
colormap=Depends(self.colormap_dependency),
kwargs: Dict = Depends(self.additional_dependency),
):
"""Return TileJSON document for a dataset."""
route_params = {
"aoi": aoi,
"z": "{z}",
"x": "{x}",
"y": "{y}",
"TileMatrixSetId": tms.identifier,
}
tiles_url = self.url_for(request, "tile", **route_params)
q = dict(request.query_params)
q.pop("aoi", None)
q.pop("TileMatrixSetId", None)
qs = urlencode(list(q.items()))
tiles_url += f"?{qs}"
with self.reader(src_path, tms=tms, **self.reader_options) as src_dst:
return {
"bounds": src_dst.bounds,
"minzoom": src_dst.minzoom,
"maxzoom": src_dst.maxzoom,
"name": "cogeotif",
"tiles": [tiles_url],
}
cog = TilerFactory() I haven't tried the code ☝️ (copy pasted most of it from https://developmentseed.org/titiler/examples/code/tiler_with_cache/ so there might have been changes in titiler since I wrote it) let me know what you think and how it goes if you try ☝️ |
Beta Was this translation helpful? Give feedback.
-
👋 Hey @vincentsarago, I took a crack at building this. The easiest solution I found was adding the cutline functionality to rio-tiler. (check out this commit from my fork: https://github.com/jwiem/rio-tiler/commit/a4821ee08075e066cd4c44f900a1d0410519cccb). In terms of Titiler, I chose to pass the The only [small] downside is that the client would have to make sure the GeoJSON was simplified at request time. Is this something you'd welcome as a PR for rio-tiler and titiler? Thank you for continued support of this awesome app! |
Beta Was this translation helpful? Give feedback.
👋 @DanSchoppe
As you said this is not really part of standard Tile Service.
While I'm not really sure we want to support this directly in TiTiler, I think it will be pretty basic to do it on your own.
here is how you could do it:
Let's first assume you have the geometry up front stored in a db or a local file (you could add an endpoint for a user to add its own geometry 🤷♂️)