diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07353d32c..bcd4456e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,11 @@ jobs: python -m pip install -e src/titiler/extensions["test,cogeo,stac"] python -m pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing + - name: Test titiler.xarray + run: | + python -m pip install -e src/titiler/xarray["test"] + python -m pytest src/titiler/xarray --cov=titiler.xarray --cov-report=xml --cov-append --cov-report=term-missing + - name: Test titiler.mosaic run: | python -m pip install -e src/titiler/mosaic["test"] diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 31536e303..a3a2e3db0 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install src/titiler/core src/titiler/extensions["cogeo,stac"] src/titiler/mosaic src/titiler/application + python -m pip install src/titiler/core src/titiler/extensions["cogeo,stac"] src/titiler/xarray src/titiler/mosaic src/titiler/application python -m pip install -r requirements/requirements-docs.txt diff --git a/README.md b/README.md index c38adc3d9..5dc5daadc 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Starting with version `0.3.0`, the `TiTiler` python module has been split into a | Package | Version | Description | ------- | ------- |------------- [**titiler.core**](https://github.com/developmentseed/titiler/tree/main/src/titiler/core) | [![titiler.core](https://img.shields.io/pypi/v/titiler.core?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.core) | The `Core` package contains libraries to help create a dynamic tiler for COG and STAC +[**titiler.xarray**](https://github.com/developmentseed/titiler/tree/main/src/titiler/xarray) | [![titiler.xarray](https://img.shields.io/pypi/v/titiler.xarray?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.xarray) | The `xarray` package contains libraries to help create a dynamic tiler for Zarr/NetCDF datasets [**titiler.extensions**](https://github.com/developmentseed/titiler/tree/main/src/titiler/extensions) | [![titiler.extensions](https://img.shields.io/pypi/v/titiler.extensions?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.extensions) | TiTiler's extensions package. Contains extensions for Tiler Factories. [**titiler.mosaic**](https://github.com/developmentseed/titiler/tree/main/src/titiler/mosaic) | [![titiler.mosaic](https://img.shields.io/pypi/v/titiler.mosaic?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.mosaic) | The `mosaic` package contains libraries to help create a dynamic tiler for MosaicJSON (adds `cogeo-mosaic` requirement) [**titiler.application**](https://github.com/developmentseed/titiler/tree/main/src/titiler/application) | [![titiler.application](https://img.shields.io/pypi/v/titiler.application?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.application) | TiTiler's `demo` package. Contains a FastAPI application with full support of COG, STAC and MosaicJSON @@ -71,6 +72,7 @@ python -m pip install -U pip python -m pip install titiler.{package} # e.g., # python -m pip install titiler.core +# python -m pip install titiler.xarray # python -m pip install titiler.extensions # python -m pip install titiler.mosaic # python -m pip install titiler.application (also installs core, extensions and mosaic) @@ -89,7 +91,7 @@ git clone https://github.com/developmentseed/titiler.git cd titiler python -m pip install -U pip -python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application +python -m pip install -e src/titiler/core -e src/titiler/xarray -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application python -m pip install uvicorn uvicorn titiler.application.main:app --reload @@ -125,6 +127,7 @@ Some options can be set via environment variables, see: https://github.com/tiang src/titiler/ - titiler modules. ├── application/ - Titiler's `Application` package ├── extensions/ - Titiler's `Extensions` package + ├── xarray/ - Titiler's `Xarray` package ├── core/ - Titiler's `Core` package └── mosaic/ - Titiler's `Mosaic` package ``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9d91f4104..05c77b3ba 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -39,7 +39,6 @@ nav: - User Guide: - Intro: "intro.md" - Dynamic Tiling: "dynamic_tiling.md" - - Mosaics: "mosaics.md" - TileMatrixSets: "tile_matrix_sets.md" - Output data format: "output_format.md" @@ -53,6 +52,13 @@ nav: - Rendering: "advanced/rendering.md" # - APIRoute and environment variables: "advanced/APIRoute_and_environment_variables.md" + - Packages: + - titiler.core: "packages/core.md" + - titiler.xarray: "packages/xarray.md" + - titiler.extensions: "packages/extensions.md" + - titiler.mosaic: "packages/mosaic.md" + - titiler.application: "packages/application.md" + - Endpoints documentation: - /cog: "endpoints/cog.md" - /stac: "endpoints/stac.md" @@ -109,6 +115,11 @@ nav: - errors: api/titiler/mosaic/errors.md - models: - responses: api/titiler/mosaic/models/responses.md + - titiler.xarray: + - io: api/titiler/xarray/io.md + - dependencies: api/titiler/xarray/dependencies.md + - extensions: api/titiler/xarray/extensions.md + - factory: api/titiler/xarray/factory.md - Deployment: - Amazon Web Services: diff --git a/docs/src/advanced/endpoints_factories.md b/docs/src/advanced/endpoints_factories.md index 9d5bcdf1a..033186d1e 100644 --- a/docs/src/advanced/endpoints_factories.md +++ b/docs/src/advanced/endpoints_factories.md @@ -7,8 +7,9 @@ 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. +## titiler.core -## BaseFactory +### BaseFactory class: `titiler.core.factory.BaseFactory` @@ -27,7 +28,7 @@ Most **Factories** are built from this [abstract based class](https://docs.pytho - **url_for**: Method to construct endpoint URL - **add_route_dependencies**: Add dependencies to routes. -## TilerFactory +### TilerFactory class: `titiler.core.factory.TilerFactory` @@ -40,7 +41,7 @@ Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reade - **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`. +- **tile_dependency**: Dependency to define `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`. @@ -50,7 +51,7 @@ Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reade - **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: {}`. +- **environment_dependency**: Dependency to define 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`. @@ -97,7 +98,7 @@ app.include_router(cog.router) | `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** -## MultiBaseTilerFactory +### MultiBaseTilerFactory class: `titiler.core.factory.MultiBaseTilerFactory` @@ -143,8 +144,7 @@ app.include_router(stac.router) | `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** - -## MultiBandTilerFactory +### MultiBandTilerFactory class: `titiler.core.factory.MultiBandTilerFactory` @@ -201,56 +201,7 @@ app.include_router(landsat.router) | `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature **Optional** -## MosaicTilerFactory - -class: `titiler.mosaic.factory.MosaicTilerFactory` - -Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/developmentseed/mosaicjson-spec). - -#### Attributes - -- **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` -- **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`. -- **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 - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- -| `GET` | `/` | JSON [MosaicJSON][mosaic_model] | return a MosaicJSON document -| `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` | 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 - - -## TMSFactory +### TMSFactory class: `titiler.core.factory.TMSFactory` @@ -278,7 +229,7 @@ app.include_router(tms.router) | `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON ([TileMatrixSet][tilematrixset]) | retrieve the definition of the specified tiling scheme (tile matrix set) -## AlgorithmFactory +### AlgorithmFactory class: `titiler.core.factory.AlgorithmFactory` @@ -306,7 +257,7 @@ app.include_router(algo.router) | `GET` | `/algorithms/{algorithmId}` | JSON ([Algorithm Metadata][algorithm_metadata]) | retrieve the metadata of the specified algorithm. -## ColorMapFactory +### ColorMapFactory class: `titiler.core.factory.ColorMapFactory` @@ -334,6 +285,121 @@ app.include_router(colormap.router) | `GET` | `/colorMaps/{colorMapId}` | JSON ([colorMap][colormap]) | retrieve the metadata or image of the specified colorMap. +## titiler.mosaic + +### MosaicTilerFactory + +class: `titiler.mosaic.factory.MosaicTilerFactory` + +Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/developmentseed/mosaicjson-spec). + +#### Attributes + +- **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` +- **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 define `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 define 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`. +- **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 + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- +| `GET` | `/` | JSON [MosaicJSON][mosaic_model] | return a MosaicJSON document +| `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` | 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 + +## titiler.xarray + +### TilerFactory + +class: `titiler.xarray.factory.TilerFactory` + +#### Attributes + +- **reader**: Dataset Reader **required**. +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.xarray.dependencies.XarrayParams` +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **dataset_dependency**: Dependency to overwrite `nodata` value and change the `Warp` resamplings. Defaults to `titiler.xarray.dependencies.DatasetParams`. +- **tile_dependency**: Dependency for tile creation options. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **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_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.xarray.dependencies.PartFeatureParams`. +- **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 define 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_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. +- **add_viewer**: . Add `/map` endpoints to the router. Defaults to `True`. + + +```python +from fastapi import FastAPI + +from titiler.xarray.factory import TilerFactory + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +md = TilerFactory( + add_part=True, + add_viewer=True, +) + +# add router endpoint to the main application +app.include_router(md.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds +| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON +| `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` | `/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** + [bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46 [info_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L56-L72 diff --git a/docs/src/api/titiler/xarray/dependencies.md b/docs/src/api/titiler/xarray/dependencies.md new file mode 100644 index 000000000..6bb4bf4a6 --- /dev/null +++ b/docs/src/api/titiler/xarray/dependencies.md @@ -0,0 +1 @@ +::: titiler.xarray.dependencies diff --git a/docs/src/api/titiler/xarray/extensions.md b/docs/src/api/titiler/xarray/extensions.md new file mode 100644 index 000000000..e6b41dc30 --- /dev/null +++ b/docs/src/api/titiler/xarray/extensions.md @@ -0,0 +1 @@ +::: titiler.xarray.extensions diff --git a/docs/src/api/titiler/xarray/factory.md b/docs/src/api/titiler/xarray/factory.md new file mode 100644 index 000000000..74f2363b4 --- /dev/null +++ b/docs/src/api/titiler/xarray/factory.md @@ -0,0 +1 @@ +::: titiler.xarray.factory diff --git a/docs/src/api/titiler/xarray/io.md b/docs/src/api/titiler/xarray/io.md new file mode 100644 index 000000000..37ccfec84 --- /dev/null +++ b/docs/src/api/titiler/xarray/io.md @@ -0,0 +1 @@ +::: titiler.xarray.io diff --git a/docs/src/mosaics.md b/docs/src/mosaics.md deleted file mode 100644 index 1695bca95..000000000 --- a/docs/src/mosaics.md +++ /dev/null @@ -1,16 +0,0 @@ - -[Work in Progress] - -![](img/africa_mosaic.png) - -`Titiler` has native support for reading and creating web map tiles from **MosaicJSON**. - -> MosaicJSON is an open standard for representing metadata about a mosaic of Cloud-Optimized GeoTIFF (COG) files. - -Ref: https://github.com/developmentseed/mosaicjson-spec - - -### Links - -- https://medium.com/devseed/cog-talk-part-2-mosaics-bbbf474e66df -- https://github.com/developmentseed/cogeo-mosaic diff --git a/docs/src/packages/application.md b/docs/src/packages/application.md new file mode 120000 index 000000000..9f698a377 --- /dev/null +++ b/docs/src/packages/application.md @@ -0,0 +1 @@ +../../../src/titiler/application/README.md \ No newline at end of file diff --git a/docs/src/packages/core.md b/docs/src/packages/core.md new file mode 120000 index 000000000..fff7ecdd5 --- /dev/null +++ b/docs/src/packages/core.md @@ -0,0 +1 @@ +../../../src/titiler/core/README.md \ No newline at end of file diff --git a/docs/src/packages/extensions.md b/docs/src/packages/extensions.md new file mode 120000 index 000000000..6fcc9e3d1 --- /dev/null +++ b/docs/src/packages/extensions.md @@ -0,0 +1 @@ +../../../src/titiler/extensions/README.md \ No newline at end of file diff --git a/docs/src/packages/mosaic.md b/docs/src/packages/mosaic.md new file mode 120000 index 000000000..cf87cb31c --- /dev/null +++ b/docs/src/packages/mosaic.md @@ -0,0 +1 @@ +../../../src/titiler/mosaic/README.md \ No newline at end of file diff --git a/docs/src/packages/xarray.md b/docs/src/packages/xarray.md new file mode 120000 index 000000000..dc85e70b9 --- /dev/null +++ b/docs/src/packages/xarray.md @@ -0,0 +1 @@ +../../../src/titiler/xarray/README.md \ No newline at end of file diff --git a/scripts/publish b/scripts/publish index 7e548c6ec..1383cd75f 100755 --- a/scripts/publish +++ b/scripts/publish @@ -2,6 +2,7 @@ SUBPACKAGE_DIRS=( "core" + "xarray" "mosaic" "application" "extensions" diff --git a/src/titiler/application/README.md b/src/titiler/application/README.md index ec06a22cd..166c10dc3 100644 --- a/src/titiler/application/README.md +++ b/src/titiler/application/README.md @@ -6,14 +6,14 @@ ## Installation ```bash -$ python -m pip install -U pip +python -m pip install -U pip # From Pypi -$ python -m pip install titiler.application +python -m pip install titiler.application # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application ``` Launch Application diff --git a/src/titiler/core/README.md b/src/titiler/core/README.md index 1ce51547d..79598062d 100644 --- a/src/titiler/core/README.md +++ b/src/titiler/core/README.md @@ -5,14 +5,14 @@ Core of Titiler's application. Contains blocks to create dynamic tile servers. ## Installation ```bash -$ python -m pip install -U pip +python -m pip install -U pip # From Pypi -$ python -m pip install titiler.core +python -m pip install titiler.core # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && python -m pip install -e src/titiler/core +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core ``` ## How To diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 892337102..faad24749 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1277,7 +1277,7 @@ def bbox_image( ): """Create image from a bbox.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.part( [minx, miny, maxx, maxy], dst_crs=dst_crs, diff --git a/src/titiler/extensions/README.md b/src/titiler/extensions/README.md index c7ce05f70..3451fae87 100644 --- a/src/titiler/extensions/README.md +++ b/src/titiler/extensions/README.md @@ -5,14 +5,14 @@ Extent TiTiler Tiler Factories ## Installation ```bash -$ python -m pip install -U pip +python -m pip install -U pip # From Pypi -$ python -m pip install titiler.extensions +python -m pip install titiler.extensions # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions ``` ## Available extensions diff --git a/src/titiler/mosaic/README.md b/src/titiler/mosaic/README.md index 6c010e8f9..10758ed34 100644 --- a/src/titiler/mosaic/README.md +++ b/src/titiler/mosaic/README.md @@ -1,18 +1,24 @@ ## titiler.mosaic -Adds support for MosaicJSON in Titiler. + + +Adds support for [MosaicJSON](https://github.com/developmentseed/mosaicjson-spec) in Titiler. + +> MosaicJSON is an open standard for representing metadata about a mosaic of Cloud-Optimized GeoTIFF (COG) files. + +Ref: https://github.com/developmentseed/mosaicjson-spec ## Installation ```bash -$ python -m pip install -U pip +python -m pip install -U pip # From Pypi -$ python -m pip install titiler.mosaic +python -m pip install titiler.mosaic # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && python -m pip install -e src/titiler/core -e src/titiler/mosaic +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/mosaic ``` ## How To @@ -23,7 +29,7 @@ from titiler.mosaic.factory import MosaicTilerFactory # Create a FastAPI application app = FastAPI( - description="A lightweight Cloud Optimized GeoTIFF tile server", + description="A Mosaic tile server", ) # Create a set of MosaicJSON endpoints @@ -33,15 +39,13 @@ mosaic = MosaicTilerFactory() app.include_router(mosaic.router, tags=["MosaicJSON"]) ``` -See [titiler.application](../application) for a full example. - ## Package structure ``` titiler/ └── mosaic/ ├── tests/ - Tests suite - └── titiler/mosaic/ - `mosaic` namespace package + └── titiler/mosaic/ - `mosaic` namespace package ├── models/ | └── responses.py - mosaic response models ├── errors.py - cogeo-mosaic known errors diff --git a/src/titiler/xarray/README.md b/src/titiler/xarray/README.md new file mode 100644 index 000000000..f33c2fe61 --- /dev/null +++ b/src/titiler/xarray/README.md @@ -0,0 +1,120 @@ +## titiler.xarray + +Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler. + +## Installation + +```bash +python -m pip install -U pip + +# From Pypi +python -m pip install "titiler.xarray[full]" + +# Or from sources +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray[full]" +``` + +#### Installation options + +Default installation for `titiler.xarray` DOES NOT include `fsspec` or any storage's specific dependencies (e.g `s3fs`) nor `engine` dependencies (`zarr`, `h5netcdf`). This is to ease the customization and deployment of user's applications. If you want to use the default's dataset reader you will need to at least use the `[minimal]` dependencies (e.g `python -m pip install "titiler.xarray[minimal]"`). + +Here is the list of available options: + +- **full**: `zarr`, `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs` +- **minimal**: `zarr`, `h5netcdf`, `fsspec` +- **gcs**: `gcsfs` +- **s3**: `s3fs` +- **http**: `aiohttp` + +## How To + +```python +from fastapi import FastAPI + +from titiler.xarray.extensions import VariablesExtension +from titiler.xarray.factory import TilerFactory + +app = FastAPI( + openapi_url="/api", + docs_url="/api.html", + description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF). + +--- + +**Documentation**: https://developmentseed.org/titiler/ + +**Source Code**: https://github.com/developmentseed/titiler + +--- + """, +) + +md = TilerFactory( + router_prefix="/md", + extensions=[ + VariablesExtension(), + ], +) +app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"]) +``` + +## Package structure + +``` +titiler/ + └── xarray/ + ├── tests/ - Tests suite + └── titiler/xarray/ - `xarray` namespace package + ├── dependencies.py - titiler-xarray dependencies + ├── extentions.py - titiler-xarray extensions + ├── io.py - titiler-xarray Readers + └── factory.py - endpoints factory +``` + +## Custom Dataset Opener + +A default Dataset IO is provided within `titiler.xarray.Reader` class but will require optional dependencies (`fsspec`, `zarr`, `h5netcdf`, ...) to be installed with `python -m pip install "titiler.xarray[full]"`. +Dependencies are optional so the entire package size can be optimized to only include dependencies required by a given application. + +Example: + +**requirements**: +- `titiler.xarray` (base) +- `h5netcdf` + + +```python +from typing import Callable +import attr +from fastapi import FastAPI +from titiler.xarray.io import Reader +from titiler.xarray.extensions import VariablesExtension +from titiler.xarray.factory import TilerFactory + +import xarray +import h5netcdf # noqa + +# Create a simple Custom reader, using `xarray.open_dataset` opener +@attr.s +class CustomReader(Reader): + """Custom io.Reader using xarray.open_dataset opener.""" + # xarray.Dataset options + opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray.open_dataset) + + +# Create FastAPI application +app = FastAPI(openapi_url="/api", docs_url="/api.html") + +# Create custom endpoints with the CustomReader +md = TilerFactory( + reader=CustomReader, + router_prefix="/md", + extensions=[ + # we also want to use the simple opener for the Extension + VariablesExtension(dataset_opener==xarray.open_dataset), + ], +) + +app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"]) +``` diff --git a/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb b/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb new file mode 100644 index 000000000..23e87b7a4 --- /dev/null +++ b/src/titiler/xarray/notebooks/xarray_dataset_cache.ipynb @@ -0,0 +1,168 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add Caching Layer for Xarray Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import pickle\n", + "from typing import Any, Callable, List, Optional\n", + "\n", + "import attr\n", + "import xarray\n", + "from morecantile import TileMatrixSet\n", + "from rio_tiler.constants import WEB_MERCATOR_TMS\n", + "from rio_tiler.io.xarray import XarrayReader\n", + "\n", + "from titiler.xarray.io import xarray_open_dataset, get_variable\n", + "\n", + "from diskcache import Cache\n", + "\n", + "cache_client = Cache()\n", + "\n", + "\n", + "@attr.s\n", + "class CustomReader(XarrayReader):\n", + " \"\"\"Reader: Open Zarr file and access DataArray.\"\"\"\n", + "\n", + " src_path: str = attr.ib()\n", + " variable: str = attr.ib()\n", + "\n", + " # xarray.Dataset options\n", + " opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray_open_dataset)\n", + "\n", + " group: Optional[Any] = attr.ib(default=None)\n", + " decode_times: bool = attr.ib(default=False)\n", + "\n", + " # xarray.DataArray options\n", + " datetime: Optional[str] = attr.ib(default=None)\n", + " drop_dim: Optional[str] = attr.ib(default=None)\n", + "\n", + " tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)\n", + "\n", + " ds: xarray.Dataset = attr.ib(init=False)\n", + " input: xarray.DataArray = attr.ib(init=False)\n", + "\n", + " _dims: List = attr.ib(init=False, factory=list)\n", + "\n", + " def __attrs_post_init__(self):\n", + " \"\"\"Set bounds and CRS.\"\"\"\n", + " ds = None\n", + " # Generate cache key and attempt to fetch the dataset from cache\n", + " cache_key = f\"{self.src_path}_group:{self.group}_time:{self.decode_times}\"\n", + " data_bytes = cache_client.get(cache_key)\n", + " if data_bytes:\n", + " print(f\"Found dataset in Cache {cache_key}\")\n", + " ds = pickle.loads(data_bytes)\n", + "\n", + " self.ds = ds or self.opener(\n", + " self.src_path,\n", + " group=self.group,\n", + " decode_times=self.decode_times,\n", + " )\n", + " if not ds:\n", + " # Serialize the dataset to bytes using pickle\n", + " cache_key = f\"{self.src_path}_group:{self.group}_time:{self.decode_times}\"\n", + " data_bytes = pickle.dumps(self.ds)\n", + " print(f\"Adding dataset in Cache: {cache_key}\")\n", + " cache_client.set(cache_key, data_bytes, tag=\"data\", expire=300)\n", + "\n", + " self.input = get_variable(\n", + " self.ds,\n", + " self.variable,\n", + " datetime=self.datetime,\n", + " drop_dim=self.drop_dim,\n", + " )\n", + " super().__attrs_post_init__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding dataset in Cache: ../tests/fixtures/dataset_2d.nc_group:None_time:False\n", + " Size: 16MB\n", + "Dimensions: (x: 2000, y: 1000)\n", + "Coordinates:\n", + " * x (x) float64 16kB -170.0 -169.8 -169.7 -169.5 ... 169.5 169.7 169.8\n", + " * y (y) float64 8kB -80.0 -79.84 -79.68 -79.52 ... 79.52 79.68 79.84\n", + "Data variables:\n", + " dataset (y, x) float64 16MB ...\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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bHH5/X6de+65ksR1XWdB/fSM+ukZ9XN21FHPeqqfeFtap/F7XAAAqxBcAACrWBtcgUBAK1eu5KLlr0H99Iz66Rn1c3bUUc8Gsn6snJwBAHCXtS0uAICbCC4AgFUILgCAVQguAIBVrAyup59+Wueff77OOeccFRUV6e233051kVLiZz/7mXw+X8wyefLk6PqTJ0+qsrJSo0eP1siRI1VRUXHGj3h6zc6dO3XdddcpPz9fPp9PL730Usx6Y4wefvhh5eXladiwYSopKdHHH38cs83Ro0e1ePFiZWRkKCsrS7fffruOHz+exL0YOGern1tvvfWMz1RZWVnMNl6tn9raWs2aNUujRo1STk6O5s+fr4aGhphtevOdampq0rx58zR8+HDl5OTo3nvvVWdnZzJ3ZcD0po6uvPLKMz5Dd9xxR8w2/a0j64LrL3/5i6qrq7Vy5Uq99957mjFjhkpLS2N+mNIlF110kZqbm6PL66+/Hl1311136a9//as2bNigHTt26NNPP9XChQtTWNqBd+LECc2YMUNPP/10t+tXr16tJ598UmvWrNGuXbs0YsQIlZaW6uTJk9FtFi9erA8++EBbtmzRpk2btHPnTi1btixZuzCgzlY/klRWVhbzmXr++edj1nu1fnbs2KHKykq99dZb2rJlizo6OjR37lydOHEius3ZvlNdXV2aN2+eTp06pTfffFPPPvus1q1bp4cffjgVu5RwvakjSVq6dGnMZ2j16tXRdQmpI2OZSy+91FRWVkYfd3V1mfz8fFNbW5vCUqXGypUrzYwZM7pd19raaoYOHWo2bNgQfe7DDz80kkxdXV2SSphakszGjRujjyORiAkGg+ZXv/pV9LnW1lYTCATM888/b4wxZt++fUaSeeedd6LbvPLKK8bn85n//Oc/SSt7Mny1fowxZsmSJeb666//2te4VD+HDh0yksyOHTuMMb37Tv397383fr/fhEKh6DbPPPOMycjIMO3t7cndgST4ah0ZY8z3vvc985Of/ORrX5OIOrKqxXXq1CnV19erpKQk+pzf71dJSYnq6upSWLLU+fjjj5Wfn68JEyZo8eLFampqkiTV19ero6Mjpq4mT56scePGOVtXBw4cUCgUiqmTzMxMFRUVReukrq5OWVlZuuSSS6LblJSUyO/3a9euXUkvcyps375dOTk5uuCCC7R8+XIdOXIkus6l+mlra5MkZWdnS+rdd6qurk7Tpk1Tbm5udJvS0lKFw2F98MEHSSx9cny1jk577rnnNGbMGE2dOlU1NTX6/PPPo+sSUUdW3WT3s88+U1dXV8wOS1Jubq4++uijFJUqdYqKirRu3TpdcMEFam5u1qpVq3TFFVdo7969CoVCSk9PV1ZWVsxrcnNzFQqFUlPgFDu93919fk6vC4VCysnJiVk/ZMgQZWdnO1FvZWVlWrhwoQoLC7V//3498MADKi8vV11dndLS0pypn0gkojvvvFOXX365pk6dKkm9+k6FQqFuP1+n13lJd3UkSTfddJPGjx+v/Px87dmzR/fff78aGhr04osvSkpMHVkVXIhVXl4e/ff06dNVVFSk8ePH64UXXtCwYcNSWDLYatGiRdF/T5s2TdOnT9fEiRO1fft2zZkzJ4UlS67Kykrt3bs3ZswYsb6ujr483jlt2jTl5eVpzpw52r9/vyZOnJiQv21VV+GYMWOUlpZ2xiyelpYWBYPBFJVq8MjKytK3vvUtNTY2KhgM6tSpU2ptbY3ZxuW6Or3fPX1+gsHgGRN9Ojs7dfToUSfrbcKECRozZowaGxsluVE/VVVV2rRpk1577TWdd9550ed7850KBoPdfr5Or/OKr6uj7hQVFUlSzGeov3VkVXClp6dr5syZ2rp1a/S5SCSirVu3qri4OIUlGxyOHz+u/fv3Ky8vTzNnztTQoUNj6qqhoUFNTU3O1lVhYaGCwWBMnYTDYe3atStaJ8XFxWptbVV9fX10m23btikSiUS/gC755JNPdOTIEeXl5Unydv0YY1RVVaWNGzdq27ZtKiwsjFnfm+9UcXGx/vnPf8aE+5YtW5SRkaEpU6YkZ0cG0NnqqDu7d++WpJjPUL/rqI+TSVLmz3/+swkEAmbdunVm3759ZtmyZSYrKytmhoor7r77brN9+3Zz4MAB88Ybb5iSkhIzZswYc+jQIWOMMXfccYcZN26c2bZtm3n33XdNcXGxKS4uTnGpB9axY8fM+++/b95//30jyfz6178277//vvn3v/9tjDHml7/8pcnKyjIvv/yy2bNnj7n++utNYWGh+eKLL6LvUVZWZr797W+bXbt2mddff91MmjTJ3HjjjanapYTqqX6OHTtm7rnnHlNXV2cOHDhgXn31VfOd73zHTJo0yZw8eTL6Hl6tn+XLl5vMzEyzfft209zcHF0+//zz6DZn+051dnaaqVOnmrlz55rdu3ebzZs3m7Fjx5qamppU7FLCna2OGhsbzSOPPGLeffddc+DAAfPyyy+bCRMmmNmzZ0ffIxF1ZF1wGWPMU089ZcaNG2fS09PNpZdeat56661UFyklbrjhBpOXl2fS09PNueeea2644QbT2NgYXf/FF1+YH//4x+Yb3/iGGT58uFmwYIFpbm5OYYkH3muvvWYknbEsWbLEGPPfKfEPPfSQyc3NNYFAwMyZM8c0NDTEvMeRI0fMjTfeaEaOHGkyMjLMbbfdZo4dO5aCvUm8nurn888/N3PnzjVjx441Q4cONePHjzdLly4946TQq/XTXb1IMmvXro1u05vv1L/+9S9TXl5uhg0bZsaMGWPuvvtu09HRkeS9GRhnq6OmpiYze/Zsk52dbQKBgPnmN79p7r33XtPW1hbzPv2tI37WBABgFavGuAAAILgAAFYhuAAAViG4AABWIbgAAFYhuAAAViG4AABWIbgAAFYhuAAAViG4AABWIbgAAFYhuAAAVvn/DbXKOsc2f2sAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "with CustomReader(\"../tests/fixtures/dataset_2d.nc\", \"dataset\") as src:\n", + " print(src.ds)\n", + " tile = src.tms.tile(src.bounds[0], src.bounds[1], src.minzoom)\n", + " img = src.tile(*tile)\n", + "\n", + "plt.imshow(img.data_as_image())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py39", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/titiler/xarray/pyproject.toml b/src/titiler/xarray/pyproject.toml new file mode 100644 index 000000000..950486d32 --- /dev/null +++ b/src/titiler/xarray/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "titiler.xarray" +description = "Xarray plugin for TiTiler." +readme = "README.md" +requires-python = ">=3.8" +authors = [ + {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, + {name = "Aimee Barciauskas", email = "aimee@developmentseed.com"}, +] +license = {text = "MIT"} +keywords = [ + "TiTiler", + "Xarray", + "Zarr", + "NetCDF", + "HDF", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: GIS", +] +dynamic = ["version"] +dependencies = [ + "titiler.core==0.19.0.dev", + "rio-tiler>=7.2,<8.0", + "xarray", + "rioxarray", +] + +[project.optional-dependencies] +full = [ + "zarr", + "h5netcdf", + "fsspec", + "s3fs", + "aiohttp", + "gcsfs", +] +minimal = [ + "zarr", + "h5netcdf", + "fsspec", +] +gcs = [ + "gcsfs", +] +s3 = [ + "s3fs", +] +http = [ + "aiohttp", +] +test = [ + "pytest", + "pytest-cov", + "pytest-asyncio", + "httpx", + "zarr", + "h5netcdf", + "fsspec", +] + +[project.urls] +Homepage = "https://developmentseed.org/titiler/" +Documentation = "https://developmentseed.org/titiler/" +Issues = "https://github.com/developmentseed/titiler/issues" +Source = "https://github.com/developmentseed/titiler" +Changelog = "https://developmentseed.org/titiler/release-notes/" + +[build-system] +requires = ["pdm-pep517"] +build-backend = "pdm.pep517.api" + +[tool.pdm.version] +source = "file" +path = "titiler/xarray/__init__.py" + +[tool.pdm.build] +includes = ["titiler/xarray"] +excludes = ["tests/", "**/.mypy_cache", "**/.DS_Store"] diff --git a/src/titiler/xarray/tests/conftest.py b/src/titiler/xarray/tests/conftest.py new file mode 100644 index 000000000..dc4af91ae --- /dev/null +++ b/src/titiler/xarray/tests/conftest.py @@ -0,0 +1 @@ +"""titiler.xarray test configuration.""" diff --git a/src/titiler/xarray/tests/fixtures/dataset_2d.nc b/src/titiler/xarray/tests/fixtures/dataset_2d.nc new file mode 100644 index 000000000..2b0b42adc Binary files /dev/null and b/src/titiler/xarray/tests/fixtures/dataset_2d.nc differ diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.nc b/src/titiler/xarray/tests/fixtures/dataset_3d.nc new file mode 100644 index 000000000..c367e452e Binary files /dev/null and b/src/titiler/xarray/tests/fixtures/dataset_3d.nc differ diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zattrs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup new file mode 100644 index 000000000..3b7daf227 --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata new file mode 100644 index 000000000..061ce5cb0 --- /dev/null +++ b/src/titiler/xarray/tests/fixtures/dataset_3d.zarr/.zmetadata @@ -0,0 +1,119 @@ +{ + "metadata": { + ".zattrs": {}, + ".zgroup": { + "zarr_format": 2 + }, + "dataset/.zarray": { + "chunks": [ + 1, + 250, + 500 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": " Info: + """Return dataset's basic info.""" + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + info = src_dst.info().model_dump() + if show_times and "time" in src_dst.input.dims: + times = [str(x.data) for x in src_dst.input.time] + info["count"] = len(times) + info["times"] = times + + return Info(**info) + + @self.router.get( + "/info.geojson", + response_model=InfoGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return dataset's basic info as a GeoJSON feature.", + } + }, + ) + def info_geojson( + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + show_times: Annotated[ + Optional[bool], + Query(description="Show info about the time dimension"), + ] = None, + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + ): + """Return dataset's basic info as a GeoJSON feature.""" + 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) + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + geometry = MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + else: + geometry = Polygon.from_bounds(*bounds) + + info = src_dst.info().model_dump() + if show_times and "time" in src_dst.input.dims: + times = [str(x.data) for x in src_dst.input.time] + info["count"] = len(times) + info["times"] = times + + return Feature( + type="Feature", + bbox=bounds, + geometry=geometry, + properties=info, + ) + + # custom /statistics endpoints (remove /statistics - GET) + def statistics(self): + """add statistics endpoints.""" + + # POST endpoint + @self.router.post( + "/statistics", + response_model=StatisticsGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", + } + }, + ) + def geojson_statistics( + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), + stats_params=Depends(self.stats_dependency), + histogram_params=Depends(self.histogram_dependency), + env=Depends(self.environment_dependency), + ): + """Get Statistics from a geojson feature or featureCollection.""" + fc = geojson + if isinstance(fc, Feature): + fc = FeatureCollection(type="FeatureCollection", features=[geojson]) + + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + for feature in fc: + shape = feature.model_dump(exclude_none=True) + image = src_dst.feature( + shape, + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + # Get the coverage % array + coverage_array = image.get_coverage_array( + shape, + shape_crs=coord_crs or WGS84_CRS, + ) + + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + coverage=coverage_array, + ) + + feature.properties = feature.properties or {} + feature.properties.update({"statistics": stats}) + + return fc.features[0] if isinstance(geojson, Feature) else fc diff --git a/src/titiler/xarray/titiler/xarray/io.py b/src/titiler/xarray/titiler/xarray/io.py new file mode 100644 index 000000000..370fa5e97 --- /dev/null +++ b/src/titiler/xarray/titiler/xarray/io.py @@ -0,0 +1,293 @@ +"""titiler.xarray.io""" + +from typing import Any, Callable, Dict, List, Optional +from urllib.parse import urlparse + +import attr +import numpy +import xarray +from morecantile import TileMatrixSet +from rio_tiler.constants import WEB_MERCATOR_TMS +from rio_tiler.io.xarray import XarrayReader + + +def xarray_open_dataset( # noqa: C901 + src_path: str, + group: Optional[str] = None, + decode_times: bool = True, +) -> xarray.Dataset: + """Open Xarray dataset with fsspec. + + Args: + src_path (str): dataset path. + group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str. + decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers. + + Returns: + xarray.Dataset + + """ + import fsspec # noqa + + try: + import gcsfs + except ImportError: # pragma: nocover + gcsfs = None # type: ignore + + try: + import s3fs + except ImportError: # pragma: nocover + s3fs = None # type: ignore + + try: + import aiohttp + except ImportError: # pragma: nocover + aiohttp = None # type: ignore + + try: + import h5netcdf + except ImportError: # pragma: nocover + h5netcdf = None # type: ignore + + try: + import zarr + except ImportError: # pragma: nocover + zarr = None # type: ignore + + parsed = urlparse(src_path) + protocol = parsed.scheme or "file" + + if any(src_path.lower().endswith(ext) for ext in [".nc", ".nc4"]): + assert ( + h5netcdf is not None + ), "'h5netcdf' must be installed to read NetCDF dataset" + + xr_engine = "h5netcdf" + + else: + assert zarr is not None, "'zarr' must be installed to read Zarr dataset" + + xr_engine = "zarr" + + if protocol in ["", "file"]: + filesystem = fsspec.filesystem(protocol) # type: ignore + file_handler = ( + filesystem.open(src_path) + if xr_engine == "h5netcdf" + else filesystem.get_mapper(src_path) + ) + + elif protocol == "s3": + assert ( + s3fs is not None + ), "'aiohttp' must be installed to read dataset stored online" + + s3_filesystem = s3fs.S3FileSystem() + file_handler = ( + s3_filesystem.open(src_path) + if xr_engine == "h5netcdf" + else s3fs.S3Map(root=src_path, s3=s3_filesystem) + ) + + elif protocol == "gs": + assert ( + gcsfs is not None + ), "'gcsfs' must be installed to read dataset stored in Google Cloud Storage" + + gcs_filesystem = gcsfs.GCSFileSystem() + file_handler = ( + gcs_filesystem.open(src_path) + if xr_engine == "h5netcdf" + else gcs_filesystem.get_mapper(root=src_path) + ) + + elif protocol in ["http", "https"]: + assert ( + aiohttp is not None + ), "'aiohttp' must be installed to read dataset stored online" + + filesystem = fsspec.filesystem(protocol) # type: ignore + file_handler = ( + filesystem.open(src_path) + if xr_engine == "h5netcdf" + else filesystem.get_mapper(src_path) + ) + + else: + raise ValueError(f"Unsupported protocol: {protocol}, for {src_path}") + + # Arguments for xarray.open_dataset + # Default args + xr_open_args: Dict[str, Any] = { + "decode_coords": "all", + "decode_times": decode_times, + } + + # Argument if we're opening a datatree + if group is not None: + xr_open_args["group"] = group + + # NetCDF arguments + if xr_engine == "h5netcdf": + xr_open_args.update( + { + "engine": "h5netcdf", + "lock": False, + } + ) + + ds = xarray.open_dataset(file_handler, **xr_open_args) + + # Fallback to Zarr + else: + ds = xarray.open_zarr(file_handler, **xr_open_args) + + return ds + + +def _arrange_dims(da: xarray.DataArray) -> xarray.DataArray: + """Arrange coordinates and time dimensions. + + An rioxarray.exceptions.InvalidDimensionOrder error is raised if the coordinates are not in the correct order time, y, and x. + See: https://github.com/corteva/rioxarray/discussions/674 + + We conform to using x and y as the spatial dimension names.. + + """ + if "x" not in da.dims and "y" not in da.dims: + try: + latitude_var_name = next( + name + for name in ["lat", "latitude", "LAT", "LATITUDE", "Lat"] + if name in da.dims + ) + longitude_var_name = next( + name + for name in ["lon", "longitude", "LON", "LONGITUDE", "Lon"] + if name in da.dims + ) + except StopIteration as e: + raise ValueError(f"Couldn't find X/Y dimensions in {da.dims}") from e + + da = da.rename({latitude_var_name: "y", longitude_var_name: "x"}) + + if "TIME" in da.dims: + da = da.rename({"TIME": "time"}) + + if extra_dims := [d for d in da.dims if d not in ["x", "y"]]: + da = da.transpose(*extra_dims, "y", "x") + else: + da = da.transpose("y", "x") + + # If min/max values are stored in `valid_range` we add them in `valid_min/valid_max` + vmin, vmax = da.attrs.get("valid_min"), da.attrs.get("valid_max") + if "valid_range" in da.attrs and not (vmin is not None and vmax is not None): + valid_range = da.attrs.get("valid_range") + da.attrs.update({"valid_min": valid_range[0], "valid_max": valid_range[1]}) + + return da + + +def get_variable( + ds: xarray.Dataset, + variable: str, + datetime: Optional[str] = None, + drop_dim: Optional[str] = None, +) -> xarray.DataArray: + """Get Xarray variable as DataArray. + + Args: + ds (xarray.Dataset): Xarray Dataset. + variable (str): Variable to extract from the Dataset. + datetime (str, optional): datetime to select from the DataArray. + drop_dim (str, optional): DataArray dimension to drop in form of `{dimension}={value}`. + + Returns: + xarray.DataArray: 2D or 3D DataArray. + + """ + da = ds[variable] + + if drop_dim: + dim_to_drop, dim_val = drop_dim.split("=") + da = da.sel({dim_to_drop: dim_val}).drop_vars(dim_to_drop) + + da = _arrange_dims(da) + + # Make sure we have a valid CRS + crs = da.rio.crs or "epsg:4326" + da = da.rio.write_crs(crs) + + if crs == "epsg:4326" and (da.x > 180).any(): + # Adjust the longitude coordinates to the -180 to 180 range + da = da.assign_coords(x=(da.x + 180) % 360 - 180) + + # Sort the dataset by the updated longitude coordinates + da = da.sortby(da.x) + + # TODO: Technically we don't have to select the first time, rio-tiler should handle 3D dataset + if "time" in da.dims: + if datetime: + # TODO: handle time interval + time_as_str = datetime.split("T")[0] + if da["time"].dtype == "O": + da["time"] = da["time"].astype("datetime64[ns]") + + da = da.sel( + time=numpy.array(time_as_str, dtype=numpy.datetime64), method="nearest" + ) + else: + da = da.isel(time=0) + + assert len(da.dims) in [2, 3], "titiler.xarray can only work with 2D or 3D dataset" + + return da + + +@attr.s +class Reader(XarrayReader): + """Reader: Open Zarr file and access DataArray.""" + + src_path: str = attr.ib() + variable: str = attr.ib() + + # xarray.Dataset options + opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray_open_dataset) + + group: Optional[str] = attr.ib(default=None) + decode_times: bool = attr.ib(default=True) + + # xarray.DataArray options + datetime: Optional[str] = attr.ib(default=None) + drop_dim: Optional[str] = attr.ib(default=None) + + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) + + ds: xarray.Dataset = attr.ib(init=False) + input: xarray.DataArray = attr.ib(init=False) + + _dims: List = attr.ib(init=False, factory=list) + + def __attrs_post_init__(self): + """Set bounds and CRS.""" + self.ds = self.opener( + self.src_path, + group=self.group, + decode_times=self.decode_times, + ) + + self.input = get_variable( + self.ds, + self.variable, + datetime=self.datetime, + drop_dim=self.drop_dim, + ) + super().__attrs_post_init__() + + def close(self): + """Close xarray dataset.""" + self.ds.close() + + def __exit__(self, exc_type, exc_value, traceback): + """Support using with Context Managers.""" + self.close() diff --git a/src/titiler/xarray/titiler/xarray/py.typed b/src/titiler/xarray/titiler/xarray/py.typed new file mode 100644 index 000000000..e69de29bb