From dc56ddc738565248c1198b58c0e72c7adf79aee2 Mon Sep 17 00:00:00 2001 From: Henry Rodman Date: Thu, 31 Oct 2024 04:59:12 -0500 Subject: [PATCH 1/3] set min/maxNativeZoom in tileLayer instead of min/maxZoom (#1015) --- CHANGES.md | 1 + src/titiler/core/titiler/core/templates/map.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9f25fc0ed..cb058260a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ * Use `@attrs.define` instead of dataclass for factories **breaking change** * Use `@attrs.define` instead of dataclass for factory extensions **breaking change** * Handle `numpy` types in JSON/GeoJSON response +* In the `map.html` template, use the tilejson's `minzoom` and `maxzoom` to populate `minNativeZoom` and `maxNativeZoom` parameters in leaflet `tileLayer` instead of `minZoom` and `maxZoom` ### titiler.core diff --git a/src/titiler/core/titiler/core/templates/map.html b/src/titiler/core/titiler/core/templates/map.html index e52c3f73a..4a361f98c 100644 --- a/src/titiler/core/titiler/core/templates/map.html +++ b/src/titiler/core/titiler/core/templates/map.html @@ -129,8 +129,8 @@ L.tileLayer( data.tiles[0], { - minZoom: data.minzoom, - maxZoom: data.maxzoom, + maxNativeZoom: data.maxzoom, + minNativeZoom: data.minzoom, bounds: L.latLngBounds([bottom, left], [top, right]), } ).addTo(map); From 6bc1429af1fb5ed9b6fdf419b843f689230617ff Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Thu, 31 Oct 2024 14:53:13 +0100 Subject: [PATCH 2/3] add OGC tileset list and tileset metadata endpoints (#1017) * add OGC tileset list and tileset metadata endpoints * fix --- CHANGES.md | 6 +- docs/src/advanced/endpoints_factories.md | 106 ++-- docs/src/endpoints/cog.md | 4 +- docs/src/endpoints/mosaic.md | 8 +- docs/src/endpoints/stac.md | 10 +- src/titiler/core/tests/test_factories.py | 98 ++- src/titiler/core/titiler/core/factory.py | 254 +++++++- src/titiler/core/titiler/core/models/OGC.py | 619 ++++++++++++++++++- src/titiler/mosaic/tests/test_factory.py | 24 +- src/titiler/mosaic/titiler/mosaic/factory.py | 249 +++++++- 10 files changed, 1281 insertions(+), 97 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cb058260a..e6afa4a81 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ * Use `@attrs.define` instead of dataclass for factories **breaking change** * Use `@attrs.define` instead of dataclass for factory extensions **breaking change** * Handle `numpy` types in JSON/GeoJSON response -* In the `map.html` template, use the tilejson's `minzoom` and `maxzoom` to populate `minNativeZoom` and `maxNativeZoom` parameters in leaflet `tileLayer` instead of `minZoom` and `maxZoom` +* In the `map.html` template, use the tilejson's `minzoom` and `maxzoom` to populate `minNativeZoom` and `maxNativeZoom` parameters in leaflet `tileLayer` instead of `minZoom` and `maxZoom` ### titiler.core @@ -84,6 +84,8 @@ * avoid `lat/lon` overflow in `map` viewer +* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints + ### titiler.mosaic * Rename `reader` attribute to `backend` in `MosaicTilerFactory` **breaking change** @@ -96,6 +98,8 @@ * re-order endpoints parameters +* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints + ### titiler.extensions * Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961) diff --git a/docs/src/advanced/endpoints_factories.md b/docs/src/advanced/endpoints_factories.md index f8be6b395..9d5bcdf1a 100644 --- a/docs/src/advanced/endpoints_factories.md +++ b/docs/src/advanced/endpoints_factories.md @@ -8,40 +8,24 @@ TiTiler's endpoints factories are helper functions that let users create a FastA Factories classes use [dependencies injection](dependencies.md) to define most of the endpoint options. -## BaseTilerFactory +## BaseFactory -class: `titiler.core.factory.BaseTilerFactory` +class: `titiler.core.factory.BaseFactory` Most **Factories** are built from this [abstract based class](https://docs.python.org/3/library/abc.html) which is used to define commons attributes and utility functions shared between all factories. -#### Methods - -- **register_routes**: Abstract method which needs to be define by each factories. -- **url_for**: Method to construct endpoint URL -- **add_route_dependencies**: Add dependencies to routes. - #### Attributes -- **reader**: Dataset Reader **required**. - **router**: FastAPI router. Defaults to `fastapi.APIRouter`. -- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. -- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. -- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. -- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. -- **rescale_dependency**: Dependency to set Min/Max values to rescale from, to 0 -> 255. Defaults to `titiler.core.dependencies.RescalingParams`. -- **color_formula_dependency**: Dependency to define the Color Formula. Defaults to `titiler.core.dependencies.ColorFormulaParams`. -- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` -- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` -- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` -- **environment_dependency**: Dependency to defile GDAL environment at runtime. Default to `lambda: {}`. -- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. -- **default_tms**: Default `TileMatrixSet` identifier to use. Defaults to `WebMercatorQuad`. - **router_prefix**: Set prefix to all factory's endpoint. Defaults to `""`. -- **optional_headers**: List of `OptionalHeader` which endpoints could add (if implemented). Defaults to `[]`. - **route_dependencies**: Additional routes dependencies to add after routes creations. Defaults to `[]`. - **extension**: TiTiler extensions to register after endpoints creations. Defaults to `[]`. -- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +#### Methods + +- **register_routes**: Abstract method which needs to be define by each factories. +- **url_for**: Method to construct endpoint URL +- **add_route_dependencies**: Add dependencies to routes. ## TilerFactory @@ -51,12 +35,24 @@ Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reade #### Attributes -- **reader**: Dataset Reader. Defaults to `Reader`. +- **reader**: Dataset Reader **required**. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. +- **tile_dependency**: Dependency to defile `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. - **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. - **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. - **img_preview_dependency**: Dependency to define image size for `/preview` and `/statistics` endpoints. Defaults to `titiler.core.dependencies.PreviewParams`. - **img_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.core.dependencies.PartFeatureParams`. -- **tile_dependency**: Dependency to defile `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **rescale_dependency**: Dependency to set Min/Max values to rescale from, to 0 -> 255. Defaults to `titiler.core.dependencies.RescalingParams`. +- **color_formula_dependency**: Dependency to define the Color Formula. Defaults to `titiler.core.dependencies.ColorFormulaParams`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **environment_dependency**: Dependency to defile GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. - **add_preview**: . Add `/preview` endpoint to the router. Defaults to `True`. - **add_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. - **add_viewer**: . Add `/map` endpoints to the router. Defaults to `True`. @@ -89,14 +85,16 @@ app.include_router(cog.router) | `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` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** +| `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` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** | `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** -| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** ## MultiBaseTilerFactory @@ -107,7 +105,7 @@ Custom `TilerFactory` to be used with [`rio_tiler.io.MultiBaseReader`](https://c #### Attributes -- **reader**: `MultiBase` Dataset Reader **required**. +- **reader**: `rio_tiler.io.base.MultiBaseReader` Dataset Reader **required**. - **layer_dependency**: Dependency to define assets or expression. Defaults to `titiler.core.dependencies.AssetsBidxExprParams`. - **assets_dependency**: Dependency to define assets to be used. Defaults to `titiler.core.dependencies.AssetsParams`. @@ -134,14 +132,16 @@ app.include_router(stac.router) | `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` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** +| `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` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets **Optional** | `POST` | `/feature[/{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 **Optional** ## MultiBandTilerFactory @@ -152,7 +152,7 @@ Custom `TilerFactory` to be used with [`rio_tiler.io.MultiBandReader`](https://c #### Attributes -- **reader**: `MultiBands` Dataset Reader **required**. +- **reader**: `rio_tiler.io.base.MultiBandReader` Dataset Reader **required**. - **layer_dependency**: Dependency to define assets or expression. Defaults to `titiler.core.dependencies.BandsExprParams`. - **bands_dependency**: Dependency to define bands to be used. Defaults to `titiler.core.dependencies.BandsParams`. @@ -189,14 +189,16 @@ app.include_router(landsat.router) | `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` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** +| `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 **Optional** | `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** | `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature **Optional** -| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** ## MosaicTilerFactory @@ -207,13 +209,25 @@ Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/d #### Attributes -- **reader**: `BaseBackend` Mosaic Reader **required**. -- **dataset_reader**: Dataset Reader. Defaults to `rio_tiler.io.Reader` +- **backend**: `cogeo_mosaic.backends.BaseBackend` Mosaic backend. Defaults to `cogeo_mosaic.backend.MosaicBackend`. - **backend_dependency**: Dependency to control options passed to the backend instance init. Defaults to `titiler.core.dependencies.DefaultDependency` -- **pixel_selection_dependency**: Dependency to select the `pixel_selection` method. Defaults to `titiler.mosaic.factory.PixelSelectionParams`. +- **dataset_reader**: Dataset Reader. Defaults to `rio_tiler.io.Reader` +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.mosaic.factory.DatasetPathParams`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. - **tile_dependency**: Dependency to defile `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **rescale_dependency**: Dependency to set Min/Max values to rescale from, to 0 -> 255. Defaults to `titiler.core.dependencies.RescalingParams`. +- **color_formula_dependency**: Dependency to define the Color Formula. Defaults to `titiler.core.dependencies.ColorFormulaParams`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **pixel_selection_dependency**: Dependency to select the `pixel_selection` method. Defaults to `titiler.mosaic.factory.PixelSelectionParams`. +- **environment_dependency**: Dependency to defile GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. - **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. -- **default_tms**: **DEPRECATED**, Default `TileMatrixSet` identifier to use. Defaults to `WebMercatorQuad`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **optional_headers**: List of OptionalHeader which endpoints could add (if implemented). Defaults to `[]`. - **add_viewer**: . Add `/map` endpoints to the router. Defaults to `True`. #### Endpoints @@ -224,14 +238,16 @@ Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/d | `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` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON +| `GET` | `/{tileMatrixSetId}/map` | HTML | return a simple map viewer **Optional** +| `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 **Optional** ## TMSFactory diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index cdcc77cf7..0209fdd90 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -14,14 +14,16 @@ 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` | JSON | List of OGC Tilesets available +| `GET` | `/cog/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata | `GET` | `/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/cog/{tileMatrixSetId}/map` | HTML | simple map viewer | `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/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset | `POST` | `/cog/feature[/{width}x{height}][].{format}]` | image/bin | create an image from a GeoJSON feature -| `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`) diff --git a/docs/src/endpoints/mosaic.md b/docs/src/endpoints/mosaic.md index f19544616..53249f781 100644 --- a/docs/src/endpoints/mosaic.md +++ b/docs/src/endpoints/mosaic.md @@ -13,14 +13,16 @@ 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/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets +| `GET` | `/mosaicjson/{tileMatrixSetId}/map` | HTML | simple map viewer +| `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 ## Description diff --git a/docs/src/endpoints/stac.md b/docs/src/endpoints/stac.md index 0c8e872b1..42bc64732 100644 --- a/docs/src/endpoints/stac.md +++ b/docs/src/endpoints/stac.md @@ -16,14 +16,16 @@ 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/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/stac/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/stac/{tileMatrixSetId}/map` | HTML | simple map viewer +| `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/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets -| `POST` | `/stac/feature[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering the assets -| `GET` | `/stac/{tileMatrixSetId}/map` | HTML | simple map viewer +| `POST` | `/stac/feature[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering the assets | `GET` | `/stac/viewer` | HTML | demo webpage (from `titiler.extensions.stacViewerExtension`) ## Description diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 1ad890ef0..e40cad4fe 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -7,15 +7,16 @@ import xml.etree.ElementTree as ET from enum import Enum from io import BytesIO -from typing import Dict, Optional, Type +from typing import Dict, Optional, Sequence, Type from unittest.mock import patch from urllib.parse import urlencode +import attr import httpx import morecantile import numpy import pytest -from attrs import define, field +from attrs import define from fastapi import Depends, FastAPI, HTTPException, Path, Query, security, status from morecantile.defaults import TileMatrixSets from rasterio.crs import CRS @@ -48,7 +49,7 @@ def test_TilerFactory(): """Test TilerFactory class.""" cog = TilerFactory() - assert len(cog.router.routes) == 20 + assert len(cog.router.routes) == 22 assert len(cog.supported_tms.list()) == NB_DEFAULT_TMS cog = TilerFactory(router_prefix="something", supported_tms=WEB_TMS) @@ -75,7 +76,7 @@ def test_TilerFactory(): assert response.status_code == 422 cog = TilerFactory(add_preview=False, add_part=False, add_viewer=False) - assert len(cog.router.routes) == 12 + assert len(cog.router.routes) == 14 app = FastAPI() cog = TilerFactory() @@ -725,6 +726,25 @@ def test_TilerFactory(): assert meta["dtype"] == "uint8" assert meta["count"] == 3 + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # covers only 5 zoom levels + assert len(resp["tileMatrixSetLimits"]) == 5 + @patch("rio_tiler.io.rasterio.rasterio") def test_MultiBaseTilerFactory(rio): @@ -732,7 +752,7 @@ def test_MultiBaseTilerFactory(rio): rio.open = mock_rasterio_open stac = MultiBaseTilerFactory(reader=STACReader) - assert len(stac.router.routes) == 22 + assert len(stac.router.routes) == 24 app = FastAPI() app.include_router(stac.router) @@ -1054,29 +1074,43 @@ def test_MultiBaseTilerFactory(rio): assert len(props) == 1 assert "(B09 - B01) / (B09 + B01)" in props + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # default minzoom/maxzoom are 0->24 + assert len(resp["tileMatrixSetLimits"]) == 25 + -@define +@attr.s class BandFileReader(MultiBandReader): """Test MultiBand""" - input: str = field() - tms: morecantile.TileMatrixSet = field( + input: str = attr.ib() + tms: morecantile.TileMatrixSet = attr.ib( default=morecantile.tms.get("WebMercatorQuad") ) - reader_options: Dict = field(factory=dict) - reader: Type[BaseReader] = field(default=Reader) + reader: Type[BaseReader] = attr.ib(default=Reader) + reader_options: Dict = attr.ib(factory=dict) - minzoom: int = field() - maxzoom: int = field() + bands: Sequence[str] = attr.ib(init=False) + default_bands: Optional[Sequence[str]] = attr.ib(init=False, default=None) - @minzoom.default - def _minzoom(self): - return self.tms.minzoom - - @maxzoom.default - def _maxzoom(self): - return self.tms.maxzoom + minzoom: int = attr.ib(init=False) + maxzoom: int = attr.ib(init=False) def __attrs_post_init__(self): """Parse Sceneid and get grid bounds.""" @@ -1086,6 +1120,9 @@ def __attrs_post_init__(self): self.crs = cog.crs self.minzoom = cog.minzoom self.maxzoom = cog.maxzoom + self.width = cog.width + self.height = cog.height + self.transform = cog.transform def _get_band_url(self, band: str) -> str: """Validate band's name and return band's url.""" @@ -1103,7 +1140,7 @@ def test_MultiBandTilerFactory(): bands = MultiBandTilerFactory( reader=BandFileReader, path_dependency=CustomPathParams ) - assert len(bands.router.routes) == 21 + assert len(bands.router.routes) == 23 app = FastAPI() app.include_router(bands.router) @@ -1397,6 +1434,25 @@ def test_MultiBandTilerFactory(): assert props["B01"] assert props["B09"] + # OGC Tileset + response = client.get(f"/tiles?directory={DATA_DIR}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?directory={DATA_DIR}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # 1 Zoom level (8) + assert len(resp["tileMatrixSetLimits"]) == 1 + def test_TMSFactory(): """test TMSFactory.""" @@ -1475,7 +1531,7 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) ], router_prefix="something", ) - assert len(cog.router.routes) == 20 + assert len(cog.router.routes) == 22 app = FastAPI() app.include_router(cog.router, prefix="/something") diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index fff72bd37..bdd2276cd 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -37,7 +37,7 @@ from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response -from starlette.routing import Match, compile_path, replace_params +from starlette.routing import Match, NoMatchFound, compile_path, replace_params from starlette.templating import Jinja2Templates from typing_extensions import Annotated @@ -70,7 +70,7 @@ TileParams, ) from titiler.core.models.mapbox import TileJSON -from titiler.core.models.OGC import TileMatrixSetList +from titiler.core.models.OGC import TileMatrixSetList, TileSet, TileSetList from titiler.core.models.responses import ( ColorMapsList, InfoGeoJSON, @@ -130,6 +130,7 @@ class BaseFactory(metaclass=abc.ABCMeta): Attributes: router (fastapi.APIRouter): Application router to register endpoints to. router_prefix (str): prefix where the router will be mounted in the application. + route_dependencies (list): Additional routes dependencies to add after routes creations. """ @@ -224,19 +225,23 @@ class TilerFactory(BaseFactory): Attributes: reader (rio_tiler.io.base.BaseReader): A rio-tiler reader. Defaults to `rio_tiler.io.Reader`. + reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. - dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. - render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). - colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). - process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). - tms_dependency (Callable): Endpoint dependency defining TileMatrixSet to use. - reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. - environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. + dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). + tile_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining tile options (e.g buffer, padding). stats_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's statistics method. histogram_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for numpy's histogram method. img_preview_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview method. img_part_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's part/feature methods. + process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). + rescale_dependency (Callable[..., Optional[RescaleType]]): + color_formula_dependency (Callable[..., Optional[str]]): + colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). + render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). + environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. + supported_tms (morecantile.defaults.TileMatrixSets): TileMatrixSets object holding the supported TileMatrixSets. + templates (Jinja2Templates): Jinja2 templates. add_preview (bool): add `/preview` endpoints. Defaults to True. add_part (bool): add `/bbox` and `/feature` endpoints. Defaults to True. add_viewer (bool): add `/map` endpoints. Defaults to True. @@ -307,9 +312,12 @@ def register_routes(self): self.bounds() self.info() self.statistics() + self.tilesets() self.tile() - self.tilejson() + if self.add_viewer: + self.map_viewer() self.wmts() + self.tilejson() self.point() # Optional Routes @@ -319,9 +327,6 @@ def register_routes(self): if self.add_part: self.part() - if self.add_viewer: - self.map_viewer() - ############################################################################ # /bounds ############################################################################ @@ -525,6 +530,229 @@ def geojson_statistics( return fc.features[0] if isinstance(geojson, Feature) else fc + ############################################################################ + # /tileset + ############################################################################ + def tilesets(self): + """Register OGC tilesets endpoints.""" + + @self.router.get( + "/tiles", + response_model=TileSetList, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + } + } + }, + summary="Retrieve a list of available raster tilesets for the specified dataset.", + ) + async def tileset_list( + request: Request, + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + ): + """Retrieve a list of available raster tilesets for the specified dataset.""" + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(crs or WGS84_CRS), + } + + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in ["crs"] + ] + query_string = f"?{urlencode(qs)}" if qs else "" + + tilesets = [] + for tms in self.supported_tms.list(): + tileset = { + "title": f"tileset tiled using {tms} TileMatrixSet", + "dataType": "map", + "crs": self.supported_tms.get(tms).crs, + "boundingBox": collection_bbox, + "links": [ + { + "href": self.url_for( + request, "tileset", tileMatrixSetId=tms + ) + + query_string, + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tms} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tms, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + }, + ], + } + + try: + tileset["links"].append( + { + "href": str( + request.url_for("tilematrixset", tileMatrixSetId=tms) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + "type": "application/json", + "title": f"Definition of '{tms}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + tilesets.append(tileset) + + data = TileSetList.model_validate({"tilesets": tilesets}) + return data + + @self.router.get( + "/tiles/{tileMatrixSetId}", + response_model=TileSet, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={200: {"content": {"application/json": {}}}}, + summary="Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).", + ) + async def tileset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), + ): + """Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).""" + tms = self.supported_tms.get(tileMatrixSetId) + with rasterio.Env(**env): + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(tms.rasterio_geographic_crs), + } + + tilematrix_limit = [] + for zoom in range(minzoom, maxzoom + 1, 1): + matrix = tms.matrix(zoom) + ulTile = tms.tile(bounds[0], bounds[3], int(matrix.id)) + lrTile = tms.tile(bounds[2], bounds[1], int(matrix.id)) + minx, maxx = (min(ulTile.x, lrTile.x), max(ulTile.x, lrTile.x)) + miny, maxy = (min(ulTile.y, lrTile.y), max(ulTile.y, lrTile.y)) + tilematrix_limit.append( + { + "tileMatrix": matrix.id, + "minTileRow": max(miny, 0), + "maxTileRow": min(maxy, matrix.matrixHeight), + "minTileCol": max(minx, 0), + "maxTileCol": min(maxx, matrix.matrixWidth), + } + ) + + qs = [(key, value) for (key, value) in request.query_params._list] + query_string = f"?{urlencode(qs)}" if qs else "" + + links = [ + { + "href": self.url_for( + request, + "tileset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tileMatrixSetId, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + "templated": True, + }, + ] + try: + links.append( + { + "href": str( + request.url_for( + "tilematrixset", tileMatrixSetId=tileMatrixSetId + ) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + if self.add_viewer: + links.append( + { + "href": self.url_for( + request, + "map_viewer", + tileMatrixSetId=tileMatrixSetId, + ) + + query_string, + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + + data = TileSet.model_validate( + { + "title": f"tileset tiled using {tileMatrixSetId} TileMatrixSet", + "dataType": "map", + "crs": tms.crs, + "boundingBox": collection_bbox, + "links": links, + "tileMatrixSetLimits": tilematrix_limit, + } + ) + + return data + ############################################################################ # /tiles ############################################################################ diff --git a/src/titiler/core/titiler/core/models/OGC.py b/src/titiler/core/titiler/core/models/OGC.py index 4e9310359..87a399e1d 100644 --- a/src/titiler/core/titiler/core/models/OGC.py +++ b/src/titiler/core/titiler/core/models/OGC.py @@ -1,9 +1,10 @@ """OGC models.""" +from datetime import datetime +from typing import Dict, List, Literal, Optional, Set, Union -from typing import List, Optional - -from pydantic import AnyHttpUrl, BaseModel, Field +from morecantile.models import CRSType +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel from typing_extensions import Annotated from titiler.core.resources.enums import MediaType @@ -117,3 +118,615 @@ class Link(BaseModel): length: Optional[int] = None model_config = {"use_enum_values": True} + + +class TimeStamp(RootModel): + """TimeStamp model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-geodata/timeStamp.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + root: Annotated[ + datetime, + Field( + json_schema_extra={ + "description": "This property indicates the time and date when the response was generated using RFC 3339 notation.", + "examples": ["2017-08-17T08:05:32Z"], + } + ), + ] + + +class BoundingBox(BaseModel): + """BoundingBox model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/2DBoundingBox.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + lowerLeft: Annotated[ + List[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + upperRight: Annotated[ + List[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] = None + orderedAxes: Annotated[ + Optional[List[str]], Field(max_length=2, min_length=2) + ] = None + + +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +Type = Literal["array", "boolean", "integer", "null", "number", "object", "string"] + +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +AccessConstraints = Literal[ + "unclassified", "restricted", "confidential", "secret", "topSecret" +] + + +class Properties(BaseModel): + """Properties model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + title: Optional[str] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Implements 'description'", + } + ), + ] = None + type: Optional[Type] = None + enum: Annotated[ + Optional[Set], + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'acceptedValues'", + }, + ), + ] = None + format: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Complements implementation of 'type'", + } + ), + ] = None + contentMediaType: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Implements 'mediaType'", + } + ), + ] = None + maximum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMaximum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + minimum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMinimum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + pattern: Optional[str] = None + maxItems: Annotated[ + Optional[int], + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'upperMultiplicity'", + }, + ), + ] = None + minItems: Annotated[ + Optional[int], + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'lowerMultiplicity'", + }, + ), + ] = 0 + observedProperty: Optional[str] = None + observedPropertyURI: Optional[AnyUrl] = None + uom: Optional[str] = None + uomURI: Optional[AnyUrl] = None + + +class PropertiesSchema(BaseModel): + """PropertiesSchema model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + type: Literal["object"] + required: Annotated[ + Optional[List[str]], + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'multiplicity' by citing property 'name' defined as 'additionalProperties'", + }, + ), + ] = None + properties: Dict[str, Properties] + + +class Style(BaseModel): + """Style model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/style.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "An identifier for this style. Implementation of 'identifier'", + } + ), + ] + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A title for this style", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this style", + } + ), + ] = None + keywords: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "keywords about this style", + } + ), + ] = None + links: Annotated[ + Optional[List[Link]], + Field( + min_length=1, + json_schema_extra={ + "description": "Links to style related resources. Possible link 'rel' values are: 'style' for a URL pointing to the style description, 'styleSpec' for a URL pointing to the specification or standard used to define the style.", + }, + ), + ] = None + + +class GeospatialData(BaseModel): + """Geospatial model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/geospatialData.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Title of this tile matrix set, normally used for display to a human", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile matrix set, normally available for display to a human", + } + ), + ] = None + keywords: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this layer", + } + ), + ] = None + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "Unique identifier of the Layer. Implementation of 'identifier'", + } + ), + ] + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + geometryDimension: Annotated[ + Optional[int], + Field( # type: ignore + ge=0, + le=3, + json_schema_extra={ + "description": "The geometry dimension of the features shown in this layer (0: points, 1: curves, 2: surfaces, 3: solids), unspecified: mixed or unknown", + }, + ), + ] = None + featureType: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Feature type identifier. Only applicable to layers of datatype 'geometries'", + } + ), + ] = None + attribution: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + pointOfContact: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the layer (e.g. e-mail address, a physical address, phone numbers, etc)", + } + ), + ] = None + publisher: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Organization or individual responsible for making the layer available", + } + ), + ] = None + theme: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Category where the layer can be grouped", + } + ), + ] = None + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] = None + epoch: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + minScaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Minimum scale denominator for usage of the layer", + } + ), + ] = None + maxScaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Maximum scale denominator for usage of the layer", + } + ), + ] = None + minCellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Minimum cell size for usage of the layer", + } + ), + ] = None + maxCellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Maximum cell size for usage of the layer", + } + ), + ] = None + maxTileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the minScaleDenominator", + } + ), + ] = None + minTileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the maxScaleDenominator", + } + ), + ] = None + boundingBox: Optional[BoundingBox] = None + created: Optional[TimeStamp] = None + updated: Optional[TimeStamp] = None + style: Optional[Style] = None + geoDataClasses: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "URI identifying a class of data contained in this layer (useful to determine compatibility with styles or processes)", + } + ), + ] = None + propertiesSchema: Optional[PropertiesSchema] = None + links: Annotated[ + Optional[List[Link]], + Field( + min_length=1, + json_schema_extra={ + "description": "Links related to this layer. Possible link 'rel' values are: 'geodata' for a URL pointing to the collection of geospatial data.", + }, + ), + ] = None + + +class TilePoint(BaseModel): + """TilePoint model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tilePoint.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + coordinates: Annotated[List[float], Field(max_length=2, min_length=2)] + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] + tileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the scaleDenominator", + } + ), + ] = None + scaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Scale denominator of the tile matrix selected", + } + ), + ] = None + cellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Cell size of the tile matrix selected", + } + ), + ] = None + + +class TileMatrixLimits(BaseModel): + """ + The limits for an individual tile matrix of a TileSet's TileMatrixSet, as defined in the OGC 2D TileMatrixSet and TileSet Metadata Standard + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileMatrixLimits.yaml + """ + + tileMatrix: str + minTileRow: Annotated[int, Field(ge=0)] + maxTileRow: Annotated[int, Field(ge=0)] + minTileCol: Annotated[int, Field(ge=0)] + maxTileCol: Annotated[int, Field(ge=0)] + + +class TileSet(BaseModel): + """ + TileSet model. + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileSet.yaml + """ + + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A title for this tileset", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile set", + } + ), + ] = None + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + crs: Annotated[CRSType, Field(json_schema_extra={"title": "CRS"})] + tileMatrixSetURI: Annotated[ + Optional[AnyUrl], + Field( + json_schema_extra={ + "description": "Reference to a Tile Matrix Set on an official source for Tile Matrix Sets", + } + ), + ] = None + links: Annotated[ + List[Link], + Field( + json_schema_extra={ + "description": "Links to related resources", + } + ), + ] + tileMatrixSetLimits: Annotated[ + Optional[List[TileMatrixLimits]], + Field( + json_schema_extra={ + "description": "Limits for the TileRow and TileCol values for each TileMatrix in the tileMatrixSet. If missing, there are no limits other that the ones imposed by the TileMatrixSet. If present the TileMatrices listed are limited and the rest not available at all", + } + ), + ] = None + epoch: Annotated[ + Optional[Union[float, int]], + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + layers: Annotated[ + Optional[List[GeospatialData]], + Field(min_length=1), + ] = None + boundingBox: Optional[BoundingBox] = None + centerPoint: Optional[TilePoint] = None + style: Optional[Style] = None + attribution: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + accessConstraints: Annotated[ + Optional[AccessConstraints], + Field( + json_schema_extra={ + "description": "Restrictions on the availability of the Tile Set that the user needs to be aware of before using or redistributing the Tile Set", + } + ), + ] = "unclassified" + keywords: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "keywords about this tileset", + } + ), + ] = None + version: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Version of the Tile Set. Changes if the data behind the tiles has been changed", + } + ), + ] = None + created: Optional[TimeStamp] = None + updated: Optional[TimeStamp] = None + pointOfContact: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the Tile Set", + } + ), + ] = None + mediaTypes: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "Media types available for the tiles", + } + ), + ] = None + + +class TileSetList(BaseModel): + """ + TileSetList model. + + Based on https://docs.ogc.org/is/20-057/20-057.html#toc34 + """ + + tilesets: List[TileSet] diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index ed53cc97f..9d627126a 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from io import BytesIO +import morecantile import numpy from cogeo_mosaic.backends import FileBackend from cogeo_mosaic.mosaic import MosaicJSON @@ -20,6 +21,8 @@ from .conftest import DATA_DIR assets = [os.path.join(DATA_DIR, asset) for asset in ["cog1.tif", "cog2.tif"]] +DEFAULT_TMS = morecantile.tms +NB_DEFAULT_TMS = len(DEFAULT_TMS.list()) @contextmanager @@ -43,7 +46,7 @@ def test_MosaicTilerFactory(): optional_headers=[OptionalHeader.x_assets], router_prefix="mosaic", ) - assert len(mosaic.router.routes) == 16 + assert len(mosaic.router.routes) == 18 app = FastAPI() app.include_router(mosaic.router, prefix="/mosaic") @@ -246,6 +249,25 @@ def test_MosaicTilerFactory(): assert response.status_code == 200 assert response.json() == [] + # OGC Tileset + response = client.get(f"/mosaic/tiles?url={mosaic_file}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/mosaic/tiles/WebMercatorQuad?url={mosaic_file}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # covers only 3 zoom levels + assert len(resp["tileMatrixSetLimits"]) == 3 + @dataclass class BackendParams(DefaultDependency): diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 0ec2cc1d7..379320b77 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -24,6 +24,7 @@ from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response +from starlette.routing import NoMatchFound from starlette.templating import Jinja2Templates from typing_extensions import Annotated @@ -36,7 +37,6 @@ CoordCRSParams, CRSParams, DatasetParams, - DatasetPathParams, DefaultDependency, ImageRenderingParams, RescaleType, @@ -45,6 +45,7 @@ ) from titiler.core.factory import DEFAULT_TEMPLATES, BaseFactory, img_endpoint_params from titiler.core.models.mapbox import TileJSON +from titiler.core.models.OGC import TileSet, TileSetList from titiler.core.resources.enums import ImageType, OptionalHeader from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.core.utils import render_image @@ -63,6 +64,11 @@ def PixelSelectionParams( return PixelSelectionMethod[pixel_selection].value() +def DatasetPathParams(url: Annotated[str, Query(description="Mosaic URL")]) -> str: + """Create dataset path from args""" + return url + + @define(kw_only=True) class MosaicTilerFactory(BaseFactory): """MosaicTiler Factory.""" @@ -120,17 +126,16 @@ def register_routes(self): self.read() self.bounds() self.info() + self.tilesets() self.tile() + if self.add_viewer: + self.map_viewer() self.tilejson() self.wmts() self.point() self.validate() self.assets() - # Optional Routes - if self.add_viewer: - self.map_viewer() - ############################################################################ # /read ############################################################################ @@ -263,6 +268,240 @@ def info_geojson( properties=src_dst.info(), ) + ############################################################################ + # /tileset + ############################################################################ + def tilesets(self): + """Register OGC tilesets endpoints.""" + + @self.router.get( + "/tiles", + response_model=TileSetList, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + } + } + }, + summary="Retrieve a list of available raster tilesets for the specified dataset.", + ) + async def tileset_list( + request: Request, + src_path=Depends(self.path_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + ): + """Retrieve a list of available raster tilesets for the specified dataset.""" + with rasterio.Env(**env): + with self.backend( + src_path, + reader=self.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(crs or WGS84_CRS), + } + + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in ["crs"] + ] + query_string = f"?{urlencode(qs)}" if qs else "" + + tilesets = [] + for tms in self.supported_tms.list(): + tileset = { + "title": f"tileset tiled using {tms} TileMatrixSet", + "dataType": "map", + "crs": self.supported_tms.get(tms).crs, + "boundingBox": collection_bbox, + "links": [ + { + "href": self.url_for( + request, "tileset", tileMatrixSetId=tms + ) + + query_string, + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tms} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tms, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + }, + ], + } + + try: + tileset["links"].append( + { + "href": str( + request.url_for("tilematrixset", tileMatrixSetId=tms) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + "type": "application/json", + "title": f"Definition of '{tms}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + tilesets.append(tileset) + + data = TileSetList.model_validate({"tilesets": tilesets}) + return data + + @self.router.get( + "/tiles/{tileMatrixSetId}", + response_model=TileSet, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={200: {"content": {"application/json": {}}}}, + summary="Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).", + ) + async def tileset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), + backend_params=Depends(self.backend_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), + ): + """Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).""" + tms = self.supported_tms.get(tileMatrixSetId) + with rasterio.Env(**env): + with self.backend( + src_path, + tms=tms, + reader=self.dataset_reader, + reader_options=reader_params.as_dict(), + **backend_params.as_dict(), + ) as src_dst: + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(tms.rasterio_geographic_crs), + } + + tilematrix_limit = [] + for zoom in range(minzoom, maxzoom + 1, 1): + matrix = tms.matrix(zoom) + ulTile = tms.tile(bounds[0], bounds[3], int(matrix.id)) + lrTile = tms.tile(bounds[2], bounds[1], int(matrix.id)) + minx, maxx = (min(ulTile.x, lrTile.x), max(ulTile.x, lrTile.x)) + miny, maxy = (min(ulTile.y, lrTile.y), max(ulTile.y, lrTile.y)) + tilematrix_limit.append( + { + "tileMatrix": matrix.id, + "minTileRow": max(miny, 0), + "maxTileRow": min(maxy, matrix.matrixHeight), + "minTileCol": max(minx, 0), + "maxTileCol": min(maxx, matrix.matrixWidth), + } + ) + + qs = [(key, value) for (key, value) in request.query_params._list] + query_string = f"?{urlencode(qs)}" if qs else "" + + links = [ + { + "href": self.url_for( + request, + "tileset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tileMatrixSetId, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + "templated": True, + }, + ] + try: + links.append( + { + "href": str( + request.url_for( + "tilematrixset", tileMatrixSetId=tileMatrixSetId + ) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + if self.add_viewer: + links.append( + { + "href": self.url_for( + request, + "map_viewer", + tileMatrixSetId=tileMatrixSetId, + ) + + query_string, + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + + data = TileSet.model_validate( + { + "title": f"tileset tiled using {tileMatrixSetId} TileMatrixSet", + "dataType": "map", + "crs": tms.crs, + "boundingBox": collection_bbox, + "links": links, + "tileMatrixSetLimits": tilematrix_limit, + } + ) + + return data + ############################################################################ # /tiles ############################################################################ From cc607c3a41f8e8d77383bb07c0a909e53b3cbab4 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Mon, 4 Nov 2024 10:02:27 +0100 Subject: [PATCH 3/3] add gif mediatype (#1018) * add gif mediatype * update changelog --- CHANGES.md | 2 ++ src/titiler/core/titiler/core/resources/enums.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e6afa4a81..e40236c13 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -86,6 +86,8 @@ * add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints +* add `gif` media type + ### titiler.mosaic * Rename `reader` attribute to `backend` in `MosaicTilerFactory` **breaking change** diff --git a/src/titiler/core/titiler/core/resources/enums.py b/src/titiler/core/titiler/core/resources/enums.py index 42d23246c..52be51fdb 100644 --- a/src/titiler/core/titiler/core/resources/enums.py +++ b/src/titiler/core/titiler/core/resources/enums.py @@ -30,6 +30,7 @@ class MediaType(str, Enum): csv = "text/csv" openapi30_json = "application/vnd.oai.openapi+json;version=3.0" openapi30_yaml = "application/vnd.oai.openapi;version=3.0" + gif = "image/gif" class ImageDriver(str, Enum): @@ -43,6 +44,7 @@ class ImageDriver(str, Enum): webp = "WEBP" jp2 = "JP2OpenJPEG" npy = "NPY" + gif = "GIF" class ImageType(str, Enum):