From 921ca0fac204128755430180f7a1526eb181b3e5 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Tue, 13 Jun 2023 16:07:44 +0200 Subject: [PATCH 001/405] use titiler custom JSONResponse to handle NaN values (#659) --- CHANGES.md | 6 ++++++ src/titiler/extensions/titiler/extensions/cogeo.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2481fa831..aaff47770 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Release Notes +## Next (TDB) + +### titiler.extensions + +* use TiTiler's custom JSONResponse for the `/validate` endpoint to avoid issue when COG has `NaN` nodata value + ## 0.11.7 (2023-05-18) ### titiler.core diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 5b0193060..2347d44db 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -5,6 +5,7 @@ from fastapi import Depends, Query from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.resources.responses import JSONResponse try: from rio_cogeo.cogeo import cog_info @@ -25,7 +26,11 @@ def register(self, factory: BaseTilerFactory): cog_info is not None ), "'rio_cogeo' must be installed to use CogValidateExtension" - @factory.router.get("/validate", response_model=Info) + @factory.router.get( + "/validate", + response_model=Info, + response_class=JSONResponse, + ) def validate( src_path: str = Depends(factory.path_dependency), strict: bool = Query(False, description="Treat warnings as errors"), From c85b18a9ec90301fe10cfd80aa2917bc26bda924 Mon Sep 17 00:00:00 2001 From: Ofir Makmal Date: Tue, 20 Jun 2023 14:26:01 +0300 Subject: [PATCH 002/405] Added hostpath, imagepullsecret and termination grace priod support. --- .../k8s/charts/templates/deployment.yaml | 21 +++++++++++++++++++ deployment/k8s/charts/values-test.yaml | 11 ++++++++++ deployment/k8s/charts/values.yaml | 11 ++++++++++ 3 files changed, 43 insertions(+) diff --git a/deployment/k8s/charts/templates/deployment.yaml b/deployment/k8s/charts/templates/deployment.yaml index 9858c13a6..5fa7255bc 100644 --- a/deployment/k8s/charts/templates/deployment.yaml +++ b/deployment/k8s/charts/templates/deployment.yaml @@ -49,10 +49,31 @@ spec: - mountPath: /config name: config readOnly: true + {{- range .Values.extraHostPathMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} + {{- if .mountPropagation }} + mountPropagation: {{ .mountPropagation }} + {{- end }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.env.terminationGracePeriodSeconds }} volumes: - name: config configMap: name: {{ include "titiler.fullname" . }}-configmap + {{- range .Values.extraHostPathMounts }} + - name: {{ .name }} + hostPath: + path: {{ .hostPath }} + type: Directory + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- range . }} + - name: {{ .name }} + {{- end }} + {{- end }} {{- with .Values.serviceAccountName }} serviceAccountName: {{ . | quote }} {{- end }} diff --git a/deployment/k8s/charts/values-test.yaml b/deployment/k8s/charts/values-test.yaml index 47bd601e4..b8865ba9c 100644 --- a/deployment/k8s/charts/values-test.yaml +++ b/deployment/k8s/charts/values-test.yaml @@ -14,6 +14,17 @@ ingress: hosts: - titiler.charter.uat.esaportal.eu +terminationGracePeriodSeconds: 30 + +extraHostPathMounts: [] + # - name: map-sources + # mountPath: /map-sources/ + # hostPath: /home/ubuntu/map-sources + # readOnly: false + # mountPropagation: HostToContainer # OPTIONAL + +imagePullSecrets: [] + env: PORT: 80 CPL_TMPDIR: /tmp diff --git a/deployment/k8s/charts/values.yaml b/deployment/k8s/charts/values.yaml index b10995183..b161b8534 100644 --- a/deployment/k8s/charts/values.yaml +++ b/deployment/k8s/charts/values.yaml @@ -9,6 +9,8 @@ image: nameOverride: "" fullnameOverride: "" +terminationGracePeriodSeconds: 30 + service: type: ClusterIP port: 80 @@ -26,6 +28,15 @@ ingress: # hosts: # - titiler.local +extraHostPathMounts: [] + # - name: map-sources + # mountPath: /map-sources/ + # hostPath: /home/ubuntu/map-sources + # readOnly: false + # mountPropagation: HostToContainer # OPTIONAL + +imagePullSecrets: [] + env: PORT: 80 CPL_TMPDIR: /tmp From e30143a83d8904c0ea755d4f9f1cdaed20a9247f Mon Sep 17 00:00:00 2001 From: Ofir Makmal Date: Tue, 20 Jun 2023 15:08:24 +0300 Subject: [PATCH 003/405] Version bump --- deployment/k8s/charts/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml index febe06e3e..5470f4a0d 100644 --- a/deployment/k8s/charts/Chart.yaml +++ b/deployment/k8s/charts/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v1 appVersion: 0.11.7 description: A dynamic Web Map tile server name: titiler -version: 1.1.0 +version: 1.1.1 icon: https://raw.githubusercontent.com/developmentseed/titiler/main/docs/logos/TiTiler_logo_small.png maintainers: - name: emmanuelmathot # Emmanuel Mathot From 047453fced0cf9d7b7e43c08a4ba4e4a569d9e04 Mon Sep 17 00:00:00 2001 From: holgerbach <132660929+holgerbach@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:05:54 +0200 Subject: [PATCH 004/405] Security contexts for k8s (#657) --- deployment/k8s/charts/Chart.yaml | 2 +- deployment/k8s/charts/templates/deployment.yaml | 4 ++++ deployment/k8s/charts/values.yaml | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml index 5470f4a0d..9d003318e 100644 --- a/deployment/k8s/charts/Chart.yaml +++ b/deployment/k8s/charts/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v1 appVersion: 0.11.7 description: A dynamic Web Map tile server name: titiler -version: 1.1.1 +version: 1.1.2 icon: https://raw.githubusercontent.com/developmentseed/titiler/main/docs/logos/TiTiler_logo_small.png maintainers: - name: emmanuelmathot # Emmanuel Mathot diff --git a/deployment/k8s/charts/templates/deployment.yaml b/deployment/k8s/charts/templates/deployment.yaml index 5fa7255bc..fca1a0b86 100644 --- a/deployment/k8s/charts/templates/deployment.yaml +++ b/deployment/k8s/charts/templates/deployment.yaml @@ -14,10 +14,14 @@ spec: labels: {{- include "titiler.selectorLabels" . | nindent 8 }} spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} env: {{- range $key, $val := .Values.env }} - name: {{ $key }} diff --git a/deployment/k8s/charts/values.yaml b/deployment/k8s/charts/values.yaml index b161b8534..ac3a54f6b 100644 --- a/deployment/k8s/charts/values.yaml +++ b/deployment/k8s/charts/values.yaml @@ -65,3 +65,17 @@ nodeSelector: {} tolerations: [] affinity: {} + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # allowPrivilegeEscalation: false + # runAsNonRoot: true + # runAsUser: 1001 + +podSecurityContext: {} + # fsGroup: 1001 + # runAsNonRoot: true + # runAsUser: 1001 From a180642adcdb39d027e2d03867bba691411e2928 Mon Sep 17 00:00:00 2001 From: Abhemanyu Sarin <86159004+abhemanyus@users.noreply.github.com> Date: Thu, 6 Jul 2023 20:30:35 +0530 Subject: [PATCH 005/405] Fix pydantic to last working version (#663) Pydantic underwent a major API change in June-July 2023, from v1 to v2. --- deployment/aws/requirements-cdk.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/aws/requirements-cdk.txt b/deployment/aws/requirements-cdk.txt index 973fc3433..ac5a11182 100644 --- a/deployment/aws/requirements-cdk.txt +++ b/deployment/aws/requirements-cdk.txt @@ -5,5 +5,5 @@ aws_cdk-aws_apigatewayv2_integrations_alpha==2.76.0a0 constructs>=10.0.0 # pydantic settings -pydantic +pydantic==1.10.11 python-dotenv From 8d6776c162587982991bdf67b2aaa103081ac966 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Mon, 10 Jul 2023 18:05:33 +0200 Subject: [PATCH 006/405] sketch use of Annotated types (#612) * sketch use of Annotated types * fix * fix2 * full round of annotations * more annotations * update dependencies * update changelog --- .pre-commit-config.yaml | 2 +- CHANGES.md | 7 + .../application/titiler/application/main.py | 6 - src/titiler/core/pyproject.toml | 6 +- src/titiler/core/tests/test_CustomCmap.py | 12 +- src/titiler/core/tests/test_CustomPath.py | 18 +- .../core/tests/test_cache_middleware.py | 18 +- .../core/tests/test_case_middleware.py | 10 +- src/titiler/core/tests/test_dependencies.py | 17 +- src/titiler/core/titiler/core/dependencies.py | 455 ++++++++++-------- src/titiler/core/titiler/core/factory.py | 420 +++++++++------- .../extensions/titiler/extensions/cogeo.py | 13 +- .../extensions/titiler/extensions/stac.py | 109 +++-- .../extensions/titiler/extensions/wms.py | 23 +- src/titiler/mosaic/titiler/mosaic/factory.py | 328 +++++++------ 15 files changed, 842 insertions(+), 602 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63ed17680..cffb6f353 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: args: ["--fix"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.3.0 hooks: - id: mypy language_version: python diff --git a/CHANGES.md b/CHANGES.md index aaff47770..dafa05b54 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,13 @@ ## Next (TDB) +* use `Annotated` Type for Query/Path parameters + +### titiler.core + +* update FastAPI dependency to `>=0.95.1` +* set `pydantic` dependency to `~=1.0` + ### titiler.extensions * use TiTiler's custom JSONResponse for the `/validate` endpoint to avoid issue when COG has `NaN` nodata value diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index d6366d8bd..89634e01c 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -35,12 +35,6 @@ from titiler.mosaic.errors import MOSAIC_STATUS_CODES from titiler.mosaic.factory import MosaicTilerFactory -try: - pass # type: ignore -except ImportError: - # Try backported to PY<39 `importlib_resources`. - pass # type: ignore - logging.getLogger("botocore.credentials").disabled = True logging.getLogger("botocore.utils").disabled = True logging.getLogger("rio-tiler").setLevel(logging.ERROR) diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index f964c014a..1c6fbcbe4 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -29,11 +29,11 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "fastapi>=0.87.0,<0.95", - "geojson-pydantic", + "fastapi>=0.95.1", + "geojson-pydantic>=0.4,<0.7", "jinja2>=2.11.2,<4.0.0", "numpy", - "pydantic", + "pydantic~=1.0", "rasterio", "rio-tiler>=4.1.6,<4.2", "simplejson", diff --git a/src/titiler/core/tests/test_CustomCmap.py b/src/titiler/core/tests/test_CustomCmap.py index 0c189247a..0d16a7038 100644 --- a/src/titiler/core/tests/test_CustomCmap.py +++ b/src/titiler/core/tests/test_CustomCmap.py @@ -1,5 +1,6 @@ """Test TiTiler Custom Colormap Params.""" +import sys from enum import Enum from io import BytesIO from typing import Dict, Optional @@ -13,6 +14,12 @@ from .conftest import DATA_DIR +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + + cmap_values = { "cmap1": {6: (4, 5, 6, 255)}, } @@ -23,7 +30,10 @@ def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), + colormap_name: Annotated[ + ColorMapName, + Query(description="Colormap name"), + ] = None, ) -> Optional[Dict]: """Colormap Dependency.""" if colormap_name: diff --git a/src/titiler/core/tests/test_CustomPath.py b/src/titiler/core/tests/test_CustomPath.py index cb1bd3298..aa76977b9 100644 --- a/src/titiler/core/tests/test_CustomPath.py +++ b/src/titiler/core/tests/test_CustomPath.py @@ -2,6 +2,7 @@ import os import re +import sys from fastapi import FastAPI, HTTPException, Query from starlette.testclient import TestClient @@ -10,13 +11,20 @@ from .conftest import DATA_DIR +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + def CustomPathParams( - name: str = Query( - ..., - alias="file", - description="Give me a url.", - ) + name: Annotated[ + str, + Query( + alias="file", + description="Give me a url.", + ), + ], ) -> str: """Custom path Dependency.""" if not re.match(".+tif$", name): diff --git a/src/titiler/core/tests/test_cache_middleware.py b/src/titiler/core/tests/test_cache_middleware.py index b1331edd9..87dc51ddf 100644 --- a/src/titiler/core/tests/test_cache_middleware.py +++ b/src/titiler/core/tests/test_cache_middleware.py @@ -1,5 +1,6 @@ """Test titiler.core.CacheControlMiddleware.""" +import sys from fastapi import FastAPI, Path from starlette.responses import Response @@ -7,6 +8,11 @@ from titiler.core.middleware import CacheControlMiddleware +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + def test_cachecontrol_middleware_exclude(): """Create App.""" @@ -29,18 +35,18 @@ async def route3(): @app.get("/tiles/{z}/{x}/{y}") async def tiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return "yeah" @app.get("/emptytiles/{z}/{x}/{y}") async def emptytiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return Response(status_code=404) diff --git a/src/titiler/core/tests/test_case_middleware.py b/src/titiler/core/tests/test_case_middleware.py index dc46ab5ae..13534dd66 100644 --- a/src/titiler/core/tests/test_case_middleware.py +++ b/src/titiler/core/tests/test_case_middleware.py @@ -1,5 +1,6 @@ """Test titiler.core.middleware.LowerCaseQueryStringMiddleware.""" +import sys from typing import List from fastapi import FastAPI, Query @@ -7,13 +8,18 @@ from titiler.core.middleware import LowerCaseQueryStringMiddleware +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + def test_lowercase_middleware(): """Make sure upper and lower case QS are accepted.""" app = FastAPI() @app.get("/route1") - async def route1(value: str = Query(...)): + async def route1(value: Annotated[str, Query()]): """route1.""" return {"value": value} @@ -33,7 +39,7 @@ def test_lowercase_middleware_multiple_values(): app = FastAPI() @app.get("/route1") - async def route1(value: List[str] = Query(...)): + async def route1(value: Annotated[List[str], Query()]): """route1.""" return {"value": value} diff --git a/src/titiler/core/tests/test_dependencies.py b/src/titiler/core/tests/test_dependencies.py index 6254aa471..2e28ac9b3 100644 --- a/src/titiler/core/tests/test_dependencies.py +++ b/src/titiler/core/tests/test_dependencies.py @@ -1,11 +1,12 @@ """test dependencies.""" import json +import sys from dataclasses import dataclass from typing import Literal import pytest -from fastapi import Depends, FastAPI, Query +from fastapi import Depends, FastAPI, Path from morecantile import tms from rio_tiler.types import ColorMapType from starlette.testclient import TestClient @@ -13,18 +14,28 @@ from titiler.core import dependencies, errors from titiler.core.resources.responses import JSONResponse +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + def test_tms(): """Create App.""" app = FastAPI() @app.get("/web/{TileMatrixSetId}") - def web(TileMatrixSetId: Literal["WebMercatorQuad"] = Query(...)): + def web( + TileMatrixSetId: Annotated[ + Literal["WebMercatorQuad"], + Path(), + ], + ): """return tms id.""" return TileMatrixSetId @app.get("/all/{TileMatrixSetId}") - def all(TileMatrixSetId: Literal[tuple(tms.list())] = Query(...)): + def all(TileMatrixSetId: Annotated[Literal[tuple(tms.list())], Path()]): """return tms id.""" return TileMatrixSetId diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index efc19260c..9a06aeaa2 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -1,6 +1,7 @@ """Common dependency.""" import json +import sys from dataclasses import dataclass from enum import Enum from typing import Dict, List, Optional, Sequence, Tuple, Union @@ -13,6 +14,12 @@ from rio_tiler.errors import MissingAssets, MissingBands from rio_tiler.types import ColorMapType +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + + ColorMapName = Enum( # type: ignore "ColorMapName", [(a, a) for a in sorted(cmap.list())] ) @@ -22,8 +29,13 @@ def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), + 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: @@ -49,7 +61,7 @@ def ColorMapParams( return None -def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str: +def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str: """Create dataset path from args""" return url @@ -72,31 +84,35 @@ def __getitem__(self, key): class BidxParams(DefaultDependency): """Band Indexes parameters.""" - indexes: Optional[List[int]] = Query( - None, - title="Band indexes", - alias="bidx", - description="Dataset band indexes", - examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, - ) + indexes: Annotated[ + Optional[List[int]], + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, + ), + ] = None @dataclass class ExpressionParams(DefaultDependency): """Expression parameters.""" - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="rio-tiler's band math expression", - examples={ - "simple": {"description": "Simple band math.", "value": "b1/b2"}, - "multi-bands": { - "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", - "value": "b1/b2;b2+b3", + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="rio-tiler's band math expression", + examples={ + "simple": {"description": "Simple band math.", "value": "b1/b2"}, + "multi-bands": { + "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", + "value": "b1/b2;b2+b3", + }, }, - }, - ) + ), + ] = None @dataclass @@ -111,76 +127,69 @@ class BidxExprParams(ExpressionParams, BidxParams): class AssetsParams(DefaultDependency): """Assets parameters.""" - assets: List[str] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], + assets: Annotated[ + Optional[List[str]], + Query( + title="Asset names", + description="Asset's names.", + examples={ + "one-asset": { + "description": "Return results for asset `data`.", + "value": ["data"], + }, + "multi-assets": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data", "cog"], + }, }, - }, - ) + ), + ] = None @dataclass -class AssetsBidxExprParams(DefaultDependency): +class AssetsBidxExprParams(AssetsParams): """Assets, Expression and Asset's band Indexes parameters.""" - assets: Optional[List[str]] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="Band math expression between assets", - examples={ - "simple": { - "description": "Return results of expression between assets.", - "value": "asset1_b1 + asset2_b1 / asset3_b1", - }, - }, - ) - - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes (coma separated indexes)", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1,2,3"], + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="Band math expression between assets", + examples={ + "simple": { + "description": "Return results of expression between assets.", + "value": "asset1_b1 + asset2_b1 / asset3_b1", + }, }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1,2,3", "cog|1"], + ), + ] = None + + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes (coma separated indexes)", + alias="asset_bidx", + examples={ + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1,2,3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1,2,3", "cog|1"], + }, }, - }, - ) + ), + ] = None - asset_as_band: Optional[bool] = Query( - None, - title="Consider asset as a 1 band dataset", - description="Asset as Band", - ) + asset_as_band: Annotated[ + Optional[bool], + Query( + title="Consider asset as a 1 band dataset", + description="Asset as Band", + ), + ] = None def __post_init__(self): """Post Init.""" @@ -213,38 +222,42 @@ def __post_init__(self): class AssetsBidxParams(AssetsParams): """Assets, Asset's band Indexes and Asset's band Expression parameters.""" - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1;2;3"], + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes", + alias="asset_bidx", + examples={ + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1;2;3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1;2;3", "cog|1"], + }, }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1;2;3", "cog|1"], + ), + ] = None + + asset_expression: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band expression", + description="Per asset band expression", + examples={ + "one-asset": { + "description": "Return results for expression `b1*b2+b3` of asset `data`.", + "value": ["data|b1*b2+b3"], + }, + "multi-assets": { + "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", + "value": ["data|b1*b2+b3", "cog|b1+b3"], + }, }, - }, - ) - - asset_expression: Optional[Sequence[str]] = Query( - None, - title="Per asset band expression", - description="Per asset band expression", - examples={ - "one-asset": { - "description": "Return results for expression `b1*b2+b3` of asset `data`.", - "value": ["data|b1*b2+b3"], - }, - "multi-assets": { - "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", - "value": ["data|b1*b2+b3", "cog|b1+b3"], - }, - }, - ) + ), + ] = None def __post_init__(self): """Post Init.""" @@ -265,21 +278,23 @@ def __post_init__(self): class BandsParams(DefaultDependency): """Band names parameters.""" - bands: List[str] = Query( - None, - title="Band names", - description="Band's names.", - examples={ - "one-band": { - "description": "Return results for band `B01`.", - "value": ["B01"], - }, - "multi-bands": { - "description": "Return results for bands `B01` and `B02`.", - "value": ["B01", "B02"], + bands: Annotated[ + Optional[List[str]], + Query( + title="Band names", + description="Band's names.", + examples={ + "one-band": { + "description": "Return results for band `B01`.", + "value": ["B01"], + }, + "multi-bands": { + "description": "Return results for bands `B01` and `B02`.", + "value": ["B01", "B02"], + }, }, - }, - ) + ), + ] = None @dataclass @@ -305,11 +320,9 @@ def __post_init__(self): class ImageParams(DefaultDependency): """Common Preview/Crop parameters.""" - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." - ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") + max_size: Annotated[int, "Maximum image size to read onto."] = 1024 + height: Annotated[Optional[int], "Force output image height."] = None + width: Annotated[Optional[int], "Force output image width."] = None def __post_init__(self): """Post Init.""" @@ -321,19 +334,27 @@ def __post_init__(self): class DatasetParams(DefaultDependency): """Low level WarpedVRT Optional parameters.""" - nodata: Optional[Union[str, int, float]] = Query( - None, title="Nodata value", description="Overwrite internal Nodata value" - ) - unscale: Optional[bool] = Query( - False, - title="Apply internal Scale/Offset", - description="Apply internal Scale/Offset", - ) - resampling_method: ResamplingName = Query( - ResamplingName.nearest, # type: ignore - alias="resampling", - description="Resampling method.", - ) + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + unscale: Annotated[ + Optional[bool], + Query( + title="Apply internal Scale/Offset", + description="Apply internal Scale/Offset", + ), + ] = False + resampling_method: Annotated[ + ResamplingName, + Query( + alias="resampling", + description="Resampling method.", + ), + ] = ResamplingName.nearest # type: ignore def __post_init__(self): """Post Init.""" @@ -346,21 +367,27 @@ def __post_init__(self): class ImageRenderingParams(DefaultDependency): """Image Rendering options.""" - add_mask: bool = Query( - True, alias="return_mask", description="Add mask to the output data." - ) + add_mask: Annotated[ + bool, + Query( + alias="return_mask", + description="Add mask to the output data.", + ), + ] = True RescaleType = List[Tuple[float, ...]] def RescalingParams( - rescale: Optional[List[str]] = Query( - None, - title="Min/Max data Rescaling", - description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", - example=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 - ) + rescale: Annotated[ + Optional[List[str]], + Query( + title="Min/Max data Rescaling", + description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", + example=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + ), + ] = None, ) -> Optional[RescaleType]: """Min/Max data Rescaling""" if rescale: @@ -373,57 +400,70 @@ def RescalingParams( class StatisticsParams(DefaultDependency): """Statistics options.""" - categorical: bool = Query( - False, description="Return statistics for categorical dataset." - ) - categories: List[Union[float, int]] = Query( - None, - alias="c", - title="Pixels values for categories.", - description="List of values for which to report counts.", - example=[1, 2, 3], - ) - percentiles: List[int] = Query( - [2, 98], - alias="p", - title="Percentile values", - description="List of percentile values.", - example=[2, 5, 95, 98], - ) + categorical: Annotated[ + bool, + Query(description="Return statistics for categorical dataset."), + ] = False + categories: Annotated[ + Optional[List[Union[float, int]]], + Query( + alias="c", + title="Pixels values for categories.", + description="List of values for which to report counts.", + example=[1, 2, 3], + ), + ] = None + percentiles: Annotated[ + Optional[List[int]], + Query( + alias="p", + title="Percentile values", + description="List of percentile values.", + example=[2, 5, 95, 98], + ), + ] = None + + def __post_init__(self): + """Set percentiles default.""" + if not self.percentiles: + self.percentiles = [2, 98] @dataclass class HistogramParams(DefaultDependency): """Numpy Histogram options.""" - bins: Optional[str] = Query( - None, - alias="histogram_bins", - title="Histogram bins.", - description=""" + bins: Annotated[ + Optional[str], + Query( + alias="histogram_bins", + title="Histogram bins.", + description=""" Defines the number of equal-width bins in the given range (10, by default). If bins is a sequence (comma `,` delimited values), it defines a monotonically increasing array of bin edges, including the rightmost edge, allowing for non-uniform bin widths. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - examples={ - "simple": { - "description": "Defines the number of equal-width bins", - "value": 8, - }, - "array": { - "description": "Defines custom bin edges (comma `,` delimited values)", - "value": "0,100,200,300", + """, + examples={ + "simple": { + "description": "Defines the number of equal-width bins", + "value": 8, + }, + "array": { + "description": "Defines custom bin edges (comma `,` delimited values)", + "value": "0,100,200,300", + }, }, - }, - ) - - range: Optional[str] = Query( - None, - alias="histogram_range", - title="Histogram range", - description=""" + ), + ] = None + + range: Annotated[ + Optional[str], + Query( + alias="histogram_range", + title="Histogram range", + description=""" Comma `,` delimited range of the bins. The lower and upper range of the bins. If not provided, range is simply (a.min(), a.max()). @@ -432,9 +472,10 @@ class HistogramParams(DefaultDependency): range affects the automatic bin computation as well. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - example="0,1000", - ) + """, + example="0,1000", + ), + ] = None def __post_init__(self): """Post Init.""" @@ -452,11 +493,13 @@ def __post_init__(self): def CoordCRSParams( - crs: str = Query( - None, - alias="coord-crs", - description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", - ) + crs: Annotated[ + Optional[str], + Query( + alias="coord-crs", + description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", + ), + ] = None, ) -> Optional[CRS]: """Coordinate Reference System Coordinates Param.""" if crs: @@ -466,11 +509,13 @@ def CoordCRSParams( def DstCRSParams( - crs: str = Query( - None, - alias="dst-crs", - description="Output Coordinate Reference System.", - ) + crs: Annotated[ + Optional[str], + Query( + alias="dst-crs", + description="Output Coordinate Reference System.", + ), + ] = None, ) -> Optional[CRS]: """Coordinate Reference System Coordinates Param.""" if crs: diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 52e16a3d6..469be0609 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1,6 +1,7 @@ """TiTiler Router factories.""" import abc +import sys from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, Union from urllib.parse import urlencode @@ -15,7 +16,7 @@ from morecantile import TileMatrixSet from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from rasterio.crs import CRS +from pydantic import conint from rio_tiler.constants import WGS84_CRS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader from rio_tiler.models import BandStatistics, Bounds, Info @@ -66,6 +67,12 @@ from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.core.routing import EndpointScope +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + + DEFAULT_TEMPLATES = Jinja2Templates( directory="", loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), @@ -428,11 +435,12 @@ def statistics( }, ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), @@ -498,35 +506,40 @@ def tile(self): # noqa: C901 **img_endpoint_params, ) def tile( - z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), - x: int = Path(..., description="TMS tiles's column"), - y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), - scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + z: Annotated[int, Path(description="TMS tiles's zoom level")], + x: Annotated[int, Path(description="TMS tiles's column")], + y: Annotated[int, Path(description="TMS tiles's row")], + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, + scale: Annotated[ + conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..." + ] = 1, + format: Annotated[ + ImageType, + "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ] = None, src_path=Depends(self.path_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - buffer: Optional[float] = Query( - None, - 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).", - ), + 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, post_process=Depends(self.process_dependency), rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), @@ -587,40 +600,52 @@ def tilejson(self): # noqa: C901 ) def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, src_path=Depends(self.path_dependency), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + 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, + post_process=Depends(self.process_dependency), + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): @@ -669,41 +694,53 @@ def map_viewer(self): # noqa: C901 def map_viewer( request: Request, src_path=Depends(self.path_dependency), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), # noqa - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), # noqa - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), # noqa - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), # noqa - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + 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, + post_process=Depends(self.process_dependency), + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( @@ -733,40 +770,50 @@ def wmts(self): # noqa: C901 ) def wmts( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, src_path=Depends(self.path_dependency), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa + tile_format: Annotated[ + ImageType, + Query(description="Output image type. Default is png."), + ] = ImageType.png, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + 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, + post_process=Depends(self.process_dependency), + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): @@ -848,10 +895,10 @@ def point(self): responses={200: {"description": "Return a value for a point"}}, ) def point( - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), reader_params=Depends(self.reader_dependency), @@ -884,21 +931,24 @@ def preview(self): @self.router.get(r"/preview", **img_endpoint_params) @self.router.get(r"/preview.{format}", **img_endpoint_params) def preview( - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + format: Annotated[ + ImageType, + "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ] = None, src_path=Depends(self.path_dependency), layer_params=Depends(self.layer_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), + dst_crs=Depends(DstCRSParams), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), @@ -954,24 +1004,29 @@ def part(self): # noqa: C901 **img_endpoint_params, ) 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"), - format: ImageType = Query(..., description="Output image type."), + 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[ + ImageType, + "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ] = None, src_path=Depends(self.path_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), post_process=Depends(self.process_dependency), rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), @@ -1024,22 +1079,25 @@ def part( **img_endpoint_params, ) def geojson_crop( - geojson: Feature = Body(..., description="GeoJSON Feature."), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + geojson: Annotated[Feature, Body(description="GeoJSON Feature.")], + format: Annotated[ + ImageType, + "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ] = None, src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), post_process=Depends(self.process_dependency), rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), reader_params=Depends(self.reader_dependency), @@ -1268,11 +1326,12 @@ def statistics( }, ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(AssetsBidxExprParamsOptional), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), @@ -1461,11 +1520,12 @@ def statistics( }, ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + coord_crs=Depends(CoordCRSParams), bands_params=Depends(BandsExprParamsOptional), dataset_params=Depends(self.dataset_dependency), image_params=Depends(self.img_dependency), @@ -1584,9 +1644,10 @@ async def TileMatrixSet_list(request: Request): operation_id="getTileMatrixSet", ) async def TileMatrixSet_info( - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Path( - ..., description="TileMatrixSet Name." - ) + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path(description="TileMatrixSet Name."), + ] ): """ OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixset @@ -1650,9 +1711,10 @@ def available_algorithms(request: Request): operation_id="getAlgorithm", ) def algorithm_metadata( - algorithm: Literal[tuple(self.supported_algorithm.list())] = Path( - ..., description="Algorithm name", alias="algorithmId" - ), + algorithm: Annotated[ + Literal[tuple(self.supported_algorithm.list())], + Path(description="Algorithm name", alias="algorithmId"), + ], ): """Retrieve the metadata of the specified algorithm.""" return metadata(self.supported_algorithm.get(algorithm)) diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 2347d44db..4571e23cb 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -1,5 +1,6 @@ """rio-cogeo Extension.""" +import sys from dataclasses import dataclass from fastapi import Depends, Query @@ -7,6 +8,11 @@ from titiler.core.factory import BaseTilerFactory, FactoryExtension from titiler.core.resources.responses import JSONResponse +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + try: from rio_cogeo.cogeo import cog_info from rio_cogeo.models import Info @@ -32,8 +38,11 @@ def register(self, factory: BaseTilerFactory): response_class=JSONResponse, ) def validate( - src_path: str = Depends(factory.path_dependency), - strict: bool = Query(False, description="Treat warnings as errors"), + src_path=Depends(factory.path_dependency), + strict: Annotated[ + bool, + Query(description="Treat warnings as errors"), + ] = False, ): """Validate a COG""" return cog_info(src_path, strict=strict) diff --git a/src/titiler/extensions/titiler/extensions/stac.py b/src/titiler/extensions/titiler/extensions/stac.py index 6848c7a35..c2cadd657 100644 --- a/src/titiler/extensions/titiler/extensions/stac.py +++ b/src/titiler/extensions/titiler/extensions/stac.py @@ -17,6 +17,11 @@ else: from typing import TypedDict +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + try: import pystac from pystac.utils import datetime_to_str, str_to_datetime @@ -58,51 +63,67 @@ def register(self, factory: BaseTilerFactory): @factory.router.get("/stac", response_model=Item, name="Create STAC Item") def create_stac( - src_path: str = Depends(factory.path_dependency), - datetime: Optional[str] = Query( - None, - description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", - ), - extensions: Optional[List[str]] = Query( - None, description="STAC extension URL the Item implements." - ), - collection: Optional[str] = Query( - None, description="The Collection ID that this item belongs to." - ), - collection_url: Optional[str] = Query( - None, description="Link to the STAC Collection." - ), + src_path=Depends(factory.path_dependency), + datetime: Annotated[ + Optional[str], + Query( + description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", + ), + ] = None, + extensions: Annotated[ + Optional[List[str]], + Query(description="STAC extension URL the Item implements."), + ] = None, + collection: Annotated[ + Optional[str], + Query(description="The Collection ID that this item belongs to."), + ] = None, + collection_url: Annotated[ + Optional[str], + Query(description="Link to the STAC Collection."), + ] = None, # properties: Optional[Dict] = Query(None, description="Additional properties to add in the item."), - id: Optional[str] = Query( - None, - description="Id to assign to the item (default to the source basename).", - ), - asset_name: Optional[str] = Query( - "data", description="asset name for the source (default to 'data')." - ), - asset_roles: Optional[List[str]] = Query( - None, description="list of asset's roles." - ), - asset_media_type: Literal[tuple(media)] = Query( # type: ignore - "auto", description="Asset's media type" - ), - asset_href: Optional[str] = Query( - None, description="Asset's URI (default to source's path)" - ), - with_proj: bool = Query( - True, description="Add the `projection` extension and properties." - ), - with_raster: bool = Query( - True, description="Add the `raster` extension and properties." - ), - with_eo: bool = Query( - True, description="Add the `eo` extension and properties." - ), - max_size: Optional[int] = Query( - 1024, - gt=0, - description="Limit array size from which to get the raster statistics.", - ), + id: Annotated[ + Optional[str], + Query( + description="Id to assign to the item (default to the source basename)." + ), + ] = None, + asset_name: Annotated[ + Optional[str], + Query(description="asset name for the source (default to 'data')."), + ] = "data", + asset_roles: Annotated[ + Optional[List[str]], + Query(description="list of asset's roles."), + ] = None, + asset_media_type: Annotated[ # type: ignore + Optional[Literal[tuple(media)]], + Query(description="Asset's media type"), + ] = "auto", + asset_href: Annotated[ + Optional[str], + Query(description="Asset's URI (default to source's path)"), + ] = None, + with_proj: Annotated[ + Optional[bool], + Query(description="Add the `projection` extension and properties."), + ] = True, + with_raster: Annotated[ + Optional[bool], + Query(description="Add the `raster` extension and properties."), + ] = True, + with_eo: Annotated[ + Optional[bool], + Query(description="Add the `eo` extension and properties."), + ] = True, + max_size: Annotated[ + Optional[int], + Query( + gt=0, + description="Limit array size from which to get the raster statistics.", + ), + ] = 1024, ): """Create STAC item.""" properties = ( diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index 95948cec1..dd34512fa 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -1,8 +1,9 @@ """wms Extension.""" +import sys from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional from urllib.parse import urlencode import jinja2 @@ -20,6 +21,12 @@ from titiler.core.factory import BaseTilerFactory, FactoryExtension from titiler.core.resources.enums import ImageType, MediaType +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + + DEFAULT_TEMPLATES = Jinja2Templates( directory="", loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), @@ -267,12 +274,14 @@ def wms( # noqa: C901 layer_params=Depends(factory.layer_dependency), dataset_params=Depends(factory.dataset_dependency), post_process=Depends(factory.process_dependency), - 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=Depends(RescalingParams), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(factory.colormap_dependency), reader_params=Depends(factory.reader_dependency), env=Depends(factory.environment_dependency), diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 2a618266c..ca4ecdb59 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -1,6 +1,7 @@ """TiTiler.mosaic Router factories.""" import os +import sys from dataclasses import dataclass from typing import Callable, Dict, Literal, Optional, Type, Union from urllib.parse import urlencode @@ -14,6 +15,7 @@ from geojson_pydantic.geometries import Polygon from morecantile import tms from morecantile.defaults import TileMatrixSets +from pydantic import conint from rio_tiler.constants import MAX_THREADS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader from rio_tiler.models import Bounds @@ -29,15 +31,20 @@ from titiler.mosaic.models.responses import Point from titiler.mosaic.resources.enums import PixelSelectionMethod +if sys.version_info >= (3, 9): + from typing import Annotated # pylint: disable=no-name-in-module +else: + from typing_extensions import Annotated + + # BaseBackend does not support other TMS than WebMercator mosaic_tms = TileMatrixSets({"WebMercatorQuad": tms.get("WebMercatorQuad")}) def PixelSelectionParams( - pixel_selection: PixelSelectionMethod = Query( - PixelSelectionMethod.first, - description="Pixel selection method.", - ) + pixel_selection: Annotated[ + PixelSelectionMethod, Query(description="Pixel selection method.") + ] = PixelSelectionMethod.first, ) -> MosaicMethodBase: """ Returns the mosaic method used to combine datasets together. @@ -232,36 +239,41 @@ def tile(self): # noqa: C901 **img_endpoint_params, ) def tile( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa - scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + z: Annotated[int, Path(description="TMS tiles's zoom level")], + x: Annotated[int, Path(description="TMS tiles's column")], + y: Annotated[int, Path(description="TMS tiles's row")], + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, + scale: Annotated[ + conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..." + ] = 1, + format: Annotated[ + ImageType, + "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ] = None, src_path=Depends(self.path_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), pixel_selection=Depends(self.pixel_selection_dependency), - buffer: Optional[float] = Query( - None, - 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 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), + 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, post_process=Depends(self.process_dependency), rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), backend_params=Depends(self.backend_dependency), @@ -269,6 +281,12 @@ def tile( env=Depends(self.environment_dependency), ): """Create map tile from a COG.""" + if scale < 1 or scale > 4: + raise HTTPException( + 400, + f"Invalid 'scale' parameter: {scale}. Scale HAVE TO be between 1 and 4", + ) + threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) strict_zoom = str(os.getenv("MOSAIC_STRICT_ZOOM", False)).lower() in [ @@ -346,41 +364,53 @@ def tilejson(self): # noqa: C901 ) def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, src_path=Depends(self.path_dependency), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + 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, + post_process=Depends(self.process_dependency), + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), @@ -438,42 +468,54 @@ def map_viewer(self): # noqa: C901 def map_viewer( request: Request, src_path=Depends(self.path_dependency), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - backend_params=Depends(self.backend_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + pixel_selection=Depends(self.pixel_selection_dependency), + 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, + rescale=Depends(self.rescale_dependency), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( @@ -503,39 +545,49 @@ def wmts(self): # noqa: C901 ) def wmts( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa + TileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"TileMatrixSet Name (default: '{self.default_tms}')", + ] = self.default_tms, src_path=Depends(self.path_dependency), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), + tile_format: Annotated[ + ImageType, + Query(description="Output image type. Default is png."), + ] = ImageType.png, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, layer_params=Depends(self.layer_dependency), # noqa dataset_params=Depends(self.dataset_dependency), # noqa pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - 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 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), + 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, post_process=Depends(self.process_dependency), # noqa rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None, colormap=Depends(self.colormap_dependency), # noqa render_params=Depends(self.render_dependency), # noqa backend_params=Depends(self.backend_dependency), @@ -626,8 +678,8 @@ def point(self): ) def point( response: Response, - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), @@ -676,11 +728,11 @@ def assets(self): responses={200: {"description": "Return list of COGs in bounding box"}}, ) def assets_for_bbox( + 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")], src_path=Depends(self.path_dependency), - minx: float = Query(None, description="Left side of bounding box"), - miny: float = Query(None, description="Bottom of bounding box"), - maxx: float = Query(None, description="Right side of bounding box"), - maxy: float = Query(None, description="Top of bounding box"), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), @@ -696,13 +748,13 @@ def assets_for_bbox( return src_dst.assets_for_bbox(minx, miny, maxx, maxy) @self.router.get( - r"/{lng},{lat}/assets", + r"/{lon},{lat}/assets", responses={200: {"description": "Return list of COGs"}}, ) def assets_for_lon_lat( + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - lng: float = Query(None, description="Longitude"), - lat: float = Query(None, description="Latitude"), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), @@ -715,16 +767,16 @@ def assets_for_lon_lat( reader_options={**reader_params}, **backend_params, ) as src_dst: - return src_dst.assets_for_point(lng, lat) + return src_dst.assets_for_point(lon, lat) @self.router.get( r"/{z}/{x}/{y}/assets", responses={200: {"description": "Return list of COGs"}}, ) def assets_for_tile( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(description="TMS tiles's zoom level")], + x: Annotated[int, Path(description="TMS tiles's column")], + y: Annotated[int, Path(description="TMS tiles's row")], src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), From 437ef180fe523e3a7a10369155df39f4b2d4e2cb Mon Sep 17 00:00:00 2001 From: d Date: Wed, 12 Jul 2023 21:04:55 +0200 Subject: [PATCH 007/405] Fix errors in extension example docs (#665) * fixed custom extension docs * fixed example in extensions readme --------- Co-authored-by: Darell van der Voort --- docs/src/advanced/Extensions.md | 7 ++++--- src/titiler/extensions/README.md | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/src/advanced/Extensions.md b/docs/src/advanced/Extensions.md index 9af9a7c2f..903f5786f 100644 --- a/docs/src/advanced/Extensions.md +++ b/docs/src/advanced/Extensions.md @@ -89,6 +89,7 @@ See [titiler.application](../application) for a full example. from dataclasses import dataclass, field from typing import Tuple, List, Optional +import rasterio from starlette.responses import Response from fastapi import Depends, FastAPI, Query from titiler.core.factory import BaseTilerFactory, FactoryExtension, TilerFactory @@ -140,8 +141,8 @@ class thumbnailExtension(FactoryExtension): env=Depends(factory.environment_dependency), ): with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - im = src.preview( + with factory.reader(src_path, **reader_params) as src: + image = src.preview( max_size=self.max_size, **layer_params, **dataset_params, @@ -160,7 +161,7 @@ class thumbnailExtension(FactoryExtension): content = image.render( img_format=format.driver, - colormap=colormap or dst_colormap, + colormap=colormap, **format.profile, **render_params, ) diff --git a/src/titiler/extensions/README.md b/src/titiler/extensions/README.md index 4ae1309ea..17c924dd1 100644 --- a/src/titiler/extensions/README.md +++ b/src/titiler/extensions/README.md @@ -118,12 +118,12 @@ class thumbnailExtension(FactoryExtension): env=Depends(factory.environment_dependency), ): with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - im = src.preview( - max_size=self.max_size, - **layer_params, - **dataset_params, - ) + with factory.reader(src_path, **reader_params) as src: + image = src.preview( + max_size=self.max_size, + **layer_params, + **dataset_params, + ) if post_process: image = post_process(image) @@ -138,7 +138,7 @@ class thumbnailExtension(FactoryExtension): content = image.render( img_format=format.driver, - colormap=colormap or dst_colormap, + colormap=colormap, **format.profile, **render_params, ) From 9dc9d6b978053a32e8455e5e7c55388edb91070b Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 17 Jul 2023 17:00:34 +0200 Subject: [PATCH 008/405] fix expression case --- docs/src/endpoints/cog.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index a1add20b0..b33c5b960 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -43,7 +43,7 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - **resampling** (str): rasterio resampling method. Default is `nearest`. @@ -73,7 +73,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size, default is 1024. - **height** (int): Force output image height. - **width** (int): Force output image width. @@ -112,7 +112,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **max_size** (int): Max image size, default is 1024. - **nodata** (str, int, float): Overwrite internal Nodata value. @@ -148,7 +148,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - **max_size** (int): Max image size, default is 1024. - **nodata** (str, int, float): Overwrite internal Nodata value. @@ -183,7 +183,7 @@ Note: if `height` and `width` are provided `max_size` will be ignored. - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. @@ -208,7 +208,7 @@ Example: - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - **resampling** (str): rasterio resampling method. Default is `nearest`. @@ -242,7 +242,7 @@ Example: - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - **resampling** (str): rasterio resampling method. Default is `nearest`. @@ -297,7 +297,7 @@ Advanced raster statistics - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. @@ -322,7 +322,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. From 8940f290bfaf9d6ad091a7bda730791f887551ff Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Mon, 17 Jul 2023 17:24:19 +0200 Subject: [PATCH 009/405] update morecantile, rio-tiler and cogeo-mosaic versions (#664) * update morecantile and rio-tiler versions * update statistics methods * update extensions * update cogeo-mosaic * remove mercantile * add boto3 * fix mosaic deps * fix and test algo * name * update jsonschema version * more mosaic tests * update stac extension * update changelog --- CHANGES.md | 11 ++ docs/src/advanced/tiler_factories.md | 32 ++-- docs/src/endpoints/cog.md | 20 +- docs/src/endpoints/mosaic.md | 8 +- docs/src/endpoints/stac.md | 20 +- docs/src/endpoints/tms.md | 6 +- .../code/create_gdal_wmts_extension.md | 6 +- docs/src/examples/code/tiler_with_cache.md | 20 +- .../code/tiler_with_custom_algorithm.md | 1 - src/titiler/application/pyproject.toml | 1 + .../application/tests/routes/test_mosaic.py | 7 +- src/titiler/application/tests/test_main.py | 6 + src/titiler/core/pyproject.toml | 3 +- src/titiler/core/tests/test_algorithms.py | 133 ++++++++++++- src/titiler/core/tests/test_dependencies.py | 12 +- src/titiler/core/tests/test_factories.py | 23 ++- .../core/titiler/core/algorithm/dem.py | 58 +++--- .../core/titiler/core/algorithm/index.py | 14 +- src/titiler/core/titiler/core/dependencies.py | 22 +-- src/titiler/core/titiler/core/factory.py | 175 +++++++++--------- src/titiler/core/titiler/core/models/OGC.py | 12 +- .../core/titiler/core/templates/map.html | 2 +- .../core/titiler/core/templates/wmts.xml | 4 +- src/titiler/extensions/pyproject.toml | 6 +- .../extensions/titiler/extensions/cogeo.py | 2 +- .../extensions/titiler/extensions/stac.py | 18 ++ .../extensions/titiler/extensions/wms.py | 22 ++- src/titiler/mosaic/pyproject.toml | 2 +- src/titiler/mosaic/tests/test_factory.py | 85 ++++++--- src/titiler/mosaic/titiler/mosaic/factory.py | 164 ++++++++++------ .../titiler/mosaic/resources/__init__.py | 1 - .../mosaic/titiler/mosaic/resources/enums.py | 22 --- 32 files changed, 576 insertions(+), 342 deletions(-) delete mode 100644 src/titiler/mosaic/titiler/mosaic/resources/__init__.py delete mode 100644 src/titiler/mosaic/titiler/mosaic/resources/enums.py diff --git a/CHANGES.md b/CHANGES.md index dafa05b54..029353735 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,15 +3,26 @@ ## Next (TDB) * use `Annotated` Type for Query/Path parameters +* replace variable `TileMatrixSetId` by `tileMatrixSetId` ### titiler.core * update FastAPI dependency to `>=0.95.1` * set `pydantic` dependency to `~=1.0` +* update `rio-tiler` dependency to `>=5.0,<6.0` +* update TMS endpoints to match OGC Tiles specification ### titiler.extensions * use TiTiler's custom JSONResponse for the `/validate` endpoint to avoid issue when COG has `NaN` nodata value +* update `rio-cogeo` dependency to `>=4.0,<5.0` +* update `rio-stac` requirement to `>=0.8,<0.9` and add `geom-densify-pts` and `geom-precision` options + +## titiler.mosaic + +* update `cogeo-mosaic` dependency to `>=6.0,<7.0` +* remove `titiler.mosaic.resources.enum.PixelSelectionMethod` and use `rio_tiler.mosaic.methods.PixelSelectionMethod` +* allow more TileMatrixSet (than only `WebMercatorQuad`) ## 0.11.7 (2023-05-18) diff --git a/docs/src/advanced/tiler_factories.md b/docs/src/advanced/tiler_factories.md index c97dadd15..a84ef7c5a 100644 --- a/docs/src/advanced/tiler_factories.md +++ b/docs/src/advanced/tiler_factories.md @@ -20,15 +20,15 @@ app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) | `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature | `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return dataset's statistics | `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `[/{tileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `[/{tileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset | `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset (**Optional**) | `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset (**Optional**) | `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature (**Optional**) | `GET` | `/map` | HTML | return a simple map viewer -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer +| `GET` | `[/{tileMatrixSetId}]/map` | HTML | return a simple map viewer ### `titiler.core.factory.MultiBaseTilerFactory` @@ -54,14 +54,14 @@ app.include_router(cog.router, tags=["STAC"]) | `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics | `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged) | `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged) -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/[{tileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON ([Point][multipoint_model]) | return pixel values from assets | `GET` | `/preview[.{format}]` | image/bin | create a preview image from assets (**Optional**) | `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets (**Optional**) | `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets (**Optional**) -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer +| `GET` | `[/{tileMatrixSetId}]/map` | HTML | return a simple map viewer ### `titiler.core.factory.MultiBandTilerFactory` @@ -92,14 +92,14 @@ app.include_router(cog.router, tags=["Landsat"]) | `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return basic info for a dataset as a GeoJSON feature | `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return info and statistics for a dataset | `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return info and statistics for a dataset -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/[{tileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel value from a dataset | `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset | `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset | `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer +| `GET` | `[/{tileMatrixSetId}]/map` | HTML | return a simple map viewer ### `titiler.mosaic.factory.MosaicTilerFactory` @@ -110,14 +110,14 @@ app.include_router(cog.router, tags=["Landsat"]) | `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return mosaic's bounds | `GET` | `/info` | JSON ([Info][mosaic_info_model]) | return mosaic's basic info | `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][mosaic_geojson_info_model]) | return mosaic's basic info as a GeoJSON feature -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON +| `GET` | `[/{tileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `[/{tileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON ([Point][mosaic_point]) | return pixel value from a MosaicJSON dataset | `GET` | `/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile | `GET` | `/{lon},{lat}/assets` | JSON | return list of assets intersecting a point | `GET` | `/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer +| `GET` | `[/{tileMatrixSetId}]/map` | HTML | return a simple map viewer !!! Important diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index b33c5b960..a46133403 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -14,14 +14,14 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog | `GET` | `/cog/info.geojson` | GeoJSON | return dataset's basic info as a GeoJSON feature | `GET` | `/cog/statistics` | JSON | return dataset's statistics | `POST` | `/cog/statistics` | GeoJSON | return dataset's statistics for a GeoJSON -| `GET` | `/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/cog[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/cog[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/cog/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/cog[/{tileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/cog[/{tileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel values from a dataset | `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset | `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset | `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a GeoJSON feature -| `GET` | `/cog[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/cog[/{tileMatrixSetId}]/map` | HTML | simple map viewer | `GET` | `/cog/validate` | JSON | validate a COG and return dataset info (from `titiler.extensions.cogValidateExtension`) | `GET` | `/cog/viewer` | HTML | demo webpage (from `titiler.extensions.cogViewerExtension`) | `GET` | `/cog/stac` | GeoJSON | create STAC Items from a dataset (from `titiler.extensions.stacExtension`) @@ -30,10 +30,10 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog ### Tiles -`:endpoint:/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/cog/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. @@ -196,10 +196,10 @@ Example: ### TilesJSON -`:endpoint:/cog[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/cog[/{tileMatrixSetId}]/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** @@ -230,10 +230,10 @@ Example: ### Map -`:endpoint:/cog[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/cog[/{tileMatrixSetId}]/map` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** diff --git a/docs/src/endpoints/mosaic.md b/docs/src/endpoints/mosaic.md index 47537129c..b1c46b990 100644 --- a/docs/src/endpoints/mosaic.md +++ b/docs/src/endpoints/mosaic.md @@ -13,14 +13,14 @@ Read Mosaic Info/Metadata and create Web map Tiles from a multiple COG. The `mos | `GET` | `/mosaicjson/bounds` | JSON | return mosaic's bounds | `GET` | `/mosaicjson/info` | JSON | return mosaic's basic info | `GET` | `/mosaicjson/info.geojson` | GeoJSON | return mosaic's basic info as a GeoJSON feature -| `GET` | `/mosaicjson/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/mosaicjson/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets +| `GET` | `/mosaicjson[/{tileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/mosaicjson[/{tileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/mosaicjson/point/{lon},{lat}` | JSON | return pixel value from a mosaic assets | `GET` | `/mosaicjson/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile | `GET` | `/mosaicjson/{lon},{lat}/assets` | JSON | return list of assets intersecting a point | `GET` | `/mosaicjson/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/mosaicjson[/{tileMatrixSetId}]/map` | HTML | simple map viewer ## Description diff --git a/docs/src/endpoints/stac.md b/docs/src/endpoints/stac.md index b70a9700d..cb108bcb0 100644 --- a/docs/src/endpoints/stac.md +++ b/docs/src/endpoints/stac.md @@ -16,24 +16,24 @@ The `/stac` routes are based on `titiler.core.factory.MultiBaseTilerFactory` but | `GET` | `/stac/asset_statistics` | JSON | return per asset statistics | `GET` | `/stac/statistics` | JSON | return asset's statistics | `POST` | `/stac/statistics` | GeoJSON | return asset's statistics for a GeoJSON -| `GET` | `/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/stac[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/stac[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/stac/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/stac[/{tileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/stac[/{tileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from assets | `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from assets | `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets | `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering the assets -| `GET` | `/stac[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/stac[/{tileMatrixSetId}]/map` | HTML | simple map viewer | `GET` | `/stac/viewer` | HTML | demo webpage (from `titiler.extensions.stacViewerExtension`) ## Description ### Tiles -`:endpoint:/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/stac/tiles[/{tileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. @@ -214,10 +214,10 @@ Example: ### TilesJSON -`:endpoint:/stac[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/stac[/{tileMatrixSetId}]/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. + - **tileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -252,10 +252,10 @@ Example: ### Map -`:endpoint:/stac[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/stac[/{tileMatrixSetId}]/map` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** - QueryParams: - **url** (str): STAC Item URL. **Required** diff --git a/docs/src/endpoints/tms.md b/docs/src/endpoints/tms.md index 41cd57516..aa01b3c01 100644 --- a/docs/src/endpoints/tms.md +++ b/docs/src/endpoints/tms.md @@ -18,7 +18,7 @@ app.include_router(tms.router, tags=["TileMatrixSets"]) | Method | URL | Output | Description | ------ | ----------------------------------- |---------- |-------------- | `GET` | `/tileMatrixSets` | JSON | return the list of supported TileMatrixSet -| `GET` | `/tileMatrixSets/{TileMatrixSetId}` | JSON | return the TileMatrixSet JSON document +| `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON | return the TileMatrixSet JSON document ## Description @@ -50,10 +50,10 @@ $ curl https://myendpoint/tileMatrixSets | jq ### Get TMS info -`:endpoint:/tileMatrixSets/{TileMatrixSetId}` - Get the TileMatrixSet JSON document +`:endpoint:/tileMatrixSets/{tileMatrixSetId}` - Get the TileMatrixSet JSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name + - **tileMatrixSetId**: TileMatrixSet name ```bash $ curl http://127.0.0.1:8000/tileMatrixSets/WebMercatorQuad | jq diff --git a/docs/src/examples/code/create_gdal_wmts_extension.md b/docs/src/examples/code/create_gdal_wmts_extension.md index 4a687c84d..6ee5d1960 100644 --- a/docs/src/examples/code/create_gdal_wmts_extension.md +++ b/docs/src/examples/code/create_gdal_wmts_extension.md @@ -41,7 +41,7 @@ class gdalwmtsExtension(FactoryExtension): }, ) @factory.router.get( - "/{TileMatrixSetId}/wmts.xml", + "/{tileMatrixSetId}/wmts.xml", response_class=XMLResponse, responses={ 200: { @@ -54,7 +54,7 @@ class gdalwmtsExtension(FactoryExtension): ) def gdal_wmts( request: Request, - TileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Query( # type: ignore + tileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Query( # type: ignore factory.default_tms, description=f"TileMatrixSet Name (default: '{factory.default_tms}')", ), @@ -74,7 +74,7 @@ class gdalwmtsExtension(FactoryExtension): ): """Return a GDAL WMTS Service description.""" route_params = { - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } wmts_url = factory.url_for(request, "wmts", **route_params) diff --git a/docs/src/examples/code/tiler_with_cache.md b/docs/src/examples/code/tiler_with_cache.md index 880ed6875..b53c2b483 100644 --- a/docs/src/examples/code/tiler_with_cache.md +++ b/docs/src/examples/code/tiler_with_cache.md @@ -186,15 +186,15 @@ class TilerFactory(TiTilerFactory): @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params, ) # Add default cache config dictionary into cached alias. @@ -204,7 +204,7 @@ class TilerFactory(TiTilerFactory): z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), x: int = Path(..., description="TMS tiles's column"), y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -235,7 +235,7 @@ class TilerFactory(TiTilerFactory): reader_params=Depends(self.reader_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: image = src_dst.tile( @@ -280,7 +280,7 @@ class TilerFactory(TiTilerFactory): response_model_exclude_none=True, ) @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, @@ -288,7 +288,7 @@ class TilerFactory(TiTilerFactory): @cached(alias="default") def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -332,7 +332,7 @@ class TilerFactory(TiTilerFactory): "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -354,7 +354,7 @@ class TilerFactory(TiTilerFactory): if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: return { "bounds": src_dst.geographic_bounds, diff --git a/docs/src/examples/code/tiler_with_custom_algorithm.md b/docs/src/examples/code/tiler_with_custom_algorithm.md index e606982e6..547d5ffe5 100644 --- a/docs/src/examples/code/tiler_with_custom_algorithm.md +++ b/docs/src/examples/code/tiler_with_custom_algorithm.md @@ -32,7 +32,6 @@ class Multiply(BaseAlgorithm): # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index f88aa0750..7ae969661 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -43,6 +43,7 @@ test = [ "pytest-asyncio", "httpx", "brotlipy", + "boto3", ] server = [ "uvicorn[standard]>=0.12.0,<0.19.0", diff --git a/src/titiler/application/tests/routes/test_mosaic.py b/src/titiler/application/tests/routes/test_mosaic.py index 3e092329d..353916733 100644 --- a/src/titiler/application/tests/routes/test_mosaic.py +++ b/src/titiler/application/tests/routes/test_mosaic.py @@ -4,7 +4,7 @@ from typing import Any, Callable from unittest.mock import patch -import mercantile +import morecantile from cogeo_mosaic.backends import FileBackend from cogeo_mosaic.mosaic import MosaicJSON @@ -111,8 +111,9 @@ def test_tile(app): """Test GET /mosaicjson/tiles endpoint""" mosaicjson = read_json_fixture(MOSAICJSON_FILE) bounds = mosaicjson["bounds"] - tile = mercantile.tile(*mosaicjson["center"]) - partial_tile = mercantile.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) + tms = morecantile.tms.get("WebMercatorQuad") + tile = tms.tile(*mosaicjson["center"]) + partial_tile = tms.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) with patch.object(FileBackend, "_read", mosaic_read_factory(MOSAICJSON_FILE)): # full tile diff --git a/src/titiler/application/tests/test_main.py b/src/titiler/application/tests/test_main.py index 81ddba727..40a46818d 100644 --- a/src/titiler/application/tests/test_main.py +++ b/src/titiler/application/tests/test_main.py @@ -6,3 +6,9 @@ def test_health(app): response = app.get("/healthz") assert response.status_code == 200 assert response.json() == {"ping": "pong!"} + + response = app.get("/openapi.json") + assert response.status_code == 200 + + response = app.get("/docs") + assert response.status_code == 200 diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index 1c6fbcbe4..1a91204ab 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -35,7 +35,8 @@ dependencies = [ "numpy", "pydantic~=1.0", "rasterio", - "rio-tiler>=4.1.6,<4.2", + "rio-tiler>=5.0,<6.0", + "morecantile>=4.3,<5.0", "simplejson", "typing_extensions;python_version<'3.8'", ] diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index ca4e95a59..eee407aa8 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -22,12 +22,11 @@ class Multiply(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Apply Multiplication factor.""" # Multiply image data bcy factor - data = img.data * self.factor + data = img.array * self.factor # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -108,3 +107,133 @@ def main(algorithm=Depends(default_algorithms.dependency)): # https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) + + +def test_normalized_index(): + """test ndi.""" + algo = default_algorithms.get("normalizedIndex")() + + arr = numpy.zeros((2, 256, 256), dtype="uint16") + arr[0, :, :] = 1 + arr[1, :, :] = 2 + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 1 + numpy.testing.assert_almost_equal(out.array[0, 0, 0], 0.3333, decimal=3) + + # with mixed 0 and masked + arr = numpy.ma.MaskedArray( + numpy.zeros((2, 256, 256), dtype="uint16"), + mask=numpy.zeros((2, 256, 256), dtype="bool"), + ) + arr.data[0, :, :] = 1 + arr.data[0, 0:10, 0:10] = 0 + arr.mask[0, 0:5, 0:5] = True + + arr.data[1, :, :] = 2 + arr.data[1, 0:10, 0:10] = 0 + arr.mask[1, 0:5, 0:5] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 2 # 0.33 and None + assert out.array[0, 0, 0] is numpy.ma.masked + assert out.array[0, 6, 6] is numpy.ma.masked + numpy.testing.assert_almost_equal(out.array[0, 10, 10], 0.3333, decimal=3) + + +def test_hillshade(): + """test hillshade.""" + algo = default_algorithms.get("hillshade")() + + arr = numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16"), + mask=numpy.zeros((1, 262, 262), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_contours(): + """test contours.""" + algo = default_algorithms.get("contours")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_terrarium(): + """test terrarium.""" + algo = default_algorithms.get("terrarium")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_terrainrgb(): + """test terrainrgb.""" + algo = default_algorithms.get("terrainrgb")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked diff --git a/src/titiler/core/tests/test_dependencies.py b/src/titiler/core/tests/test_dependencies.py index 2e28ac9b3..c269384e2 100644 --- a/src/titiler/core/tests/test_dependencies.py +++ b/src/titiler/core/tests/test_dependencies.py @@ -24,20 +24,20 @@ def test_tms(): """Create App.""" app = FastAPI() - @app.get("/web/{TileMatrixSetId}") + @app.get("/web/{tileMatrixSetId}") def web( - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal["WebMercatorQuad"], Path(), ], ): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId - @app.get("/all/{TileMatrixSetId}") - def all(TileMatrixSetId: Annotated[Literal[tuple(tms.list())], Path()]): + @app.get("/all/{tileMatrixSetId}") + def all(tileMatrixSetId: Annotated[Literal[tuple(tms.list())], Path()]): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId client = TestClient(app) response = client.get("/web/WebMercatorQuad") diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 408cf96a9..5510d7348 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -53,6 +53,12 @@ def test_TilerFactory(): app.include_router(cog.router, prefix="/something") client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" @@ -219,7 +225,7 @@ def test_TilerFactory(): assert response.json()["tilejson"] response_qs = client.get( - f"/tilejson.json?url={DATA_DIR}/cog.tif&TileMatrixSetId=WorldCRS84Quad" + f"/tilejson.json?url={DATA_DIR}/cog.tif&tileMatrixSetId=WorldCRS84Quad" ) assert response.json()["tiles"] == response_qs.json()["tiles"] @@ -657,6 +663,12 @@ def test_MultiBaseTilerFactory(rio): client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + response = client.get(f"/assets?url={DATA_DIR}/item.json") assert response.status_code == 200 assert len(response.json()) == 2 @@ -981,6 +993,12 @@ def test_MultiBandTilerFactory(): client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + response = client.get(f"/bands?directory={DATA_DIR}") assert response.status_code == 200 assert response.json() == ["B01", "B09"] @@ -1267,8 +1285,7 @@ def test_TMSFactory(): response = client.get("/tms/tileMatrixSets/WebMercatorQuad") assert response.status_code == 200 body = response.json() - assert body["type"] == "TileMatrixSetType" - assert body["identifier"] == "WebMercatorQuad" + assert body["id"] == "WebMercatorQuad" response = client.get("/tms/tileMatrixSets/WebMercatorQua") assert response.status_code == 422 diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 858d538d2..ae9072efa 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -24,12 +24,7 @@ class HillShade(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Create hillshade from DEM dataset.""" - data = img.data[0] - mask = img.mask - bounds = img.bounds - - x, y = numpy.gradient(data) - + x, y = numpy.gradient(img.array[0]) slope = numpy.pi / 2.0 - numpy.arctan(numpy.sqrt(x * x + y * y)) aspect = numpy.arctan2(-x, y) azimuthrad = self.azimuth * numpy.pi / 180.0 @@ -37,28 +32,26 @@ def __call__(self, img: ImageData) -> ImageData: shaded = numpy.sin(altituderad) * numpy.sin(slope) + numpy.cos( altituderad ) * numpy.cos(slope) * numpy.cos(azimuthrad - aspect) - hillshade_array = 255 * (shaded + 1) / 2 - - data = numpy.expand_dims(hillshade_array, axis=0).astype(dtype=numpy.uint8) + data = 255 * (shaded + 1) / 2 + bounds = img.bounds if self.buffer: - data = data[:, self.buffer : -self.buffer, self.buffer : -self.buffer] - mask = mask[self.buffer : -self.buffer, self.buffer : -self.buffer] - # image bounds without buffer + data = data[self.buffer : -self.buffer, self.buffer : -self.buffer] + window = windows.Window( col_off=self.buffer, row_off=self.buffer, - width=mask.shape[1], - height=mask.shape[0], + width=data.shape[1], + height=data.shape[0], ) bounds = windows.bounds(window, img.transform) return ImageData( - data, - mask, + data.astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=bounds, + band_names=["hillshade"], ) @@ -84,15 +77,19 @@ def __call__(self, img: ImageData) -> ImageData: data = img.data # Apply rescaling for minz,maxz to 1->255 and apply Terrain colormap - arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype("uint8") + arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype( + self.output_dtype + ) arr, _ = apply_cmap(arr, cmap.get("terrain")) # set black (0) for contour lines arr = numpy.where(data % self.increment < self.thickness, 0, arr) + data = numpy.ma.MaskedArray(arr) + data.mask = ~img.mask + return ImageData( - arr, - img.mask, + data, assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -109,15 +106,13 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" - data = numpy.clip(img.data[0] + 32768.0, 0.0, 65535.0) + data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) r = data / 256 g = data % 256 b = (data * 256) % 256 - arr = numpy.stack([r, g, b]).astype(numpy.uint8) return ImageData( - arr, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -153,27 +148,22 @@ def _range_check(datarange): round_digits = 0 - data = img.data[0].astype(numpy.float64) + data = img.array[0].astype(numpy.float64) data -= self.baseval data /= self.interval data = numpy.around(data / 2**round_digits) * 2**round_digits - rows, cols = data.shape datarange = data.max() - data.min() if _range_check(datarange): - raise ValueError("Data of {} larger than 256 ** 3".format(datarange)) + raise ValueError(f"Data of {datarange} larger than 256 ** 3") - rgb = numpy.zeros((3, rows, cols), dtype=numpy.uint8) - rgb[2] = ((data / 256) - (data // 256)) * 256 - rgb[1] = (((data // 256) / 256) - ((data // 256) // 256)) * 256 - rgb[0] = ( - (((data // 256) // 256) / 256) - (((data // 256) // 256) // 256) - ) * 256 + r = ((((data // 256) // 256) / 256) - (((data // 256) // 256) // 256)) * 256 + g = (((data // 256) / 256) - ((data // 256) // 256)) * 256 + b = ((data / 256) - (data // 256)) * 256 return ImageData( - rgb, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, diff --git a/src/titiler/core/titiler/core/algorithm/index.py b/src/titiler/core/titiler/core/algorithm/index.py index 1e28e0f45..2d47c4a71 100644 --- a/src/titiler/core/titiler/core/algorithm/index.py +++ b/src/titiler/core/titiler/core/algorithm/index.py @@ -20,18 +20,14 @@ class NormalizedIndex(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Normalized difference.""" - b1 = img.data[0].astype("float32") - b2 = img.data[1].astype("float32") - - arr = numpy.where(img.mask, (b2 - b1) / (b2 + b1), 0) - - # ImageData only accept image in form of (count, height, width) - arr = numpy.expand_dims(arr, axis=0).astype(self.output_dtype) - + b1 = img.array[0].astype("float32") + b2 = img.array[1].astype("float32") + arr = numpy.ma.MaskedArray((b2 - b1) / (b2 + b1), dtype=self.output_dtype) + bnames = img.band_names return ImageData( arr, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, + band_names=[f"({bnames[1]} - {bnames[0]}) / ({bnames[1]} + {bnames[0]})"], ) diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index 9a06aeaa2..220189a01 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -9,10 +9,9 @@ import numpy from fastapi import HTTPException, Query from rasterio.crs import CRS -from rasterio.enums import Resampling from rio_tiler.colormap import cmap, parse_color from rio_tiler.errors import MissingAssets, MissingBands -from rio_tiler.types import ColorMapType +from rio_tiler.types import ColorMapType, RIOResampling if sys.version_info >= (3, 9): from typing import Annotated # pylint: disable=no-name-in-module @@ -23,9 +22,6 @@ ColorMapName = Enum( # type: ignore "ColorMapName", [(a, a) for a in sorted(cmap.list())] ) -ResamplingName = Enum( # type: ignore - "ResamplingName", [(r.name, r.name) for r in Resampling] -) def ColorMapParams( @@ -349,18 +345,18 @@ class DatasetParams(DefaultDependency): ), ] = False resampling_method: Annotated[ - ResamplingName, + RIOResampling, Query( alias="resampling", description="Resampling method.", ), - ] = ResamplingName.nearest # type: ignore + ] = "nearest" def __post_init__(self): """Post Init.""" if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) - self.resampling_method = self.resampling_method.value # type: ignore + self.resampling_method = self.resampling_method @dataclass @@ -385,7 +381,7 @@ def RescalingParams( Query( title="Min/Max data Rescaling", description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", - example=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + examples=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 ), ] = None, ) -> Optional[RescaleType]: @@ -410,7 +406,7 @@ class StatisticsParams(DefaultDependency): alias="c", title="Pixels values for categories.", description="List of values for which to report counts.", - example=[1, 2, 3], + examples=[1, 2, 3], ), ] = None percentiles: Annotated[ @@ -418,8 +414,8 @@ class StatisticsParams(DefaultDependency): Query( alias="p", title="Percentile values", - description="List of percentile values.", - example=[2, 5, 95, 98], + description="List of percentile values (default to [2, 98]).", + examples=[2, 5, 95, 98], ), ] = None @@ -473,7 +469,7 @@ class HistogramParams(DefaultDependency): link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html """, - example="0,1000", + examples="0,1000", ), ] = None diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 469be0609..d18118a16 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -19,9 +19,8 @@ from pydantic import conint from rio_tiler.constants import WGS84_CRS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader -from rio_tiler.models import BandStatistics, Bounds, Info +from rio_tiler.models import Bounds, Info from rio_tiler.types import ColorMapType -from rio_tiler.utils import get_array_statistics from starlette.requests import Request from starlette.responses import HTMLResponse, Response from starlette.routing import Match, compile_path, replace_params @@ -464,23 +463,13 @@ def geojson_statistics( **image_params, **dataset_params, ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + + stats = data.statistics( + **stats_params, hist_options={**histogram_params} ) feature.properties = feature.properties or {} - feature.properties.update( - { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } - } - ) + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc @@ -494,24 +483,39 @@ def tile(self): # noqa: C901 @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params, ) def tile( - z: Annotated[int, Path(description="TMS tiles's zoom level")], - x: Annotated[int, Path(description="TMS tiles's column")], - y: Annotated[int, Path(description="TMS tiles's row")], - TileMatrixSetId: Annotated[ + 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.", + ), + ], + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, scale: Annotated[ conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..." @@ -546,7 +550,7 @@ def tile( env=Depends(self.environment_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader(src_path, tms=tms, **reader_params) as src_dst: image = src_dst.tile( @@ -593,16 +597,16 @@ def tilejson(self): # noqa: C901 response_model_exclude_none=True, ) @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, ) def tilejson( request: Request, - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, src_path=Depends(self.path_dependency), tile_format: Annotated[ @@ -655,7 +659,7 @@ def tilejson( "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -676,7 +680,7 @@ def tilejson( if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader(src_path, tms=tms, **reader_params) as src_dst: return { @@ -690,13 +694,13 @@ def map_viewer(self): # noqa: C901 """Register /map endpoint.""" @self.router.get("/map", response_class=HTMLResponse) - @self.router.get("/{TileMatrixSetId}/map", response_class=HTMLResponse) + @self.router.get("/{tileMatrixSetId}/map", response_class=HTMLResponse) def map_viewer( request: Request, src_path=Depends(self.path_dependency), - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, tile_format: Annotated[ Optional[ImageType], @@ -744,12 +748,12 @@ def map_viewer( ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( - request, "tilejson", TileMatrixSetId=TileMatrixSetId + request, "tilejson", tileMatrixSetId=tileMatrixSetId ) if request.query_params._list: tilejson_url += f"?{urlencode(request.query_params._list)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) return self.templates.TemplateResponse( name="map.html", context={ @@ -766,13 +770,13 @@ def wmts(self): # noqa: C901 @self.router.get("/WMTSCapabilities.xml", response_class=XMLResponse) @self.router.get( - "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse + "/{tileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse ) def wmts( request: Request, - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, src_path=Depends(self.path_dependency), tile_format: Annotated[ @@ -824,7 +828,7 @@ def wmts( "y": "{TileRow}", "scale": tile_scale, "format": tile_format.value, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } tiles_url = self.url_for(request, "tile", **route_params) @@ -845,7 +849,7 @@ def wmts( if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader(src_path, tms=tms, **reader_params) as src_dst: bounds = src_dst.geographic_bounds @@ -857,9 +861,9 @@ def wmts( matrix = tms.matrix(zoom) tm = f""" - {matrix.identifier} + {matrix.id} {matrix.scaleDenominator} - {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} + {matrix.pointOfOrigin[0]} {matrix.pointOfOrigin[1]} {matrix.tileWidth} {matrix.tileHeight} {matrix.matrixWidth} @@ -1360,23 +1364,14 @@ def geojson_statistics( **dataset_params, ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + stats = data.statistics( + **stats_params, hist_options={**histogram_params} ) feature.properties = feature.properties or {} - feature.properties.update( - { - # NOTE: because we use `src_dst.feature` the statistics will be in form of - # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` - "statistics": { - f"{data.band_names[ix]}": BandStatistics(**stats[ix]) - for ix in range(len(stats)) - } - } - ) + # NOTE: because we use `src_dst.feature` the statistics will be in form of + # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc @@ -1553,23 +1548,12 @@ def geojson_statistics( **image_params, **dataset_params, ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + stats = data.statistics( + **stats_params, hist_options={**histogram_params} ) feature.properties = feature.properties or {} - feature.properties.update( - { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } - } - ) + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc @@ -1610,49 +1594,66 @@ def register_routes(self): response_model_exclude_none=True, summary="Retrieve the list of available tiling schemes (tile matrix sets).", operation_id="getTileMatrixSetsList", + responses={ + 200: { + "content": { + MediaType.json.value: {}, + }, + }, + }, ) - async def TileMatrixSet_list(request: Request): + async def tilematrixsets(request: Request): """ OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets """ - return { - "tileMatrixSets": [ + data = TileMatrixSetList( + tileMatrixSets=[ { - "id": tms, - "title": tms, + "id": tms_id, "links": [ { "href": self.url_for( request, - "TileMatrixSet_info", - TileMatrixSetId=tms, + "tilematrixset", + tileMatrixSetId=tms_id, ), - "rel": "item", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", "type": "application/json", + "title": f"Definition of {tms_id} tileMatrixSet", } ], } - for tms in self.supported_tms.list() + for tms_id in self.supported_tms.list() ] - } + ) + + return data @self.router.get( - r"/tileMatrixSets/{TileMatrixSetId}", + "/tileMatrixSets/{tileMatrixSetId}", response_model=TileMatrixSet, response_model_exclude_none=True, summary="Retrieve the definition of the specified tiling scheme (tile matrix set).", operation_id="getTileMatrixSet", + responses={ + 200: { + "content": { + MediaType.json.value: {}, + }, + }, + }, ) - async def TileMatrixSet_info( - TileMatrixSetId: Annotated[ + async def tilematrixset( + request: Request, + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - Path(description="TileMatrixSet Name."), - ] + Path(description="Identifier for a supported TileMatrixSet."), + ], ): """ OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixset """ - return self.supported_tms.get(TileMatrixSetId) + return self.supported_tms.get(tileMatrixSetId) @dataclass diff --git a/src/titiler/core/titiler/core/models/OGC.py b/src/titiler/core/titiler/core/models/OGC.py index 6b735dff2..c1040833e 100644 --- a/src/titiler/core/titiler/core/models/OGC.py +++ b/src/titiler/core/titiler/core/models/OGC.py @@ -1,7 +1,7 @@ """OGC models.""" -from typing import List +from typing import List, Optional from pydantic import AnyHttpUrl, BaseModel @@ -11,29 +11,22 @@ class TileMatrixSetLink(BaseModel): TileMatrixSetLink model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ href: AnyHttpUrl rel: str = "item" type: str = "application/json" - class Config: - """Config for model.""" - - use_enum_values = True - class TileMatrixSetRef(BaseModel): """ TileMatrixSetRef model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ id: str - title: str + title: Optional[str] links: List[TileMatrixSetLink] @@ -42,7 +35,6 @@ class TileMatrixSetList(BaseModel): TileMatrixSetList model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ tileMatrixSets: List[TileMatrixSetRef] diff --git a/src/titiler/core/titiler/core/templates/map.html b/src/titiler/core/titiler/core/templates/map.html index 700b2dae8..b5781fa99 100644 --- a/src/titiler/core/titiler/core/templates/map.html +++ b/src/titiler/core/titiler/core/templates/map.html @@ -61,7 +61,7 @@ const auckland = L.marker([-36.864664, 174.792059]).addTo(map); const seattle = L.marker([47.596842, -122.333087]).addTo(map); -if ("{{ tms.identifier }}" === "WebMercatorQuad") { +if ("{{ tms.id }}" === "WebMercatorQuad") { L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); diff --git a/src/titiler/core/titiler/core/templates/wmts.xml b/src/titiler/core/titiler/core/templates/wmts.xml index a42f54095..8305851ce 100644 --- a/src/titiler/core/titiler/core/templates/wmts.xml +++ b/src/titiler/core/titiler/core/templates/wmts.xml @@ -46,12 +46,12 @@ {{ media_type }} - {{ tms.identifier }} + {{ tms.id }} - {{ tms.identifier }} + {{ tms.id }} {{ tms.crs.srs }} {% for item in tileMatrix %} {{ item | safe }} diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 3c3972416..4a66a4f1a 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -38,13 +38,13 @@ test = [ "pytest-cov", "pytest-asyncio", "httpx", - "jsonschema>=3.0", + "jsonschema>=3.0,<4.18.0", ] cogeo = [ - "rio-cogeo>=3.1,<4.0", + "rio-cogeo>=4.0,<5.0", ] stac = [ - "rio-stac>=0.6,<0.7", + "rio-stac>=0.8,<0.9", ] [project.urls] diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 4571e23cb..87c56e347 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -30,7 +30,7 @@ def register(self, factory: BaseTilerFactory): assert ( cog_info is not None - ), "'rio_cogeo' must be installed to use CogValidateExtension" + ), "'rio-cogeo' must be installed to use CogValidateExtension" @factory.router.get( "/validate", diff --git a/src/titiler/extensions/titiler/extensions/stac.py b/src/titiler/extensions/titiler/extensions/stac.py index c2cadd657..eebeb7e0b 100644 --- a/src/titiler/extensions/titiler/extensions/stac.py +++ b/src/titiler/extensions/titiler/extensions/stac.py @@ -124,6 +124,22 @@ def create_stac( description="Limit array size from which to get the raster statistics.", ), ] = 1024, + geom_densify_pts: Annotated[ + Optional[int], + Query( + alias="geom-densify-pts", + ge=0, + description="Number of points to add to each edge to account for nonlinear edges transformation.", + ), + ] = 0, + geom_precision: Annotated[ + Optional[int], + Query( + alias="geom-precision", + ge=-1, + description="Round geometry coordinates to this number of decimal.", + ), + ] = -1, ): """Create STAC item.""" properties = ( @@ -159,4 +175,6 @@ def create_stac( with_raster=with_raster, with_eo=with_eo, raster_max_size=max_size, + geom_densify_pts=geom_densify_pts, + geom_precision=geom_precision, ).to_dict() diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index dd34512fa..ce12ad483 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -44,19 +44,21 @@ class WMSMediaType(str, Enum): webp = "image/webp" +@dataclass class OverlayMethod(MosaicMethodBase): """Overlay data on top.""" - def feed(self, tile): - """Add data to tile.""" - if self.tile is None: - self.tile = tile + def feed(self, array: numpy.ma.MaskedArray): + """Add data to the mosaic array.""" + if self.mosaic is None: # type: ignore + self.mosaic = array - pidex = self.tile.mask & ~tile.mask + else: + pidex = self.mosaic.mask & ~array.mask - mask = numpy.where(pidex, tile.mask, self.tile.mask) - self.tile = numpy.ma.where(pidex, tile, self.tile) - self.tile.mask = mask + mask = numpy.where(pidex, array.mask, self.mosaic.mask) + self.mosaic = numpy.ma.where(pidex, array, self.mosaic) + self.mosaic.mask = mask @dataclass @@ -526,9 +528,11 @@ def _reader(src_path: str): if color_formula: image.apply_color_formula(color_formula) + if colormap: + image = image.apply_colormap(colormap) + content = image.render( img_format=format.driver, - colormap=colormap, add_mask=transparent, **format.profile, ) diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 5bcb697c3..34d0aaabd 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ "titiler.core==0.11.7", - "cogeo-mosaic>=5.0,<5.2", + "cogeo-mosaic>=6.2,<7.0", ] [project.optional-dependencies] diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index abd514f0b..7056fb6b0 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -5,19 +5,17 @@ from contextlib import contextmanager from dataclasses import dataclass from io import BytesIO -from typing import Optional -import attr import numpy from cogeo_mosaic.backends import FileBackend from cogeo_mosaic.mosaic import MosaicJSON from fastapi import FastAPI +from rio_tiler.mosaic.methods import PixelSelectionMethod from starlette.testclient import TestClient from titiler.core.dependencies import DefaultDependency from titiler.core.resources.enums import OptionalHeader from titiler.mosaic.factory import MosaicTilerFactory -from titiler.mosaic.resources.enums import PixelSelectionMethod from .conftest import DATA_DIR @@ -45,12 +43,18 @@ def test_MosaicTilerFactory(): optional_headers=[OptionalHeader.x_assets], router_prefix="mosaic", ) - assert len(mosaic.router.routes) == 23 + assert len(mosaic.router.routes) == 24 app = FastAPI() app.include_router(mosaic.router, prefix="/mosaic") client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + with tmpmosaic() as mosaic_file: response = client.get( "/mosaic/", @@ -94,10 +98,28 @@ def test_MosaicTilerFactory(): ) assert response.status_code == 200 + response = client.get( + "/mosaic/point/-7903683.846322423,5780349.220256353", + params={"url": mosaic_file, "coord-crs": "epsg:3857"}, + ) + assert response.status_code == 200 + response = client.get("/mosaic/tiles/7/37/45", params={"url": mosaic_file}) assert response.status_code == 200 assert response.headers["X-Assets"] + response = client.get( + "/mosaic/tiles/WebMercatorQuad/7/37/45", params={"url": mosaic_file} + ) + assert response.status_code == 200 + assert response.headers["X-Assets"] + + response = client.get( + "/mosaic/tiles/WGS1984Quad/8/148/61", params={"url": mosaic_file} + ) + assert response.status_code == 200 + assert response.headers["X-Assets"] + # Buffer response = client.get( "/mosaic/tiles/7/37/45.npy", params={"url": mosaic_file, "buffer": 10} @@ -132,7 +154,7 @@ def test_MosaicTilerFactory(): "tile_format": "png", "minzoom": 6, "maxzoom": 9, - "TileMatrixSetId": "WebMercatorQuad", + "tileMatrixSetId": "WebMercatorQuad", }, ) assert response.status_code == 200 @@ -143,7 +165,7 @@ def test_MosaicTilerFactory(): ) assert body["minzoom"] == 6 assert body["maxzoom"] == 9 - assert "TileMatrixSetId" not in body["tiles"][0] + assert "tileMatrixSetId" not in body["tiles"][0] response = client.get( "/mosaic/WMTSCapabilities.xml", @@ -172,6 +194,16 @@ def test_MosaicTilerFactory(): filepath.split("/")[-1] in ["cog1.tif"] for filepath in response.json() ) + response = client.get( + "/mosaic/WGS1984Quad/8/148/61/assets", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + response = client.get("/mosaic/-71,46/assets", params={"url": mosaic_file}) assert response.status_code == 200 assert all( @@ -179,6 +211,16 @@ def test_MosaicTilerFactory(): for filepath in response.json() ) + response = client.get( + "/mosaic/-7903683.846322423,5780349.220256353/assets", + params={"url": mosaic_file, "coord-crs": "epsg:3857"}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + response = client.get( "/mosaic/-75.9375,43.06888777416962,-73.125,45.089035564831015/assets", params={"url": mosaic_file}, @@ -189,6 +231,16 @@ def test_MosaicTilerFactory(): for filepath in response.json() ) + response = client.get( + "/mosaic/-8453323.83211421,5322463.153553393,-8140237.76425813,5635549.221409473/assets", + params={"url": mosaic_file, "coord-crs": "epsg:3857"}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + response = client.get( "/mosaic/10,10,11,11/assets", params={"url": mosaic_file}, @@ -205,25 +257,10 @@ class BackendParams(DefaultDependency): maxzoom: int = 8 -@attr.s -class CustomFileBackend(FileBackend): - """Fake backend to prove we can overwrite min/max zoom.""" - - minzoom: Optional[int] = attr.ib(default=None) - maxzoom: Optional[int] = attr.ib(default=None) - - def __attrs_post_init__(self): - """Post Init: if not passed in init, try to read from self.input.""" - self.mosaic_def = self.mosaic_def or self._read() - self.minzoom = self.minzoom or self.mosaic_def.minzoom - self.maxzoom = self.maxzoom or self.mosaic_def.maxzoom - self.bounds = self.mosaic_def.bounds - - def test_MosaicTilerFactory_BackendParams(): """Test MosaicTilerFactory factory with Backend dependency.""" mosaic = MosaicTilerFactory( - reader=CustomFileBackend, + reader=FileBackend, backend_dependency=BackendParams, router_prefix="/mosaic", ) @@ -250,7 +287,7 @@ def test_MosaicTilerFactory_PixelSelectionParams(): """Test MosaicTilerFactory factory with a customized default PixelSelectionMethod.""" mosaic = MosaicTilerFactory(router_prefix="/mosaic") mosaic_highest = MosaicTilerFactory( - pixel_selection_dependency=lambda: PixelSelectionMethod.highest.method(), + pixel_selection_dependency=lambda: PixelSelectionMethod.highest.value, router_prefix="/mosaic_highest", ) @@ -279,7 +316,7 @@ def test_MosaicTilerFactory_PixelSelectionParams(): def test_MosaicTilerFactory_strict_zoom(monkeypatch): """Test MosaicTilerFactory factory with STRICT Zoom Mode""" - monkeypatch.setenv("MOSAIC_STRICT_ZOOM", True) + monkeypatch.setenv("MOSAIC_STRICT_ZOOM", "TRUE") mosaic = MosaicTilerFactory() app = FastAPI() diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index ca4ecdb59..6cceeae2b 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -13,23 +13,23 @@ from fastapi import Depends, HTTPException, Path, Query from geojson_pydantic.features import Feature from geojson_pydantic.geometries import Polygon -from morecantile import tms +from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets from pydantic import conint -from rio_tiler.constants import MAX_THREADS +from rio_tiler.constants import MAX_THREADS, WGS84_CRS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader from rio_tiler.models import Bounds +from rio_tiler.mosaic.methods import PixelSelectionMethod from rio_tiler.mosaic.methods.base import MosaicMethodBase from starlette.requests import Request from starlette.responses import HTMLResponse, Response -from titiler.core.dependencies import DefaultDependency +from titiler.core.dependencies import CoordCRSParams, DefaultDependency from titiler.core.factory import BaseTilerFactory, img_endpoint_params from titiler.core.models.mapbox import TileJSON from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.mosaic.models.responses import Point -from titiler.mosaic.resources.enums import PixelSelectionMethod if sys.version_info >= (3, 9): from typing import Annotated # pylint: disable=no-name-in-module @@ -37,19 +37,16 @@ from typing_extensions import Annotated -# BaseBackend does not support other TMS than WebMercator -mosaic_tms = TileMatrixSets({"WebMercatorQuad": tms.get("WebMercatorQuad")}) - - def PixelSelectionParams( - pixel_selection: Annotated[ - PixelSelectionMethod, Query(description="Pixel selection method.") - ] = PixelSelectionMethod.first, + pixel_selection: Annotated[ # type: ignore + Literal[tuple([e.name for e in PixelSelectionMethod])], + Query(description="Pixel selection method."), + ] = "first", ) -> MosaicMethodBase: """ Returns the mosaic method used to combine datasets together. """ - return pixel_selection.method() + return PixelSelectionMethod[pixel_selection].value() @dataclass @@ -72,7 +69,7 @@ class MosaicTilerFactory(BaseTilerFactory): pixel_selection_dependency: Callable[..., MosaicMethodBase] = PixelSelectionParams - supported_tms: TileMatrixSets = mosaic_tms + supported_tms: TileMatrixSets = morecantile_tms default_tms: str = "WebMercatorQuad" # Add/Remove some endpoints @@ -223,31 +220,47 @@ def info_geojson( def tile(self): # noqa: C901 """Register /tiles endpoints.""" - @self.router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get("/tiles/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get("/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) + @self.router.get("/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) + @self.router.get("/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) + @self.router.get("/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params, ) def tile( - z: Annotated[int, Path(description="TMS tiles's zoom level")], - x: Annotated[int, Path(description="TMS tiles's column")], - y: Annotated[int, Path(description="TMS tiles's row")], - TileMatrixSetId: Annotated[ + 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.", + ), + ], + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, scale: Annotated[ - conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..." + conint(gt=0, le=4), + "Tile size scale. 1=256x256, 2=512x512...", ] = 1, format: Annotated[ ImageType, @@ -294,9 +307,11 @@ def tile( "yes", ] + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader( src_path, + tms=tms, reader=self.dataset_reader, reader_options={**reader_params}, **backend_params, @@ -357,16 +372,16 @@ def tilejson(self): # noqa: C901 response_model_exclude_none=True, ) @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, ) def tilejson( request: Request, - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, src_path=Depends(self.path_dependency), tile_format: Annotated[ @@ -421,7 +436,7 @@ def tilejson( "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -442,9 +457,11 @@ def tilejson( if qs: tiles_url += f"?{urlencode(qs)}" + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader( src_path, + tms=tms, reader=self.dataset_reader, reader_options={**reader_params}, **backend_params, @@ -452,6 +469,7 @@ def tilejson( center = list(src_dst.mosaic_def.center) if minzoom is not None: center[-1] = minzoom + return { "bounds": src_dst.bounds, "center": tuple(center), @@ -464,13 +482,13 @@ def map_viewer(self): # noqa: C901 """Register /map endpoint.""" @self.router.get("/map", response_class=HTMLResponse) - @self.router.get("/{TileMatrixSetId}/map", response_class=HTMLResponse) + @self.router.get("/{tileMatrixSetId}/map", response_class=HTMLResponse) def map_viewer( request: Request, src_path=Depends(self.path_dependency), - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, tile_format: Annotated[ Optional[ImageType], @@ -519,12 +537,12 @@ def map_viewer( ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( - request, "tilejson", TileMatrixSetId=TileMatrixSetId + request, "tilejson", tileMatrixSetId=tileMatrixSetId ) if request.query_params._list: tilejson_url += f"?{urlencode(request.query_params._list)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) return self.templates.TemplateResponse( name="map.html", context={ @@ -541,13 +559,13 @@ def wmts(self): # noqa: C901 @self.router.get("/WMTSCapabilities.xml", response_class=XMLResponse) @self.router.get( - "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse + "/{tileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse ) def wmts( request: Request, - TileMatrixSetId: Annotated[ + tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"TileMatrixSet Name (default: '{self.default_tms}')", + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", ] = self.default_tms, src_path=Depends(self.path_dependency), tile_format: Annotated[ @@ -601,7 +619,7 @@ def wmts( "y": "{TileRow}", "scale": tile_scale, "format": tile_format.value, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } tiles_url = self.url_for(request, "tile", **route_params) @@ -622,10 +640,11 @@ def wmts( if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader( src_path, + tms=tms, reader=self.dataset_reader, reader_options={**reader_params}, **backend_params, @@ -635,13 +654,13 @@ def wmts( maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom tileMatrix = [] - for zoom in range(minzoom, maxzoom + 1): + for zoom in range(minzoom, maxzoom + 1): # type: ignore matrix = tms.matrix(zoom) tm = f""" - {matrix.identifier} + {matrix.id} {matrix.scaleDenominator} - {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} + {matrix.pointOfOrigin[0]} {matrix.pointOfOrigin[1]} {matrix.tileWidth} {matrix.tileHeight} {matrix.matrixWidth} @@ -671,7 +690,7 @@ def point(self): """Register /point endpoint.""" @self.router.get( - r"/point/{lon},{lat}", + "/point/{lon},{lat}", response_model=Point, response_class=JSONResponse, responses={200: {"description": "Return a value for a point"}}, @@ -681,6 +700,7 @@ def point( lon: Annotated[float, Path(description="Longitude")], lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), backend_params=Depends(self.backend_dependency), @@ -700,6 +720,7 @@ def point( values = src_dst.point( lon, lat, + coord_crs=coord_crs or WGS84_CRS, threads=threads, **layer_params, **dataset_params, @@ -724,7 +745,7 @@ def assets(self): """Register /assets endpoint.""" @self.router.get( - r"/{minx},{miny},{maxx},{maxy}/assets", + "/{minx},{miny},{maxx},{maxy}/assets", responses={200: {"description": "Return list of COGs in bounding box"}}, ) def assets_for_bbox( @@ -733,6 +754,7 @@ def assets_for_bbox( maxx: Annotated[float, Path(description="Bounding box max X")], maxy: Annotated[float, Path(description="Bounding box max Y")], src_path=Depends(self.path_dependency), + coord_crs=Depends(CoordCRSParams), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), @@ -745,16 +767,23 @@ def assets_for_bbox( reader_options={**reader_params}, **backend_params, ) as src_dst: - return src_dst.assets_for_bbox(minx, miny, maxx, maxy) + return src_dst.assets_for_bbox( + minx, + miny, + maxx, + maxy, + coord_crs=coord_crs or WGS84_CRS, + ) @self.router.get( - r"/{lon},{lat}/assets", + "/{lon},{lat}/assets", responses={200: {"description": "Return list of COGs"}}, ) def assets_for_lon_lat( lon: Annotated[float, Path(description="Longitude")], lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), + coord_crs=Depends(CoordCRSParams), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), @@ -767,25 +796,54 @@ def assets_for_lon_lat( reader_options={**reader_params}, **backend_params, ) as src_dst: - return src_dst.assets_for_point(lon, lat) + return src_dst.assets_for_point( + lon, + lat, + coord_crs=coord_crs or WGS84_CRS, + ) @self.router.get( - r"/{z}/{x}/{y}/assets", + "/{z}/{x}/{y}/assets", + responses={200: {"description": "Return list of COGs"}}, + ) + @self.router.get( + "/{tileMatrixSetId}/{z}/{x}/{y}/assets", responses={200: {"description": "Return list of COGs"}}, ) def assets_for_tile( - z: Annotated[int, Path(description="TMS tiles's zoom level")], - x: Annotated[int, Path(description="TMS tiles's column")], - y: Annotated[int, Path(description="TMS tiles's row")], + 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.", + ), + ], + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", + ] = self.default_tms, src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Return a list of assets which overlap a given tile""" + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): with self.reader( src_path, + tms=tms, reader=self.dataset_reader, reader_options={**reader_params}, **backend_params, diff --git a/src/titiler/mosaic/titiler/mosaic/resources/__init__.py b/src/titiler/mosaic/titiler/mosaic/resources/__init__.py deleted file mode 100644 index 60f13dcb0..000000000 --- a/src/titiler/mosaic/titiler/mosaic/resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""titiler.core.resources.""" diff --git a/src/titiler/mosaic/titiler/mosaic/resources/enums.py b/src/titiler/mosaic/titiler/mosaic/resources/enums.py deleted file mode 100644 index 53822891d..000000000 --- a/src/titiler/mosaic/titiler/mosaic/resources/enums.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Titiler.mosaic Enums.""" - -from enum import Enum -from types import DynamicClassAttribute - -from rio_tiler.mosaic.methods import defaults - - -class PixelSelectionMethod(str, Enum): - """rio-tiler.mosaic pixel selection methods""" - - first = "first" - highest = "highest" - lowest = "lowest" - mean = "mean" - median = "median" - stdev = "stdev" - - @DynamicClassAttribute - def method(self): - """Return rio-tiler-mosaic pixel selection class""" - return getattr(defaults, f"{self._value_.title()}Method") From d33a41d34ccf66438ac80b59ec1776a2ca70fe88 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 17 Jul 2023 17:28:58 +0200 Subject: [PATCH 010/405] remove deleted docs --- .github/workflows/deploy_mkdocs.yml | 1 - docs/mkdocs.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index f9b537459..0781cc394 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -58,7 +58,6 @@ jobs: --exclude_source \ --overwrite \ titiler.mosaic.factory \ - titiler.mosaic.resources.enums \ titiler.mosaic.errors - name: Deploy docs diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1d3aa53f7..a3fdf9564 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -76,7 +76,6 @@ nav: - viewer: api/titiler/extensions/viewer.md - titiler.mosaic: - factory: api/titiler/mosaic/factory.md - - enums: api/titiler/mosaic/resources/enums.md - errors: api/titiler/mosaic/errors.md - titiler.application: From 20abf132a4cad8af7e8fb94c1cb51eb12d603983 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 17 Jul 2023 18:02:00 +0200 Subject: [PATCH 011/405] release date --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 029353735..ceb36d82e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Release Notes -## Next (TDB) +## 0.12.0 (2023-07-17) * use `Annotated` Type for Query/Path parameters * replace variable `TileMatrixSetId` by `tileMatrixSetId` From 17cdff2f0ddf08dbd9a47c2140b13c4bbcc30b6d Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 17 Jul 2023 18:02:14 +0200 Subject: [PATCH 012/405] =?UTF-8?q?Bump=20version:=200.11.7=20=E2=86=92=20?= =?UTF-8?q?0.12.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- deployment/aws/lambda/Dockerfile | 2 +- deployment/k8s/charts/Chart.yaml | 2 +- pyproject.toml | 10 +++++----- src/titiler/application/pyproject.toml | 6 +++--- .../application/titiler/application/__init__.py | 2 +- src/titiler/core/titiler/core/__init__.py | 2 +- src/titiler/extensions/pyproject.toml | 2 +- src/titiler/extensions/titiler/extensions/__init__.py | 2 +- src/titiler/mosaic/pyproject.toml | 2 +- src/titiler/mosaic/titiler/mosaic/__init__.py | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0c5332639..de813b66b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.11.7 +current_version = 0.12.0 commit = True tag = True tag_name = {new_version} diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile index 9e8fc276e..27fc9ff60 100644 --- a/deployment/aws/lambda/Dockerfile +++ b/deployment/aws/lambda/Dockerfile @@ -5,7 +5,7 @@ FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION} WORKDIR /tmp RUN pip install pip -U -RUN pip install "titiler.application==0.11.7" "mangum>=0.10.0" -t /asset --no-binary pydantic +RUN pip install "titiler.application==0.12.0" "mangum>=0.10.0" -t /asset --no-binary pydantic # Reduce package size and remove useless files RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done; diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml index 9d003318e..0b72336c6 100644 --- a/deployment/k8s/charts/Chart.yaml +++ b/deployment/k8s/charts/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 0.11.7 +appVersion: 0.12.0 description: A dynamic Web Map tile server name: titiler version: 1.1.2 diff --git a/pyproject.toml b/pyproject.toml index b691b6110..5305dd3ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,12 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: GIS", ] -version="0.11.7" +version="0.12.0" dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions==0.11.7", - "titiler.mosaic==0.11.7", - "titiler.application==0.11.7", + "titiler.core==0.12.0", + "titiler.extensions==0.12.0", + "titiler.mosaic==0.12.0", + "titiler.application==0.12.0", ] [project.optional-dependencies] diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index 7ae969661..667e16567 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -29,9 +29,9 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions[cogeo,stac]==0.11.7", - "titiler.mosaic==0.11.7", + "titiler.core==0.12.0", + "titiler.extensions[cogeo,stac]==0.12.0", + "titiler.mosaic==0.12.0", "starlette-cramjam>=0.3,<0.4", "python-dotenv", ] diff --git a/src/titiler/application/titiler/application/__init__.py b/src/titiler/application/titiler/application/__init__.py index f59030372..ec0c66906 100644 --- a/src/titiler/application/titiler/application/__init__.py +++ b/src/titiler/application/titiler/application/__init__.py @@ -1,3 +1,3 @@ """titiler.application""" -__version__ = "0.11.7" +__version__ = "0.12.0" diff --git a/src/titiler/core/titiler/core/__init__.py b/src/titiler/core/titiler/core/__init__.py index f26d8963f..ac0da476f 100644 --- a/src/titiler/core/titiler/core/__init__.py +++ b/src/titiler/core/titiler/core/__init__.py @@ -1,6 +1,6 @@ """titiler.core""" -__version__ = "0.11.7" +__version__ = "0.12.0" from . import dependencies, errors, factory, routing # noqa from .factory import ( # noqa diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 4a66a4f1a..9859313c0 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7" + "titiler.core==0.12.0" ] [project.optional-dependencies] diff --git a/src/titiler/extensions/titiler/extensions/__init__.py b/src/titiler/extensions/titiler/extensions/__init__.py index 18c28ff64..36552673c 100644 --- a/src/titiler/extensions/titiler/extensions/__init__.py +++ b/src/titiler/extensions/titiler/extensions/__init__.py @@ -1,6 +1,6 @@ """titiler.extensions""" -__version__ = "0.11.7" +__version__ = "0.12.0" from .cogeo import cogValidateExtension # noqa from .stac import stacExtension # noqa diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 34d0aaabd..094cb7fa1 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7", + "titiler.core==0.12.0", "cogeo-mosaic>=6.2,<7.0", ] diff --git a/src/titiler/mosaic/titiler/mosaic/__init__.py b/src/titiler/mosaic/titiler/mosaic/__init__.py index 29d6d4db2..408139859 100644 --- a/src/titiler/mosaic/titiler/mosaic/__init__.py +++ b/src/titiler/mosaic/titiler/mosaic/__init__.py @@ -1,6 +1,6 @@ """titiler.mosaic""" -__version__ = "0.11.7" +__version__ = "0.12.0" from . import errors, factory # noqa from .factory import MosaicTilerFactory # noqa From b44b5196538c496947b941489a2fef07d5e81e6b Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Wed, 19 Jul 2023 15:21:29 +0200 Subject: [PATCH 013/405] change openapi/docs url and update landing page (#671) * change openapi/docs url and update landing page * update tests --- CHANGES.md | 7 + src/titiler/application/tests/test_main.py | 4 +- .../application/titiler/application/main.py | 71 +++++++++- .../titiler/application/templates/index.html | 130 +++++++++++------- 4 files changed, 157 insertions(+), 55 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ceb36d82e..69a61815f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Release Notes +## Unreleased + +### titiler.application + +* use `/api` and `/api.html` for documentation (instead of `/openapi.json` and `/docs`) +* update landing page + ## 0.12.0 (2023-07-17) * use `Annotated` Type for Query/Path parameters diff --git a/src/titiler/application/tests/test_main.py b/src/titiler/application/tests/test_main.py index 40a46818d..f573f4ca0 100644 --- a/src/titiler/application/tests/test_main.py +++ b/src/titiler/application/tests/test_main.py @@ -7,8 +7,8 @@ def test_health(app): assert response.status_code == 200 assert response.json() == {"ping": "pong!"} - response = app.get("/openapi.json") + response = app.get("/api") assert response.status_code == 200 - response = app.get("/docs") + response = app.get("/api.html") assert response.status_code == 200 diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index 89634e01c..d9508a611 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -49,6 +49,8 @@ app = FastAPI( title=api_settings.name, + openapi_url="/api", + docs_url="/api.html", description="""A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL. --- @@ -162,9 +164,70 @@ def ping(): @app.get("/", response_class=HTMLResponse, include_in_schema=False) def landing(request: Request): - """TiTiler Landing page""" + """TiTiler landing page.""" + data = { + "title": "titiler", + "links": [ + { + "title": "Landing page", + "href": str(request.url_for("landing")), + "type": "text/html", + "rel": "self", + }, + { + "title": "the API definition (JSON)", + "href": str(request.url_for("openapi")), + "type": "application/vnd.oai.openapi+json;version=3.0", + "rel": "service-desc", + }, + { + "title": "the API documentation", + "href": str(request.url_for("swagger_ui_html")), + "type": "text/html", + "rel": "service-doc", + }, + { + "title": "TiTiler Documentation (external link)", + "href": "https://developmentseed.org/titiler/", + "type": "text/html", + "rel": "doc", + }, + { + "title": "TiTiler source code (external link)", + "href": "https://github.com/developmentseed/titiler", + "type": "text/html", + "rel": "doc", + }, + ], + } + + urlpath = request.url.path + crumbs = [] + baseurl = str(request.base_url).rstrip("/") + + crumbpath = str(baseurl) + for crumb in urlpath.split("/"): + crumbpath = crumbpath.rstrip("/") + part = crumb + if part is None or part == "": + part = "Home" + crumbpath += f"/{crumb}" + crumbs.append({"url": crumbpath.rstrip("/"), "part": part.capitalize()}) + return templates.TemplateResponse( - name="index.html", - context={"request": request}, - media_type="text/html", + "index.html", + { + "request": request, + "response": data, + "template": { + "api_root": baseurl, + "params": request.query_params, + "title": "TiTiler", + }, + "crumbs": crumbs, + "url": str(request.url), + "baseurl": baseurl, + "urlpath": str(request.url.path), + "urlparams": str(request.url.query), + }, ) diff --git a/src/titiler/application/titiler/application/templates/index.html b/src/titiler/application/titiler/application/templates/index.html index 70f9c7ea3..04d73b056 100644 --- a/src/titiler/application/titiler/application/templates/index.html +++ b/src/titiler/application/titiler/application/templates/index.html @@ -1,56 +1,88 @@ - - + + + {{ template.title }} + + + + + + + + + - - - TiTiler - - - - -
-
- ______   __     ______   __     __         ______     ______
-/\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
-\/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
-   \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
-    \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
-            
+
+  ______   __     ______   __     __         ______     ______
+ /\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
+ \/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
+    \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
+     \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
+             
+

+ {{ response.description }} +

-

API documentations: /docs -

-

TiTiler Online documentations: https://developmentseed.org/titiler/ -

-

+

Links

+ -
- Created by - - Development Seed - -
- + + +
+ Created by + + Development Seed + +
+ + From aee5ece08fe56518635c868f21f0eccfe3103db6 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 19 Jul 2023 19:23:17 +0200 Subject: [PATCH 014/405] fix landing page --- .../application/titiler/application/templates/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/titiler/application/titiler/application/templates/index.html b/src/titiler/application/titiler/application/templates/index.html index 04d73b056..a085fcd44 100644 --- a/src/titiler/application/titiler/application/templates/index.html +++ b/src/titiler/application/titiler/application/templates/index.html @@ -24,7 +24,7 @@