diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 614fc611c..d8501cc0f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.6 +current_version = 0.17.0 commit = True tag = True tag_name = {new_version} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 86a2b2050..4fa1bd99b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,14 @@ +# Set update schedule for GitHub Actions + version: 2 updates: - - package-ecosystem: github-actions - directory: / + + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: daily \ No newline at end of file + # Check for updates to GitHub Actions every week + interval: "weekly" + groups: + all: + patterns: + - "*" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d88de7bf6..5f8524cc7 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: install siege run: | diff --git a/.github/workflows/check_charts.yaml b/.github/workflows/check_charts.yaml index e79f20937..0e42664b9 100644 --- a/.github/workflows/check_charts.yaml +++ b/.github/workflows/check_charts.yaml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -33,16 +33,16 @@ jobs: fi - name: Set up Helm - uses: azure/setup-helm@v1 + uses: azure/setup-helm@v4 with: version: v3.9.2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: python-version: 3.7 - name: Set up chart-testing - uses: helm/chart-testing-action@v2.2.1 + uses: helm/chart-testing-action@v2.6.1 - name: Run chart-testing (list-changed) id: list-changed @@ -56,7 +56,7 @@ jobs: run: ct lint --chart-dirs deployment/k8s --target-branch ${{ github.event.repository.default_branch }} - name: Build container - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 if: steps.list-changed.outputs.changed == 'true' with: # See https://github.com/developmentseed/titiler/discussions/387 @@ -67,7 +67,7 @@ jobs: tags: "titiler:dev" - name: Create kind cluster - uses: helm/kind-action@v1.2.0 + uses: helm/kind-action@v1.9.0 if: steps.list-changed.outputs.changed == 'true' - name: Load container image in kind cluster diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb654d7f4..04ec6a7c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -67,7 +67,7 @@ jobs: - name: Upload Results if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: unittests @@ -79,7 +79,7 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: @@ -161,17 +161,17 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} # Uvicorn - # - name: Build and push uvicorn - # uses: docker/build-push-action@v5 - # with: - # platforms: linux/amd64,linux/arm64 - # context: . - # file: dockerfiles/Dockerfile.uvicorn - # push: ${{ github.event_name != 'pull_request' }} - # tags: ${{ steps.meta-uvicorn.outputs.tags }} - # labels: ${{ steps.meta-uvicorn.outputs.labels }} - # cache-from: type=gha - # cache-to: type=gha,mode=max + - name: Build and push uvicorn + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + context: . + file: dockerfiles/Dockerfile.uvicorn + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-uvicorn.outputs.tags }} + labels: ${{ steps.meta-uvicorn.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max # Gunicorn - name: Build and push @@ -204,7 +204,7 @@ jobs: shell: bash - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 0781cc394..5a8928a04 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout main - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/CHANGES.md b/CHANGES.md index 2e0f9f3f6..cbe3508fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,69 @@ ## Unreleased +### titiler.core + +* Add `use_epsg` parameter to WMTS endpoint to resolve ArcMAP issues and fix XML formating (author @gadomski, https://github.com/developmentseed/titiler/pull/782) +* Add more OpenAPI metadata for algorithm (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/783) + +### titiler.application + +* fix invalid url parsing in HTML responses + +## 0.17.0 (2024-01-17) + +### titiler.core + +* update `rio-tiler` version to `>6.3.0` +* use new `align_bounds_with_dataset=True` rio-tiler option in GeoJSON statistics methods for more precise calculation + +## 0.16.2 (2024-01-17) + +### titiler.core + +* fix leafletjs template maxZoom to great than 18 for `/map` endpoint (author @Firefishy, https://github.com/developmentseed/titiler/pull/749) + +## 0.16.1 (2024-01-08) + +### titiler.core + +* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method + +### titiler.mosaic + +* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method + +## 0.16.0 (2024-01-08) + +### titiler.core + +* update FastAPI version lower limit to `>=0.107.0` +* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744) + +### titiler.extensions + +* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744) + +### titiler.application + +* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744) + +## 0.15.8 (2024-01-08) + +### titiler.core + +* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1] + +### titiler.mosaic + +* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1] + +## 0.15.7 (2024-01-08) + +### titiler.core + +* update FastAPI version upper limit to `<0.107.0` to avoid starlette breaking change (`0.28`) + ### titiler.application * add simple *auth* (optional) based on `global_access_token` string, set with `TITILER_API_GLOBAL_ACCESS_TOKEN` environment variable (author @DeflateAwning, https://github.com/developmentseed/titiler/pull/735) diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile index 69961ce19..98751b851 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.15.6" "mangum>=0.10.0" -t /asset --no-binary pydantic +RUN pip install "titiler.application==0.17.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/azure/README.md b/deployment/azure/README.md index 7516cde29..b7d395f69 100644 --- a/deployment/azure/README.md +++ b/deployment/azure/README.md @@ -1,13 +1,13 @@ ### Function -TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. As for AWS Lambda we can make our FastAPI application work on Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker). +TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. We can make our FastAPI application work as an Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker). If you are not familiar with **Azure functions** we recommend checking https://docs.microsoft.com/en-us/azure/azure-functions/ first. Minimal TiTiler Azure function code: ```python import azure.functions as func -from titiler.application.routers import cog, mosaic, stac, tms +from titiler.application.main import cog, mosaic, stac, tms from fastapi import FastAPI @@ -20,14 +20,12 @@ app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"]) app.include_router(tms.router, tags=["TileMatrixSets"]) -def main( +async def main( req: func.HttpRequest, context: func.Context, ) -> func.HttpResponse: - return func.AsgiMiddleware(app).handle(req, context) + return await func.AsgiMiddleware(app).handle_async(req, context) ``` -Note: there is a `bug` in `azure.functions.AsgiMiddleware` which prevent using `starlette.BaseHTTPMiddleware` middlewares (see: https://github.com/Azure/azure-functions-python-worker/issues/903). - #### Requirements - Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli - Azure Function Tool: https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local @@ -42,8 +40,8 @@ $ cd titiler/deployment/azure $ az login $ az group create --name AzureFunctionsTiTiler-rg --location eastus -$ az storage account create --name TiTilerStorage --sku Standard_LRS -$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.8 --functions-version 3 --name titiler --os-type linux +$ az storage account create --name titilerstorage --sku Standard_LRS -g AzureFunctionsTiTiler-rg +$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.8 --functions-version 3 --name titiler --os-type linux -g AzureFunctionsTiTiler-rg -s titilerstorage $ func azure functionapp publish titiler ``` diff --git a/deployment/azure/app/__init__.py b/deployment/azure/app/__init__.py index 5aab2727d..33e1d2939 100644 --- a/deployment/azure/app/__init__.py +++ b/deployment/azure/app/__init__.py @@ -8,17 +8,15 @@ from starlette_cramjam.middleware import CompressionMiddleware from titiler.application import __version__ as titiler_version -from titiler.application.custom import templates -from titiler.application.routers import cog, mosaic, stac, tms +from titiler.application.main import cog, mosaic, stac, templates, tms from titiler.application.settings import ApiSettings from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers - -# from titiler.core.middleware import ( -# CacheControlMiddleware, -# LoggerMiddleware, -# LowerCaseQueryStringMiddleware, -# TotalTimeMiddleware, -# ) +from titiler.core.middleware import ( + CacheControlMiddleware, + LoggerMiddleware, + LowerCaseQueryStringMiddleware, + TotalTimeMiddleware, +) from titiler.mosaic.errors import MOSAIC_STATUS_CODES api_settings = ApiSettings() @@ -68,19 +66,18 @@ }, ) -# see https://github.com/encode/starlette/issues/1320 -# app.add_middleware( -# CacheControlMiddleware, -# cachecontrol=api_settings.cachecontrol, -# exclude_path={r"/healthz"}, -# ) +app.add_middleware( + CacheControlMiddleware, + cachecontrol=api_settings.cachecontrol, + exclude_path={r"/healthz"}, +) -# if api_settings.debug: -# app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) -# app.add_middleware(TotalTimeMiddleware) +if api_settings.debug: + app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) + app.add_middleware(TotalTimeMiddleware) -# if api_settings.lower_case_query_parameters: -# app.add_middleware(LowerCaseQueryStringMiddleware) +if api_settings.lower_case_query_parameters: + app.add_middleware(LowerCaseQueryStringMiddleware) @app.get("/healthz", description="Health Check", tags=["Health Check"]) @@ -99,9 +96,9 @@ def landing(request: Request): ) -def main( +async def main( req: func.HttpRequest, context: func.Context, ) -> func.HttpResponse: """Run App in AsgiMiddleware.""" - return func.AsgiMiddleware(app).handle(req, context) + return await func.AsgiMiddleware(app).handle_async(req, context) diff --git a/deployment/azure/host.json b/deployment/azure/host.json index 8e588272b..6e86c559b 100644 --- a/deployment/azure/host.json +++ b/deployment/azure/host.json @@ -10,7 +10,7 @@ }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[2.*, 3.0.0)" + "version": "[3.*, 4.0.0)" }, "extensions": { "http": { diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml index 8758e641f..6b5dbe9b6 100644 --- a/deployment/k8s/charts/Chart.yaml +++ b/deployment/k8s/charts/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 0.15.6 +appVersion: 0.17.0 description: A dynamic Web Map tile server name: titiler version: 1.1.2 diff --git a/docker-compose.yml b/docker-compose.yml index 9e3207e76..a3c61fa53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: titiler: # See https://github.com/developmentseed/titiler/discussions/387 - platform: linux/amd64 + # platform: linux/amd64 build: context: . dockerfile: dockerfiles/Dockerfile.gunicorn @@ -48,7 +48,7 @@ services: titiler-uvicorn: # See https://github.com/developmentseed/titiler/discussions/387 - platform: linux/amd64 + # platform: linux/amd64 build: context: . dockerfile: dockerfiles/Dockerfile.uvicorn diff --git a/docs/src/advanced/rendering.md b/docs/src/advanced/rendering.md index ea31bfd3a..fa131ff4d 100644 --- a/docs/src/advanced/rendering.md +++ b/docs/src/advanced/rendering.md @@ -16,19 +16,22 @@ Titiler supports both default colormaps (each with a name) and custom color maps ### Default Colormaps -Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. +Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. A list of available color maps can be found in Titiler's Swagger docs, or in the [rio-tiler documentation](https://cogeotiff.github.io/rio-tiler/colormap/#default-rio-tilers-colormaps). To use a default colormap, simply use the parameter `colormap_name`: -```python3 -import requests +```python +import httpx -resp = requests.get("titiler.xyz/cog/preview", params={ - "url": "", - "colormap_name": "" # e.g. autumn_r -}) +resp = httpx.get( + "https://titiler.xyz/cog/preview", + params={ + "url": "", + "colormap_name": "" # e.g. autumn_r + } +) ``` You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse it. @@ -37,19 +40,19 @@ You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse If you'd like to specify your own colormap, you can specify your own using an encoded JSON: -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https://titiler.xyz/cog/preview", params={ - "url": "", + "url": "", "bidx": "1", - "colormap": { - "0": "#e5f5f9", - "10": "#99d8c9", - "255": "#2ca25f", - } + "colormap": json.dumps({ + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + }) } ) ``` @@ -75,13 +78,13 @@ Titiler supports color formulae as defined in [Mapbox's `rio-color` plugin](http In Titiler, color_formulae are applied through the `color_formula` parameter as a string. An example of this option in action: -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https://titiler.xyz/cog/preview", params={ - "url": "", + "url": "", "color_formula": "gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5" } ) @@ -91,25 +94,41 @@ response = requests.get( Rescaling is the act of adjusting the minimum and maximum values when rendering an image. In an image with a single band, the rescaled minimum value will be set to black, and the rescaled maximum value will be set to white. This is useful if you want to accentuate features that only appear at a certain pixel value (e.g. you have a DEM, but you want to highlight how the terrain changes between sea level and 100m). -Titiler supports rescaling on a per-band basis, using the `rescaling` parameter. The input is a list of comma-delimited min-max ranges (e.g. ["0,100", "100,200", "0,1000]). +All titiler endpoinds returning *image* support `rescale` parameter. The parameter should be in form of `"rescale={min},{max}"`. -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https;//titiler.xyz/cog/preview", params={ - "url": "", - "rescaling": ["0,100", "0,1000", "0,10000"] - } + "url": "", + "rescale": "0,100", + }, +) +``` + +Titiler supports rescaling on a per-band basis, using multiple `rescale` parameters. + +```python +import httpx + +response = httpx.get( + "https;//titiler.xyz/cog/preview", + params=( + ("url", ""), + ("rescale", "0,100"), + ("rescale", "0,1000"), + ("rescale", "0,10000"), + ), ) ``` By default, Titiler will rescale the bands using the min/max values of the input datatype. For example, PNG images 8- or 16-bit unsigned pixels, -giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. +giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. -For certain datasets (e.g. DEMs) this default behaviour can make the image seem washed out (or even entirely one color), +For certain datasets (e.g. DEMs) this default behaviour can make the image seem washed out (or even entirely one color), so if you see this happen look into rescaling your images to something that makes sense for your data. It is also possible to add a [rescaling dependency](../../api/titiler/core/dependencies/#rescalingparams) to automatically apply -a default rescale. \ No newline at end of file +a default rescale. diff --git a/docs/src/examples/code/mosaic_from_urls.md b/docs/src/examples/code/mosaic_from_urls.md index 28f9e5913..211ed43b8 100644 --- a/docs/src/examples/code/mosaic_from_urls.md +++ b/docs/src/examples/code/mosaic_from_urls.md @@ -105,7 +105,7 @@ class MultiFilesBackend(BaseBackend): ```python """routes. -app/router.py +app/routers.py """ diff --git a/docs/src/examples/code/tiler_with_custom_tms.md b/docs/src/examples/code/tiler_with_custom_tms.md index eb4edacc1..0813dee9c 100644 --- a/docs/src/examples/code/tiler_with_custom_tms.md +++ b/docs/src/examples/code/tiler_with_custom_tms.md @@ -20,15 +20,14 @@ from pyproj import CRS EPSG6933 = TileMatrixSet.custom( (-17357881.81713629, -7324184.56362408, 17357881.81713629, 7324184.56362408), CRS.from_epsg(6933), - identifier="EPSG6933", + id="EPSG6933", matrix_scale=[1, 1], ) - # 2. Register TMS -tms = tms.register([EPSG6933]) +tms = tms.register({EPSG6933.id:EPSG6933}) -tms = TMSFactory(supported_tms=tms) -cog = TilerFactory(supported_tms=tms) +tms_factory = TMSFactory(supported_tms=tms) +cog_factory = TilerFactory(supported_tms=tms) ``` 2 - Create app and register our custom endpoints @@ -44,11 +43,11 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from fastapi import FastAPI -from .routes import cog, tms +from .routes import cog_factory, tms_factory app = FastAPI(title="My simple app with custom TMS") -app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) -app.include_router(tms.router, tags=["Tiling Schemes"]) +app.include_router(cog_factory.router, tags=["Cloud Optimized GeoTIFF"]) +app.include_router(tms_factory.router, tags=["Tiling Schemes"]) add_exception_handlers(app, DEFAULT_STATUS_CODES) ``` diff --git a/pyproject.toml b/pyproject.toml index 25adc3481..0348f988f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,12 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: GIS", ] -version="0.15.6" +version="0.17.0" dependencies = [ - "titiler.core==0.15.6", - "titiler.extensions==0.15.6", - "titiler.mosaic==0.15.6", - "titiler.application==0.15.6", + "titiler.core==0.17.0", + "titiler.extensions==0.17.0", + "titiler.mosaic==0.17.0", + "titiler.application==0.17.0", ] [project.optional-dependencies] @@ -80,7 +80,7 @@ exclude = [ ] [build-system] -requires = ["hatchling"] +requires = ["hatchling>=1.12.0"] build-backend = "hatchling.build" [tool.coverage.run] @@ -132,3 +132,6 @@ explicit_package_bases = true filterwarnings = [ "ignore::rasterio.errors.NotGeoreferencedWarning", ] + +[tool.hatch.build.targets.wheel] +bypass-selection = true diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index 3388b7299..0bc96a660 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -29,9 +29,9 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.15.6", - "titiler.extensions[cogeo,stac]==0.15.6", - "titiler.mosaic==0.15.6", + "titiler.core==0.17.0", + "titiler.extensions[cogeo,stac]==0.17.0", + "titiler.mosaic==0.17.0", "starlette-cramjam>=0.3,<0.4", "pydantic-settings~=2.0", ] diff --git a/src/titiler/application/tests/routes/test_cog.py b/src/titiler/application/tests/routes/test_cog.py index e69a0b501..0bcb751b8 100644 --- a/src/titiler/application/tests/routes/test_cog.py +++ b/src/titiler/application/tests/routes/test_cog.py @@ -67,6 +67,10 @@ def test_wmts(rio, app): "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@1x.png?url=https" in response.content.decode() ) + assert ( + "http://www.opengis.net/def/crs/EPSG/0/3857" + in response.content.decode() + ) response = app.get( "/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&tile_scale=2&tile_format=jpg" @@ -78,6 +82,13 @@ def test_wmts(rio, app): in response.content.decode() ) + response = app.get( + "/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&use_epsg=true" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + assert "EPSG:3857" in response.content.decode() + @patch("rio_tiler.io.rasterio.rasterio") def test_tile(rio, app): diff --git a/src/titiler/application/titiler/application/__init__.py b/src/titiler/application/titiler/application/__init__.py index 5618f7f98..dd13b697c 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.15.6" +__version__ = "0.17.0" diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index 8e81fddca..522088c63 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -1,6 +1,7 @@ """titiler app.""" import logging +import re import jinja2 from fastapi import Depends, FastAPI, HTTPException, Security @@ -40,10 +41,10 @@ logging.getLogger("botocore.utils").disabled = True logging.getLogger("rio-tiler").setLevel(logging.ERROR) -templates = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +templates = Jinja2Templates(env=jinja2_env) api_settings = ApiSettings() @@ -56,7 +57,8 @@ def validate_access_token(access_token: str = Security(api_key_query)): """Validates API key access token, set as the `api_settings.global_access_token` value. Returns True if no access token is required, or if the access token is valid. - Raises an HTTPException (401) if the access token is required but invalid/missing.""" + Raises an HTTPException (401) if the access token is required but invalid/missing. + """ if api_settings.global_access_token is None: return True @@ -244,6 +246,8 @@ def landing(request: Request): } urlpath = request.url.path + if root_path := request.app.root_path: + urlpath = re.sub(r"^" + root_path, "", urlpath) crumbs = [] baseurl = str(request.base_url).rstrip("/") diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index 7323f97b7..4101793dc 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -29,13 +29,13 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "fastapi>=0.100.0", + "fastapi>=0.107.0", "geojson-pydantic>=1.0,<2.0", "jinja2>=2.11.2,<4.0.0", "numpy", "pydantic~=2.0", "rasterio", - "rio-tiler>=6.2.5,<7.0", + "rio-tiler>=6.3.0,<7.0", "morecantile>=5.0,<6.0", "simplejson", "typing_extensions>=4.6.1", diff --git a/src/titiler/core/titiler/core/__init__.py b/src/titiler/core/titiler/core/__init__.py index f6d699e73..add491ed5 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.15.6" +__version__ = "0.17.0" from . import dependencies, errors, factory, routing # noqa from .factory import ( # noqa diff --git a/src/titiler/core/titiler/core/algorithm/base.py b/src/titiler/core/titiler/core/algorithm/base.py index cc2855622..33ee9c370 100644 --- a/src/titiler/core/titiler/core/algorithm/base.py +++ b/src/titiler/core/titiler/core/algorithm/base.py @@ -32,6 +32,9 @@ def __call__(self, img: ImageData) -> ImageData: class AlgorithmMetadata(BaseModel): """Algorithm metadata.""" + title: Optional[str] = None + description: Optional[str] = None + inputs: Dict outputs: Dict parameters: Dict diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 882049e3b..2976c4898 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -1,6 +1,7 @@ """titiler.core.algorithm DEM.""" import numpy +from pydantic import Field from rasterio import windows from rio_tiler.colormap import apply_cmap, cmap from rio_tiler.models import ImageData @@ -12,10 +13,13 @@ class HillShade(BaseAlgorithm): """Hillshade.""" + title: str = "Hillshade" + description: str = "Create hillshade from DEM dataset." + # parameters - azimuth: int = 90 - angle_altitude: float = 90.0 - buffer: int = 3 + azimuth: int = Field(90, ge=0, lt=360) + angle_altitude: float = Field(90.0, ge=-90.0, lt=90.0) + buffer: int = Field(3, ge=0, lt=99) # metadata input_nbands: int = 1 @@ -61,11 +65,14 @@ class Contours(BaseAlgorithm): Original idea from https://custom-scripts.sentinel-hub.com/dem/contour-lines/ """ + title: str = "Contours" + description: str = "Create contours from DEM dataset." + # parameters - increment: int = 35 - thickness: int = 1 - minz: int = -12000 - maxz: int = 8000 + increment: int = Field(35, ge=0, lt=999) + thickness: int = Field(1, ge=0, lt=10) + minz: int = Field(-12000, ge=-99999, lt=99999) + maxz: int = Field(8000, ge=-99999, lt=99999) # metadata input_nbands: int = 1 @@ -99,6 +106,9 @@ def __call__(self, img: ImageData) -> ImageData: class Terrarium(BaseAlgorithm): """Encode DEM into RGB (Mapzen Terrarium).""" + title: str = "Terrarium" + description: str = "Encode DEM into RGB (Mapzen Terrarium)." + # metadata input_nbands: int = 1 output_nbands: int = 3 @@ -122,9 +132,12 @@ def __call__(self, img: ImageData) -> ImageData: class TerrainRGB(BaseAlgorithm): """Encode DEM into RGB (Mapbox Terrain RGB).""" + title: str = "Terrarium" + description: str = "Encode DEM into RGB (Mapbox Terrain RGB)." + # parameters - interval: float = 0.1 - baseval: float = -10000.0 + interval: float = Field(0.1, ge=0.0, lt=1.0) + baseval: float = Field(-10000.0, ge=-99999.0, lt=99999.0) # metadata input_nbands: int = 1 diff --git a/src/titiler/core/titiler/core/algorithm/index.py b/src/titiler/core/titiler/core/algorithm/index.py index 2d47c4a71..e0351d77e 100644 --- a/src/titiler/core/titiler/core/algorithm/index.py +++ b/src/titiler/core/titiler/core/algorithm/index.py @@ -11,6 +11,9 @@ class NormalizedIndex(BaseAlgorithm): """Normalized Difference Index.""" + title: str = "Normalized Difference Index" + description: str = "Compute normalized difference index from two bands." + # metadata input_nbands: int = 2 output_nbands: int = 1 diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 8ce191498..5fb38772a 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -70,11 +70,10 @@ from titiler.core.routing import EndpointScope from titiler.core.utils import render_image -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore - +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) img_endpoint_params: Dict[str, Any] = { "responses": { @@ -239,7 +238,8 @@ def add_route_dependencies( route.dependant.dependencies.insert( # type: ignore 0, get_parameterless_sub_dependant( - depends=depends, path=route.path_format # type: ignore + depends=depends, + path=route.path_format, # type: ignore ), ) @@ -438,7 +438,7 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, + "content": {"application/geo+json": {}}, "description": "Return dataset's statistics from feature or featureCollection.", } }, @@ -473,6 +473,7 @@ def geojson_statistics( shape, shape_crs=coord_crs or WGS84_CRS, dst_crs=dst_crs, + align_bounds_with_dataset=True, **layer_params, **image_params, **dataset_params, @@ -741,7 +742,7 @@ def map_viewer( "request": request, "tilejson_endpoint": tilejson_url, "tms": tms, - "resolutions": [tms._resolution(matrix) for matrix in tms], + "resolutions": [matrix.cellSize for matrix in tms], }, media_type="text/html", ) @@ -778,6 +779,12 @@ def wmts( Optional[int], Query(description="Overwrite default maxzoom."), ] = None, + use_epsg: Annotated[ + bool, + Query( + description="Use EPSG code, not opengis.net, for the ows:SupportedCRS in the TileMatrixSet (set to True to enable ArcMap compatability)" + ), + ] = False, layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), tile_params=Depends(self.tile_dependency), @@ -807,6 +814,7 @@ def wmts( "minzoom", "maxzoom", "service", + "use_epsg", "request", ] qs = [ @@ -839,6 +847,11 @@ def wmts( """ tileMatrix.append(tm) + if use_epsg: + supported_crs = f"EPSG:{tms.crs.to_epsg()}" + else: + supported_crs = tms.crs.srs + return self.templates.TemplateResponse( "wmts.xml", { @@ -847,6 +860,7 @@ def wmts( "bounds": bounds, "tileMatrix": tileMatrix, "tms": tms, + "supported_crs": supported_crs, "title": "Cloud Optimized GeoTIFF", "layer_name": "cogeo", "media_type": tile_format.mediatype, @@ -1271,7 +1285,7 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, + "content": {"application/geo+json": {}}, "description": "Return dataset's statistics from feature or featureCollection.", } }, @@ -1309,6 +1323,7 @@ def geojson_statistics( feature.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, dst_crs=dst_crs, + align_bounds_with_dataset=True, **layer_params, **image_params, **dataset_params, @@ -1472,7 +1487,7 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, + "content": {"application/geo+json": {}}, "description": "Return dataset's statistics from feature or featureCollection.", } }, @@ -1510,6 +1525,7 @@ def geojson_statistics( feature.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, dst_crs=dst_crs, + align_bounds_with_dataset=True, **bands_params, **image_params, **dataset_params, @@ -1651,6 +1667,15 @@ def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: """Algorithm Metadata""" props = algorithm.model_json_schema()["properties"] + # title and description + info = { + k: v["default"] + for k, v in props.items() + if k == "title" or k == "description" + } + title = info.get("title", None) + description = info.get("description", None) + # Inputs Metadata ins = { k.replace("input_", ""): v["default"] @@ -1669,9 +1694,18 @@ def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: params = { k: v for k, v in props.items() - if not k.startswith("input_") and not k.startswith("output_") + if not k.startswith("input_") + and not k.startswith("output_") + and k != "title" + and k != "description" } - return AlgorithmMetadata(inputs=ins, outputs=outs, parameters=params) + return AlgorithmMetadata( + title=title, + description=description, + inputs=ins, + outputs=outs, + parameters=params, + ) @self.router.get( "/algorithms", diff --git a/src/titiler/core/titiler/core/templates/map.html b/src/titiler/core/titiler/core/templates/map.html index 5aac27d8b..5c4f284c0 100644 --- a/src/titiler/core/titiler/core/templates/map.html +++ b/src/titiler/core/titiler/core/templates/map.html @@ -113,7 +113,7 @@ L.tileLayer( data.tiles[0], { minZoom: data.minzoom, - maxNativeZoom: data.maxzoom, + maxZoom: data.maxzoom, bounds: L.latLngBounds([bottom, left], [top, right]), } ).addTo(map); diff --git a/src/titiler/core/titiler/core/templates/wmts.xml b/src/titiler/core/titiler/core/templates/wmts.xml index 8305851ce..897d0acbd 100644 --- a/src/titiler/core/titiler/core/templates/wmts.xml +++ b/src/titiler/core/titiler/core/templates/wmts.xml @@ -8,7 +8,7 @@ - + RESTful @@ -21,7 +21,7 @@ - + RESTful @@ -52,11 +52,11 @@ {{ tms.id }} - {{ tms.crs.srs }} + {{ supported_crs }} {% for item in tileMatrix %} {{ item | safe }} {% endfor %} - + diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 586ad2b11..0794f458a 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.15.6" + "titiler.core==0.17.0" ] [project.optional-dependencies] diff --git a/src/titiler/extensions/titiler/extensions/__init__.py b/src/titiler/extensions/titiler/extensions/__init__.py index bb898cad8..34337934b 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.15.6" +__version__ = "0.17.0" from .cogeo import cogValidateExtension # noqa from .stac import stacExtension # noqa diff --git a/src/titiler/extensions/titiler/extensions/viewer.py b/src/titiler/extensions/titiler/extensions/viewer.py index cdd14192b..48838de12 100644 --- a/src/titiler/extensions/titiler/extensions/viewer.py +++ b/src/titiler/extensions/titiler/extensions/viewer.py @@ -9,10 +9,10 @@ from titiler.core.factory import BaseTilerFactory, FactoryExtension -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) @dataclass diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index 76fc37ab1..6b59ed2a0 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -22,10 +22,10 @@ from titiler.core.resources.enums import ImageType, MediaType from titiler.core.utils import render_image -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) class WMSMediaType(str, Enum): @@ -522,7 +522,6 @@ def _reader(src_path: str): return image, format, transparent if request_type.lower() == "getmap": - # List of required parameters (styles and crs are excluded) req_keys = { "version", diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 91d906061..82d3b26d3 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.15.6", + "titiler.core==0.17.0", "cogeo-mosaic>=7.0,<8.0", ] diff --git a/src/titiler/mosaic/titiler/mosaic/__init__.py b/src/titiler/mosaic/titiler/mosaic/__init__.py index 087ad9912..4391812ed 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.15.6" +__version__ = "0.17.0" from . import errors, factory # noqa from .factory import MosaicTilerFactory # noqa diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 60cab4d89..52f83f1aa 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -505,7 +505,7 @@ def map_viewer( "request": request, "tilejson_endpoint": tilejson_url, "tms": tms, - "resolutions": [tms._resolution(matrix) for matrix in tms], + "resolutions": [matrix.cellSize for matrix in tms], }, media_type="text/html", )