Skip to content

Commit db8d93e

Browse files
[xarray] add preview endpoints (#1198)
1 parent b8cc304 commit db8d93e

5 files changed

Lines changed: 133 additions & 16 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010

1111
* add OpenTelemetry tracing to the FastAPI application
1212

13+
### titiler.xarray
14+
15+
* add `add_preview` in factory attribute (default to `False`)
16+
1317
### Misc
1418

15-
* Add otel-collector and jaeger to the docker network
19+
* Add otel-collector and jaeger to the docker network
1620

1721
## 0.22.4 (2025-07-02)
1822

docs/src/advanced/endpoints_factories.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,9 @@ class: `titiler.xarray.factory.TilerFactory`
359359
- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`.
360360
- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`.
361361
- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`.
362-
- **add_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`.
363-
- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`.
364-
362+
- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`.
363+
- **add_viewer**: Add `/map.html` endpoints to the router. Defaults to `True`.
364+
- **add_preview**: Add `/preview` endpoints to the router. Default to `False`.
365365

366366
```python
367367
from fastapi import FastAPI
@@ -373,8 +373,9 @@ app = FastAPI()
373373

374374
# Create router and register set of endpoints
375375
md = TilerFactory(
376-
add_part=True,
377-
add_viewer=True,
376+
add_part=True, # default to True
377+
add_viewer=True, # default to True
378+
add_preview=True, # default to False
378379
)
379380

380381
# add router endpoint to the main application
@@ -398,6 +399,7 @@ app.include_router(md.router)
398399
| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset
399400
| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional**
400401
| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional**
402+
| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional**
401403

402404

403405
[bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46

src/titiler/xarray/tests/test_factory.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,21 @@ def test_deprecated_extension():
3030
def test_tiler_factory():
3131
"""Test factory with options."""
3232
"""Test TilerFactory class."""
33+
md = TilerFactory()
34+
assert len(md.router.routes) == 19
35+
36+
with pytest.warns(UserWarning):
37+
md = TilerFactory(
38+
# /preview, /preview.{format}, /preview/{width}x{height}.{format}
39+
add_preview=True,
40+
)
41+
assert len(md.router.routes) == 22
42+
3343
md = TilerFactory(
34-
add_viewer=False,
35-
add_part=False,
44+
router_prefix="/md",
45+
# /dataset, /dataset/dict, /dataset/keys
3646
extensions=[DatasetMetadataExtension()],
3747
)
38-
assert len(md.router.routes) == 16
39-
40-
md = TilerFactory(router_prefix="/md", extensions=[DatasetMetadataExtension()])
4148
assert len(md.router.routes) == 22
4249

4350
app = FastAPI()
@@ -377,3 +384,56 @@ def test_zarr_group(group, app):
377384
params={"url": zarr_pyramid, "variable": "dataset", "group": str(group)},
378385
)
379386
assert resp.json()["values"] == [group * 2 + 1]
387+
388+
389+
@pytest.mark.parametrize(
390+
"filename",
391+
[dataset_2d_nc, dataset_3d_nc, dataset_3d_zarr],
392+
)
393+
def test_preview(filename):
394+
"""App fixture."""
395+
with pytest.warns(UserWarning):
396+
md = TilerFactory(add_preview=True)
397+
398+
app = FastAPI()
399+
app.include_router(md.router)
400+
with TestClient(app) as client:
401+
resp = client.get(
402+
"/preview",
403+
params={
404+
"url": filename,
405+
"variable": "dataset",
406+
"rescale": "0,500",
407+
"bidx": 1,
408+
},
409+
)
410+
assert resp.status_code == 200
411+
assert resp.headers["content-type"] == "image/jpeg"
412+
413+
resp = client.get(
414+
"/preview.png",
415+
params={
416+
"url": filename,
417+
"variable": "dataset",
418+
"rescale": "0,500",
419+
"bidx": 1,
420+
},
421+
)
422+
assert resp.status_code == 200
423+
assert resp.headers["content-type"] == "image/png"
424+
425+
resp = client.get(
426+
"/preview/1024x1024.png",
427+
params={
428+
"url": filename,
429+
"variable": "dataset",
430+
"rescale": "0,500",
431+
"bidx": 1,
432+
},
433+
)
434+
assert resp.status_code == 200
435+
assert resp.headers["content-type"] == "image/png"
436+
with MemoryFile(resp.content) as mem:
437+
with mem.open() as dst:
438+
assert dst.width == 1024
439+
assert dst.height == 1024

src/titiler/xarray/titiler/xarray/dependencies.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,37 @@ def __post_init__(self):
145145
"""Post Init."""
146146
if self.width or self.height:
147147
self.max_size = None
148+
149+
150+
# Custom PreviewParams which add `resampling`
151+
@dataclass
152+
class PreviewParams(DefaultDependency):
153+
"""Common Preview parameters."""
154+
155+
max_size: Annotated[
156+
Optional[int], Field(description="Maximum image size to read onto.")
157+
] = 1024
158+
height: Annotated[
159+
Optional[int], Field(description="Force output image height.")
160+
] = None
161+
width: Annotated[Optional[int], Field(description="Force output image width.")] = (
162+
None
163+
)
164+
resampling_method: Annotated[
165+
Optional[RIOResampling],
166+
Query(
167+
alias="resampling",
168+
description="RasterIO resampling algorithm. Defaults to `nearest`.",
169+
),
170+
] = None
171+
172+
def __post_init__(self):
173+
"""Post Init."""
174+
if self.width or self.height:
175+
self.max_size = None
176+
177+
# NOTE: By default we don't exclude None when we forward the parameter to the preview() method
178+
# because we need to be able to pass max_size=None
179+
# So we need to set the `resampling_method` to a default = 'nearest'
180+
# https://github.com/developmentseed/titiler/blob/b8cc304382d0cb3b4f16cea9dbb0cfba35517085/src/titiler/core/titiler/core/factory.py#L1300
181+
self.resampling_method = self.resampling_method or "nearest"

src/titiler/xarray/titiler/xarray/factory.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""TiTiler.xarray factory."""
22

33
import logging
4+
import warnings
45
from typing import Any, Callable, Optional, Type, Union
56

67
import rasterio
7-
from attrs import define, field
8+
from attrs import define
89
from fastapi import Body, Depends, Query
910
from geojson_pydantic.features import Feature, FeatureCollection
1011
from rio_tiler.constants import WGS84_CRS
@@ -26,7 +27,12 @@
2627
from titiler.core.models.responses import InfoGeoJSON, StatisticsGeoJSON
2728
from titiler.core.resources.responses import GeoJSONResponse, JSONResponse
2829
from titiler.core.utils import bounds_to_geometry
29-
from titiler.xarray.dependencies import DatasetParams, PartFeatureParams, XarrayParams
30+
from titiler.xarray.dependencies import (
31+
DatasetParams,
32+
PartFeatureParams,
33+
PreviewParams,
34+
XarrayParams,
35+
)
3036
from titiler.xarray.io import Reader
3137

3238
logger = logging.getLogger(__name__)
@@ -55,14 +61,25 @@ class TilerFactory(BaseTilerFactory):
5561
stats_dependency: Type[DefaultDependency] = StatisticsParams
5662
histogram_dependency: Type[DefaultDependency] = HistogramParams
5763

64+
img_preview_dependency: Type[DefaultDependency] = PreviewParams
5865
img_part_dependency: Type[DefaultDependency] = PartFeatureParams
5966

6067
add_viewer: bool = True
6168
add_part: bool = True
6269

63-
# remove some attribute from init
64-
img_preview_dependency: Type[DefaultDependency] = field(init=False)
65-
add_preview: bool = field(init=False, default=False)
70+
# /preview endpoints disabled by default
71+
add_preview: bool = False
72+
73+
def __attrs_post_init__(self):
74+
"""Raise warning if preview is enabled."""
75+
if self.add_preview:
76+
warnings.warn(
77+
"`preview` endpoints enabled Xarray based TilerFactory. MultiDim dataset might not be suitable for preview.",
78+
UserWarning,
79+
stacklevel=1,
80+
)
81+
82+
super().__attrs_post_init__()
6683

6784
# Custom /info endpoints (adds `show_times` options)
6885
def info(self):

0 commit comments

Comments
 (0)