From 90207d1ee85d86707edecd73f0500a7a994200d9 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Thu, 19 Oct 2023 00:05:19 +0200 Subject: [PATCH] Add algo (#59) * add algorithm support, swith to osm and swith to maplibre * update titiler --- CHANGES.md | 17 +++ pyproject.toml | 3 +- rio_viz/app.py | 212 +++++++++++++++------------------- rio_viz/templates/assets.html | 24 ++-- rio_viz/templates/bands.html | 26 ++--- rio_viz/templates/index.html | 31 ++--- rio_viz/templates/map.html | 16 +-- tests/test_app.py | 14 +-- 8 files changed, 158 insertions(+), 185 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 48d2fc8..5f66a4d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,21 @@ +# 0.12.0 (TBD) + +* add Algorithm support and update basemap source +* update titiler requirement to `>=0.14,<0.15` + +- renamed `/crop` endpoints to `/bbox/...` or `/feature/...` + - `/crop/{minx},{miny},{maxx},{maxy}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}.{format}` + + - `/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` + + - `/crop [POST]` -> `/feature [POST]` + + - `/crop.{format} [POST]` -> `/feature.{format} [POST]` + + - `/crop/{width}x{height}.{format} [POST]` -> `/feature/{width}x{height}.{format} [POST]` + + # 0.11.0 (2023-07-27) * update titiler requirement to `>=0.13,<0.14` diff --git a/pyproject.toml b/pyproject.toml index fe3bb55..cf9e449 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,7 @@ dynamic = ["version"] dependencies = [ "braceexpand", "rio-cogeo>=5.0", - "rio-tiler>=6.0,<7.0", - "titiler.core>=0.13.0,<0.14", + "titiler.core>=0.15.0,<0.16", "starlette-cramjam>=0.3,<0.4", "uvicorn", "server-thread>=0.2.0", diff --git a/rio_viz/app.py b/rio_viz/app.py index 605cd5f..13ea520 100644 --- a/rio_viz/app.py +++ b/rio_viz/app.py @@ -1,7 +1,6 @@ """rio_viz app.""" import pathlib -import sys import urllib.parse from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union @@ -17,12 +16,12 @@ from starlette.requests import Request from starlette.responses import HTMLResponse, Response from starlette.templating import Jinja2Templates -from starlette.types import ASGIApp from starlette_cramjam.middleware import CompressionMiddleware from typing_extensions import Annotated from rio_viz.resources.enums import RasterFormat, VectorTileFormat +from titiler.core.algorithm import algorithms as available_algorithms from titiler.core.dependencies import ( AssetsBidxExprParamsOptional, AssetsBidxParams, @@ -30,12 +29,14 @@ BandsExprParamsOptional, BandsParams, BidxExprParams, + ColorFormulaParams, ColorMapParams, DatasetParams, DefaultDependency, HistogramParams, - ImageParams, ImageRenderingParams, + PartFeatureParams, + PreviewParams, RescalingParams, StatisticsParams, ) @@ -43,6 +44,7 @@ from titiler.core.middleware import CacheControlMiddleware from titiler.core.models.mapbox import TileJSON from titiler.core.resources.responses import JSONResponse, XMLResponse +from titiler.core.utils import render_image try: from rio_tiler_mvt import pixels_encoder # noqa @@ -204,7 +206,7 @@ def info(params=Depends(self.info_dependency)): ) def statistics( layer_params=Depends(self.statistics_dependency), - image_params: ImageParams = Depends(), + image_params: PreviewParams = Depends(), dataset_params: DatasetParams = Depends(), stats_params: StatisticsParams = Depends(), histogram_params: HistogramParams = Depends(), @@ -269,21 +271,18 @@ def point( "description": "Return a preview.", } - @self.router.get(r"/preview", **preview_params, tags=["API"]) - @self.router.get(r"/preview.{format}", **preview_params, tags=["API"]) + @self.router.get("/preview", **preview_params, tags=["API"]) + @self.router.get("/preview.{format}", **preview_params, tags=["API"]) def preview( format: Optional[RasterFormat] = None, layer_params=Depends(self.layer_dependency), - img_params: ImageParams = Depends(), + img_params: PreviewParams = Depends(), dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + rescale: RescalingParams = Depends(), + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), ): """Handle /preview requests.""" with self.reader(self.src_path) as src_dst: # type: ignore @@ -300,25 +299,23 @@ def preview( ) dst_colormap = getattr(src_dst, "colormap", None) + if post_process: + image = post_process(image) + if rescale: image.rescale(rescale) if color_formula: image.apply_color_formula(color_formula) - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = RasterFormat.jpeg if image.mask.all() else RasterFormat.png - - content = image.render( - img_format=format.driver, - **format.profile, + content, media_type = render_image( + image, + output_format=format, + colormap=colormap or dst_colormap, **render_params, ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) part_params = { "responses": { @@ -332,35 +329,32 @@ def preview( } @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}.{format}", **part_params, tags=["API"], ) @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", **part_params, tags=["API"], ) def part( - minx: float = Path(..., description="Bounding box min X"), - miny: float = Path(..., description="Bounding box min Y"), - maxx: float = Path(..., description="Bounding box max X"), - maxy: float = Path(..., description="Bounding box max Y"), + minx: Annotated[float, Path(description="Bounding box min X")], + miny: Annotated[float, Path(description="Bounding box min Y")], + maxx: Annotated[float, Path(description="Bounding box max X")], + maxy: Annotated[float, Path(description="Bounding box max Y")], format: Annotated[ RasterFormat, "Output image type.", ] = RasterFormat.png, layer_params=Depends(self.layer_dependency), - img_params: ImageParams = Depends(), + img_params: PartFeatureParams = Depends(), dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + rescale: RescalingParams = Depends(), + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), ): """Create image from part of a dataset.""" with self.reader(self.src_path) as src_dst: # type: ignore @@ -378,22 +372,23 @@ def part( ) dst_colormap = getattr(src_dst, "colormap", None) + if post_process: + image = post_process(image) + if rescale: image.rescale(rescale) if color_formula: image.apply_color_formula(color_formula) - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - content = image.render( - img_format=format.driver, - **format.profile, + content, media_type = render_image( + image, + output_format=format, + colormap=colormap or dst_colormap, **render_params, ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) feature_params = { "responses": { @@ -406,25 +401,22 @@ def part( "description": "Return part of a dataset defined by a geojson feature.", } - @self.router.post(r"/crop", **feature_params, tags=["API"]) - @self.router.post(r"/crop.{format}", **feature_params, tags=["API"]) + @self.router.post("/feature", **feature_params, tags=["API"]) + @self.router.post("/feature.{format}", **feature_params, tags=["API"]) @self.router.post( - r"/crop/{width}x{height}.{format}", **feature_params, tags=["API"] + "/feature/{width}x{height}.{format}", **feature_params, tags=["API"] ) def geojson_part( geom: Feature, format: Annotated[Optional[RasterFormat], "Output image type."] = None, layer_params=Depends(self.layer_dependency), - img_params: ImageParams = Depends(), + img_params: PartFeatureParams = Depends(), dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + rescale: RescalingParams = Depends(), + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), ): """Handle /feature requests.""" with self.reader(self.src_path) as src_dst: # type: ignore @@ -439,25 +431,23 @@ def geojson_part( ) dst_colormap = getattr(src_dst, "colormap", None) + if post_process: + image = post_process(image) + if rescale: image.rescale(rescale) if color_formula: image.apply_color_formula(color_formula) - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = RasterFormat.jpeg if image.mask.all() else RasterFormat.png - - content = image.render( - img_format=format.driver, - **format.profile, + content, media_type = render_image( + image, + output_format=format, + colormap=colormap or dst_colormap, **render_params, ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) tile_params = { "responses": { @@ -470,30 +460,43 @@ def geojson_part( "description": "Read COG and return a tile", } - @self.router.get(r"/tiles/{z}/{x}/{y}", **tile_params, tags=["API"]) - @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **tile_params, tags=["API"]) + @self.router.get("/tiles/{z}/{x}/{y}", **tile_params, tags=["API"]) + @self.router.get("/tiles/{z}/{x}/{y}.{format}", **tile_params, tags=["API"]) def tile( - z: int, - x: int, - y: int, + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", + ), + ], format: Annotated[TileFormat, "Output tile type."] = None, layer_params=Depends(self.layer_dependency), dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), rescale: RescalingParams = Depends(), - color_formula: Annotated[ - Optional[str], - Query( - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - ] = None, + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), feature_type: Annotated[ Optional[Literal["point", "polygon"]], Query(title="Feature type (Only for MVT)"), ] = None, - tilesize: Optional[int] = Query(None, description="Tile Size."), + tilesize: Annotated[ + Optional[int], + Query(description="Tile Size."), + ] = None, + post_process=Depends(available_algorithms.dependency), ): """Handle /tiles requests.""" default_tilesize = 256 @@ -542,27 +545,27 @@ def tile( feature_type=feature_type, ) + media_type = format.mediatype + # Raster Tile else: + if post_process: + image = post_process(image) + if rescale: image.rescale(rescale) if color_formula: image.apply_color_formula(color_formula) - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = RasterFormat.jpeg if image.mask.all() else RasterFormat.png - - content = image.render( - img_format=format.driver, - **format.profile, + content, media_type = render_image( + image, + output_format=format, + colormap=colormap or dst_colormap, **render_params, ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) @self.router.get( "/tilejson.json", @@ -581,14 +584,9 @@ def tilejson( dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), rescale: RescalingParams = Depends(), - color_formula: Annotated[ - Optional[str], - Query( - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - ] = None, - colormap: ColorMapParams = Depends(), # noqa + color_formula: ColorFormulaParams = Depends(), + colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), feature_type: Annotated[ Optional[Literal["point", "polygon"]], Query(title="Feature type (Only for MVT)"), @@ -642,24 +640,11 @@ def wmts( ] = RasterFormat.png, layer_params=Depends(self.layer_dependency), dataset_params: DatasetParams = Depends(), - buffer: Annotated[ - Optional[float], - Query( - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - ] = None, render_params: ImageRenderingParams = Depends(), rescale: RescalingParams = Depends(), - color_formula: Annotated[ - Optional[str], - Query( - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - ] = None, + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), feature_type: Annotated[ Optional[Literal["point", "polygon"]], Query(title="Feature type (Only for MVT)"), @@ -735,14 +720,9 @@ def map_viewer( dataset_params: DatasetParams = Depends(), render_params: ImageRenderingParams = Depends(), rescale: RescalingParams = Depends(), - color_formula: Annotated[ - Optional[str], - Query( - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - ] = None, + color_formula: ColorFormulaParams = Depends(), colormap: ColorMapParams = Depends(), + post_process=Depends(available_algorithms.dependency), tilesize: Annotated[ Optional[int], Query(description="Tile Size."), diff --git a/rio_viz/templates/assets.html b/rio_viz/templates/assets.html index 40ae2e6..c75744b 100644 --- a/rio_viz/templates/assets.html +++ b/rio_viz/templates/assets.html @@ -4,8 +4,8 @@ rio-viz for Asset Reader (STAC) - - + + @@ -178,7 +178,7 @@ -ms-transition: all .5s ease; transition: all ease .5s; } - .mapboxgl-ctrl-attrib { + .maplibregl-ctrl-attrib { font-size: 10px; } } @@ -315,8 +315,6 @@ crossing_dateline: false } -mapboxgl.accessToken = '' - const tilejson_endpoint = '{{ tilejson_endpoint }}' const info_endpoint = '{{ info_endpoint }}' const stats_endpoint = '{{ stats_endpoint }}' @@ -333,29 +331,25 @@ 'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308] } -var map = new mapboxgl.Map({ +var map = new maplibregl.Map({ container: 'map', style: { version: 8, sources: { - 'toner-lite': { + 'basemap': { type: 'raster', tiles: [ - 'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png' + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' ], tileSize: 256, - attribution: - 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' + attribution: '© OpenStreetMap' } }, layers: [ { 'id': 'basemap', 'type': 'raster', - 'source': 'toner-lite', + 'source': 'basemap', 'minzoom': 0, 'maxzoom': 20 } @@ -1020,7 +1014,7 @@ html += `lon${e.lngLat.lng.toString().slice(0, 7)}` html += `lat${e.lngLat.lat.toString().slice(0, 7)}` html += '' - new mapboxgl.Popup() + new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(html) .addTo(map) diff --git a/rio_viz/templates/bands.html b/rio_viz/templates/bands.html index afdabb1..058fc19 100644 --- a/rio_viz/templates/bands.html +++ b/rio_viz/templates/bands.html @@ -4,8 +4,8 @@ Rio Viz - - + + @@ -174,7 +174,7 @@ -ms-transition: all .5s ease; transition: all ease .5s; } - .mapboxgl-ctrl-attrib { + .maplibregl-ctrl-attrib { font-size: 10px; } } @@ -326,8 +326,6 @@ crossing_dateline: false } -mapboxgl.accessToken = '' - const tilejson_endpoint = '{{ tilejson_endpoint }}' const info_endpoint = '{{ info_endpoint }}' const stats_endpoint = '{{ stats_endpoint }}' @@ -349,29 +347,25 @@ document.getElementById('3d-poly').classList.add('none') } -var map = new mapboxgl.Map({ +var map = new maplibregl.Map({ container: 'map', style: { version: 8, sources: { - 'toner-lite': { + 'basemap': { type: 'raster', tiles: [ - 'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', - 'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png' + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' ], tileSize: 256, - attribution: - 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' + attribution: '© OpenStreetMap' } }, layers: [ { 'id': 'basemap', 'type': 'raster', - 'source': 'toner-lite', + 'source': 'basemap', 'minzoom': 0, 'maxzoom': 20 } @@ -1080,7 +1074,7 @@ html += `lon${e.lngLat.lng.toString().slice(0, 7)}` html += `lat${e.lngLat.lat.toString().slice(0, 7)}` html += '' - new mapboxgl.Popup() + new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(html) .addTo(map) @@ -1118,7 +1112,7 @@ html += `lon${e.lngLat.lng.toString().slice(0, 7)}` html += `lat${e.lngLat.lat.toString().slice(0, 7)}` html += '' - new mapboxgl.Popup() + new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(html) .addTo(map) diff --git a/rio_viz/templates/index.html b/rio_viz/templates/index.html index a59601e..f04b0cd 100644 --- a/rio_viz/templates/index.html +++ b/rio_viz/templates/index.html @@ -4,11 +4,11 @@ Rio Viz - - + + - - + +