diff --git a/CHANGES.md b/CHANGES.md index ee5667a93..47a3487dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ ## Next Release * Allow a default `color_formula` parameter to be set via a dependency (author @samn, https://github.com/developmentseed/titiler/pull/707) - +* add `titiler.core.dependencies.create_colormap_dependency` to create ColorMapParams dependency from `rio_tiler.colormap.ColorMaps` object ## 0.15.0 (2023-09-28) diff --git a/docs/src/advanced/customization.md b/docs/src/advanced/customization.md index 13bcbfaa6..b2f09ac19 100644 --- a/docs/src/advanced/customization.md +++ b/docs/src/advanced/customization.md @@ -1,6 +1,33 @@ `TiTiler` is designed to help user customize input/output for each endpoint. This section goes over some simple customization examples. +### Custom Colormap + +Add user defined colormap to the default colormaps provided by rio-tiler + +```python +from fastapi import FastAPI + +from rio_tiler.colormap import cmap as default_cmap + +from titiler.core.dependencies import create_colormap_dependency +from titiler.core.factory import TilerFactory + + +app = FastAPI(title="My simple app with custom TMS") + +cmap_values = { + "cmap1": {6: (4, 5, 6, 255)}, +} +# add custom colormap `cmap1` to the default colormaps +cmap = default_cmap.register(cmap_values) +ColorMapParams = create_colormap_dependency(cmap) + + +cog = TilerFactory(colormap_dependency=ColorMapParams) +app.include_router(cog.router) +``` + ### Custom DatasetPathParams for `reader_dependency` One common customization could be to create your own `path_dependency`. This dependency is used on all endpoint and pass inputs to the *Readers* (MosaicBackend, COGReader, STACReader...). diff --git a/docs/src/examples/code/tiler_with_custom_colormap.md b/docs/src/examples/code/tiler_with_custom_colormap.md index 8a9e5f86e..0ea9cb30d 100644 --- a/docs/src/examples/code/tiler_with_custom_colormap.md +++ b/docs/src/examples/code/tiler_with_custom_colormap.md @@ -33,34 +33,34 @@ app/dependencies.py """ import json -from enum import Enum -from typing import Dict, Optional + +from typing import Dict, Optional, Literal +from typing_extensions import Annotated import numpy import matplotlib -from rio_tiler.colormap import cmap, parse_color +from rio_tiler.colormap import parse_color +from rio_tiler.colormap import cmap as default_cmap from fastapi import HTTPException, Query -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - -class ColorMapType(str, Enum): - """Colormap types.""" - - explicit = "explicit" - linear = "linear" - - def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), - colormap_type: ColorMapType = Query(ColorMapType.explicit, description="User input colormap type."), + colormap_name: Annotated[ # type: ignore + Literal[tuple(default_cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + str, + Query(description="JSON encoded custom Colormap"), + ] = None, + colormap_type: Annotated[ + Literal["explicit", "linear"], + Query(description="User input colormap type."), + ] = "explicit", ) -> Optional[Dict]: """Colormap Dependency.""" if colormap_name: - return cmap.get(colormap_name.value) + return default_cmap.get(colormap_name) if colormap: try: @@ -73,7 +73,7 @@ def ColorMapParams( status_code=400, detail="Could not parse the colormap value." ) - if colormap_type == ColorMapType.linear: + if colormap_type == "linear": # input colormap has to start from 0 to 255 ? cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custom', diff --git a/src/titiler/core/tests/test_CustomCmap.py b/src/titiler/core/tests/test_CustomCmap.py index c509bcaa6..8b396aaf7 100644 --- a/src/titiler/core/tests/test_CustomCmap.py +++ b/src/titiler/core/tests/test_CustomCmap.py @@ -1,15 +1,13 @@ """Test TiTiler Custom Colormap Params.""" -from enum import Enum from io import BytesIO -from typing import Dict, Optional import numpy -from fastapi import FastAPI, Query +from fastapi import FastAPI from rio_tiler.colormap import ColorMaps from starlette.testclient import TestClient -from typing_extensions import Annotated +from titiler.core.dependencies import create_colormap_dependency from titiler.core.factory import TilerFactory from .conftest import DATA_DIR @@ -18,22 +16,8 @@ "cmap1": {6: (4, 5, 6, 255)}, } cmap = ColorMaps(data=cmap_values) -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - - -def ColorMapParams( - colormap_name: Annotated[ - ColorMapName, - Query(description="Colormap name"), - ] = None, -) -> Optional[Dict]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) - - return None + +ColorMapParams = create_colormap_dependency(cmap) def test_CustomCmap(): diff --git a/src/titiler/core/titiler/core/algorithm/__init__.py b/src/titiler/core/titiler/core/algorithm/__init__.py index b77327384..85481f03b 100644 --- a/src/titiler/core/titiler/core/algorithm/__init__.py +++ b/src/titiler/core/titiler/core/algorithm/__init__.py @@ -2,7 +2,7 @@ import json from copy import copy -from typing import Dict, List, Literal, Optional, Type +from typing import Annotated, Dict, List, Literal, Optional, Type import attr from fastapi import HTTPException, Query @@ -55,10 +55,13 @@ def dependency(self): """FastAPI PostProcess dependency.""" def post_process( - algorithm: Literal[tuple(self.data.keys())] = Query( - None, description="Algorithm name" - ), - algorithm_params: str = Query(None, description="Algorithm parameter"), + algorithm: Annotated[ + Literal[tuple(self.data.keys())], Query(description="Algorithm name") + ] = None, + algorithm_params: Annotated[ + Optional[str], + Query(description="Algorithm parameter"), + ] = None, ) -> Optional[BaseAlgorithm]: """Data Post-Processing options.""" kwargs = json.loads(algorithm_params) if algorithm_params else {} diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index 18ece156a..2f2ae07d7 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -2,53 +2,59 @@ import json from dataclasses import dataclass -from enum import Enum -from typing import Dict, List, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union import numpy from fastapi import HTTPException, Query from rasterio.crs import CRS -from rio_tiler.colormap import cmap, parse_color +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.colormap import parse_color from rio_tiler.errors import MissingAssets, MissingBands -from rio_tiler.types import ColorMapType, RIOResampling +from rio_tiler.types import RIOResampling from typing_extensions import Annotated -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) +def create_colormap_dependency(cmap: ColorMaps) -> Callable: + """Create Colormap Dependency.""" -def ColorMapParams( - colormap_name: Annotated[ - Optional[ColorMapName], - Query(description="Colormap name"), - ] = None, - colormap: Annotated[ - Optional[str], Query(description="JSON encoded custom Colormap") - ] = None, -) -> Optional[ColorMapType]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) - - if colormap: - try: - c = json.loads( - colormap, - object_hook=lambda x: {int(k): parse_color(v) for k, v in x.items()}, - ) + def deps( + colormap_name: Annotated[ # type: ignore + Literal[tuple(cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, + ): + if colormap_name: + return cmap.get(colormap_name) - # Make sure to match colormap type - if isinstance(c, Sequence): - c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) - return c - except json.JSONDecodeError as e: - raise HTTPException( - status_code=400, detail="Could not parse the colormap value." - ) from e + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] - return None + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None + + return deps + + +ColorMapParams = create_colormap_dependency(default_cmap) def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str: