titiler-xarray 0.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- titiler.xarray-0.19.0/PKG-INFO +153 -0
- titiler.xarray-0.19.0/README.md +120 -0
- titiler.xarray-0.19.0/pyproject.toml +100 -0
- titiler.xarray-0.19.0/titiler/xarray/__init__.py +3 -0
- titiler.xarray-0.19.0/titiler/xarray/dependencies.py +124 -0
- titiler.xarray-0.19.0/titiler/xarray/extensions.py +38 -0
- titiler.xarray-0.19.0/titiler/xarray/factory.py +212 -0
- titiler.xarray-0.19.0/titiler/xarray/io.py +293 -0
- titiler.xarray-0.19.0/titiler/xarray/py.typed +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: titiler.xarray
|
|
3
|
+
Version: 0.19.0
|
|
4
|
+
Summary: Xarray plugin for TiTiler.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: TiTiler,Xarray,Zarr,NetCDF,HDF
|
|
7
|
+
Author-email: Vincent Sarago <vincent@developmentseed.com>,Aimee Barciauskas <aimee@developmentseed.com>
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
20
|
+
Provides-Extra: full
|
|
21
|
+
Provides-Extra: gcs
|
|
22
|
+
Provides-Extra: http
|
|
23
|
+
Provides-Extra: minimal
|
|
24
|
+
Provides-Extra: s3
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Project-URL: Changelog, https://developmentseed.org/titiler/release-notes/
|
|
27
|
+
Project-URL: Documentation, https://developmentseed.org/titiler/
|
|
28
|
+
Project-URL: Homepage, https://developmentseed.org/titiler/
|
|
29
|
+
Project-URL: Issues, https://github.com/developmentseed/titiler/issues
|
|
30
|
+
Project-URL: Source, https://github.com/developmentseed/titiler
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
## titiler.xarray
|
|
34
|
+
|
|
35
|
+
Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python -m pip install -U pip
|
|
41
|
+
|
|
42
|
+
# From Pypi
|
|
43
|
+
python -m pip install "titiler.xarray[full]"
|
|
44
|
+
|
|
45
|
+
# Or from sources
|
|
46
|
+
git clone https://github.com/developmentseed/titiler.git
|
|
47
|
+
cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray[full]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Installation options
|
|
51
|
+
|
|
52
|
+
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]"`).
|
|
53
|
+
|
|
54
|
+
Here is the list of available options:
|
|
55
|
+
|
|
56
|
+
- **full**: `zarr`, `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs`
|
|
57
|
+
- **minimal**: `zarr`, `h5netcdf`, `fsspec`
|
|
58
|
+
- **gcs**: `gcsfs`
|
|
59
|
+
- **s3**: `s3fs`
|
|
60
|
+
- **http**: `aiohttp`
|
|
61
|
+
|
|
62
|
+
## How To
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from fastapi import FastAPI
|
|
66
|
+
|
|
67
|
+
from titiler.xarray.extensions import VariablesExtension
|
|
68
|
+
from titiler.xarray.factory import TilerFactory
|
|
69
|
+
|
|
70
|
+
app = FastAPI(
|
|
71
|
+
openapi_url="/api",
|
|
72
|
+
docs_url="/api.html",
|
|
73
|
+
description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
**Documentation**: <a href="https://developmentseed.org/titiler/" target="_blank">https://developmentseed.org/titiler/</a>
|
|
78
|
+
|
|
79
|
+
**Source Code**: <a href="https://github.com/developmentseed/titiler" target="_blank">https://github.com/developmentseed/titiler</a>
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
""",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
md = TilerFactory(
|
|
86
|
+
router_prefix="/md",
|
|
87
|
+
extensions=[
|
|
88
|
+
VariablesExtension(),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Package structure
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
titiler/
|
|
98
|
+
└── xarray/
|
|
99
|
+
├── tests/ - Tests suite
|
|
100
|
+
└── titiler/xarray/ - `xarray` namespace package
|
|
101
|
+
├── dependencies.py - titiler-xarray dependencies
|
|
102
|
+
├── extentions.py - titiler-xarray extensions
|
|
103
|
+
├── io.py - titiler-xarray Readers
|
|
104
|
+
└── factory.py - endpoints factory
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Custom Dataset Opener
|
|
108
|
+
|
|
109
|
+
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]"`.
|
|
110
|
+
Dependencies are optional so the entire package size can be optimized to only include dependencies required by a given application.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
|
|
114
|
+
**requirements**:
|
|
115
|
+
- `titiler.xarray` (base)
|
|
116
|
+
- `h5netcdf`
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from typing import Callable
|
|
121
|
+
import attr
|
|
122
|
+
from fastapi import FastAPI
|
|
123
|
+
from titiler.xarray.io import Reader
|
|
124
|
+
from titiler.xarray.extensions import VariablesExtension
|
|
125
|
+
from titiler.xarray.factory import TilerFactory
|
|
126
|
+
|
|
127
|
+
import xarray
|
|
128
|
+
import h5netcdf # noqa
|
|
129
|
+
|
|
130
|
+
# Create a simple Custom reader, using `xarray.open_dataset` opener
|
|
131
|
+
@attr.s
|
|
132
|
+
class CustomReader(Reader):
|
|
133
|
+
"""Custom io.Reader using xarray.open_dataset opener."""
|
|
134
|
+
# xarray.Dataset options
|
|
135
|
+
opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray.open_dataset)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Create FastAPI application
|
|
139
|
+
app = FastAPI(openapi_url="/api", docs_url="/api.html")
|
|
140
|
+
|
|
141
|
+
# Create custom endpoints with the CustomReader
|
|
142
|
+
md = TilerFactory(
|
|
143
|
+
reader=CustomReader,
|
|
144
|
+
router_prefix="/md",
|
|
145
|
+
extensions=[
|
|
146
|
+
# we also want to use the simple opener for the Extension
|
|
147
|
+
VariablesExtension(dataset_opener==xarray.open_dataset),
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
|
|
152
|
+
```
|
|
153
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
## titiler.xarray
|
|
2
|
+
|
|
3
|
+
Adds support for Xarray Dataset (NetCDF/Zarr) in Titiler.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -U pip
|
|
9
|
+
|
|
10
|
+
# From Pypi
|
|
11
|
+
python -m pip install "titiler.xarray[full]"
|
|
12
|
+
|
|
13
|
+
# Or from sources
|
|
14
|
+
git clone https://github.com/developmentseed/titiler.git
|
|
15
|
+
cd titiler && python -m pip install -e src/titiler/core -e "src/titiler/xarray[full]"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### Installation options
|
|
19
|
+
|
|
20
|
+
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]"`).
|
|
21
|
+
|
|
22
|
+
Here is the list of available options:
|
|
23
|
+
|
|
24
|
+
- **full**: `zarr`, `h5netcdf`, `fsspec`, `s3fs`, `aiohttp`, `gcsfs`
|
|
25
|
+
- **minimal**: `zarr`, `h5netcdf`, `fsspec`
|
|
26
|
+
- **gcs**: `gcsfs`
|
|
27
|
+
- **s3**: `s3fs`
|
|
28
|
+
- **http**: `aiohttp`
|
|
29
|
+
|
|
30
|
+
## How To
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from fastapi import FastAPI
|
|
34
|
+
|
|
35
|
+
from titiler.xarray.extensions import VariablesExtension
|
|
36
|
+
from titiler.xarray.factory import TilerFactory
|
|
37
|
+
|
|
38
|
+
app = FastAPI(
|
|
39
|
+
openapi_url="/api",
|
|
40
|
+
docs_url="/api.html",
|
|
41
|
+
description="""Xarray based tiles server for MultiDimensional dataset (Zarr/NetCDF).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
**Documentation**: <a href="https://developmentseed.org/titiler/" target="_blank">https://developmentseed.org/titiler/</a>
|
|
46
|
+
|
|
47
|
+
**Source Code**: <a href="https://github.com/developmentseed/titiler" target="_blank">https://github.com/developmentseed/titiler</a>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
""",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
md = TilerFactory(
|
|
54
|
+
router_prefix="/md",
|
|
55
|
+
extensions=[
|
|
56
|
+
VariablesExtension(),
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Package structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
titiler/
|
|
66
|
+
└── xarray/
|
|
67
|
+
├── tests/ - Tests suite
|
|
68
|
+
└── titiler/xarray/ - `xarray` namespace package
|
|
69
|
+
├── dependencies.py - titiler-xarray dependencies
|
|
70
|
+
├── extentions.py - titiler-xarray extensions
|
|
71
|
+
├── io.py - titiler-xarray Readers
|
|
72
|
+
└── factory.py - endpoints factory
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Custom Dataset Opener
|
|
76
|
+
|
|
77
|
+
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]"`.
|
|
78
|
+
Dependencies are optional so the entire package size can be optimized to only include dependencies required by a given application.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
|
|
82
|
+
**requirements**:
|
|
83
|
+
- `titiler.xarray` (base)
|
|
84
|
+
- `h5netcdf`
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from typing import Callable
|
|
89
|
+
import attr
|
|
90
|
+
from fastapi import FastAPI
|
|
91
|
+
from titiler.xarray.io import Reader
|
|
92
|
+
from titiler.xarray.extensions import VariablesExtension
|
|
93
|
+
from titiler.xarray.factory import TilerFactory
|
|
94
|
+
|
|
95
|
+
import xarray
|
|
96
|
+
import h5netcdf # noqa
|
|
97
|
+
|
|
98
|
+
# Create a simple Custom reader, using `xarray.open_dataset` opener
|
|
99
|
+
@attr.s
|
|
100
|
+
class CustomReader(Reader):
|
|
101
|
+
"""Custom io.Reader using xarray.open_dataset opener."""
|
|
102
|
+
# xarray.Dataset options
|
|
103
|
+
opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray.open_dataset)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Create FastAPI application
|
|
107
|
+
app = FastAPI(openapi_url="/api", docs_url="/api.html")
|
|
108
|
+
|
|
109
|
+
# Create custom endpoints with the CustomReader
|
|
110
|
+
md = TilerFactory(
|
|
111
|
+
reader=CustomReader,
|
|
112
|
+
router_prefix="/md",
|
|
113
|
+
extensions=[
|
|
114
|
+
# we also want to use the simple opener for the Extension
|
|
115
|
+
VariablesExtension(dataset_opener==xarray.open_dataset),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
|
|
120
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "titiler.xarray"
|
|
3
|
+
description = "Xarray plugin for TiTiler."
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.8"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Vincent Sarago", email = "vincent@developmentseed.com" },
|
|
8
|
+
{ name = "Aimee Barciauskas", email = "aimee@developmentseed.com" },
|
|
9
|
+
]
|
|
10
|
+
keywords = [
|
|
11
|
+
"TiTiler",
|
|
12
|
+
"Xarray",
|
|
13
|
+
"Zarr",
|
|
14
|
+
"NetCDF",
|
|
15
|
+
"HDF",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Information Technology",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
29
|
+
]
|
|
30
|
+
dynamic = []
|
|
31
|
+
dependencies = [
|
|
32
|
+
"titiler.core==0.19.0",
|
|
33
|
+
"rio-tiler>=7.2,<8.0",
|
|
34
|
+
"xarray",
|
|
35
|
+
"rioxarray",
|
|
36
|
+
]
|
|
37
|
+
version = "0.19.0"
|
|
38
|
+
|
|
39
|
+
[project.license]
|
|
40
|
+
text = "MIT"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
full = [
|
|
44
|
+
"zarr",
|
|
45
|
+
"h5netcdf",
|
|
46
|
+
"fsspec",
|
|
47
|
+
"s3fs",
|
|
48
|
+
"aiohttp",
|
|
49
|
+
"gcsfs",
|
|
50
|
+
]
|
|
51
|
+
minimal = [
|
|
52
|
+
"zarr",
|
|
53
|
+
"h5netcdf",
|
|
54
|
+
"fsspec",
|
|
55
|
+
]
|
|
56
|
+
gcs = [
|
|
57
|
+
"gcsfs",
|
|
58
|
+
]
|
|
59
|
+
s3 = [
|
|
60
|
+
"s3fs",
|
|
61
|
+
]
|
|
62
|
+
http = [
|
|
63
|
+
"aiohttp",
|
|
64
|
+
]
|
|
65
|
+
test = [
|
|
66
|
+
"pytest",
|
|
67
|
+
"pytest-cov",
|
|
68
|
+
"pytest-asyncio",
|
|
69
|
+
"httpx",
|
|
70
|
+
"zarr",
|
|
71
|
+
"h5netcdf",
|
|
72
|
+
"fsspec",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[project.urls]
|
|
76
|
+
Homepage = "https://developmentseed.org/titiler/"
|
|
77
|
+
Documentation = "https://developmentseed.org/titiler/"
|
|
78
|
+
Issues = "https://github.com/developmentseed/titiler/issues"
|
|
79
|
+
Source = "https://github.com/developmentseed/titiler"
|
|
80
|
+
Changelog = "https://developmentseed.org/titiler/release-notes/"
|
|
81
|
+
|
|
82
|
+
[build-system]
|
|
83
|
+
requires = [
|
|
84
|
+
"pdm-pep517",
|
|
85
|
+
]
|
|
86
|
+
build-backend = "pdm.pep517.api"
|
|
87
|
+
|
|
88
|
+
[tool.pdm.version]
|
|
89
|
+
source = "file"
|
|
90
|
+
path = "titiler/xarray/__init__.py"
|
|
91
|
+
|
|
92
|
+
[tool.pdm.build]
|
|
93
|
+
includes = [
|
|
94
|
+
"titiler/xarray",
|
|
95
|
+
]
|
|
96
|
+
excludes = [
|
|
97
|
+
"tests/",
|
|
98
|
+
"**/.mypy_cache",
|
|
99
|
+
"**/.DS_Store",
|
|
100
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""titiler.xarray dependencies."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
import numpy
|
|
7
|
+
from fastapi import Query
|
|
8
|
+
from rio_tiler.types import RIOResampling, WarpResampling
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from titiler.core.dependencies import DefaultDependency
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class XarrayIOParams(DefaultDependency):
|
|
16
|
+
"""Dataset IO Options."""
|
|
17
|
+
|
|
18
|
+
group: Annotated[
|
|
19
|
+
Optional[str],
|
|
20
|
+
Query(
|
|
21
|
+
description="Select a specific zarr group from a zarr hierarchy. Could be associated with a zoom level or dataset."
|
|
22
|
+
),
|
|
23
|
+
] = None
|
|
24
|
+
|
|
25
|
+
decode_times: Annotated[
|
|
26
|
+
Optional[bool],
|
|
27
|
+
Query(
|
|
28
|
+
title="decode_times",
|
|
29
|
+
description="Whether to decode times",
|
|
30
|
+
),
|
|
31
|
+
] = None
|
|
32
|
+
|
|
33
|
+
# cache_client
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class XarrayDsParams(DefaultDependency):
|
|
38
|
+
"""Xarray Dataset Options."""
|
|
39
|
+
|
|
40
|
+
variable: Annotated[str, Query(description="Xarray Variable name")]
|
|
41
|
+
|
|
42
|
+
drop_dim: Annotated[
|
|
43
|
+
Optional[str],
|
|
44
|
+
Query(description="Dimension to drop"),
|
|
45
|
+
] = None
|
|
46
|
+
|
|
47
|
+
datetime: Annotated[
|
|
48
|
+
Optional[str], Query(description="Slice of time to read (if available)")
|
|
49
|
+
] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class XarrayParams(XarrayIOParams, XarrayDsParams):
|
|
54
|
+
"""Xarray Reader dependency."""
|
|
55
|
+
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class CompatXarrayParams(XarrayIOParams):
|
|
61
|
+
"""Custom XarrayParams endpoints.
|
|
62
|
+
|
|
63
|
+
This Dependency aims to be used in a tiler where both GDAL/Xarray dataset would be supported.
|
|
64
|
+
By default `variable` won't be required but when using an Xarray dataset,
|
|
65
|
+
it would fail without the variable query-parameter set.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
variable: Annotated[Optional[str], Query(description="Xarray Variable name")] = None
|
|
69
|
+
|
|
70
|
+
drop_dim: Annotated[
|
|
71
|
+
Optional[str],
|
|
72
|
+
Query(description="Dimension to drop"),
|
|
73
|
+
] = None
|
|
74
|
+
|
|
75
|
+
datetime: Annotated[
|
|
76
|
+
Optional[str], Query(description="Slice of time to read (if available)")
|
|
77
|
+
] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class DatasetParams(DefaultDependency):
|
|
82
|
+
"""Low level WarpedVRT Optional parameters."""
|
|
83
|
+
|
|
84
|
+
nodata: Annotated[
|
|
85
|
+
Optional[Union[str, int, float]],
|
|
86
|
+
Query(
|
|
87
|
+
title="Nodata value",
|
|
88
|
+
description="Overwrite internal Nodata value",
|
|
89
|
+
),
|
|
90
|
+
] = None
|
|
91
|
+
reproject_method: Annotated[
|
|
92
|
+
Optional[WarpResampling],
|
|
93
|
+
Query(
|
|
94
|
+
alias="reproject",
|
|
95
|
+
description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.",
|
|
96
|
+
),
|
|
97
|
+
] = None
|
|
98
|
+
|
|
99
|
+
def __post_init__(self):
|
|
100
|
+
"""Post Init."""
|
|
101
|
+
if self.nodata is not None:
|
|
102
|
+
self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Custom PartFeatureParams which add `resampling`
|
|
106
|
+
@dataclass
|
|
107
|
+
class PartFeatureParams(DefaultDependency):
|
|
108
|
+
"""Common parameters for bbox and feature."""
|
|
109
|
+
|
|
110
|
+
max_size: Annotated[Optional[int], "Maximum image size to read onto."] = None
|
|
111
|
+
height: Annotated[Optional[int], "Force output image height."] = None
|
|
112
|
+
width: Annotated[Optional[int], "Force output image width."] = None
|
|
113
|
+
resampling_method: Annotated[
|
|
114
|
+
Optional[RIOResampling],
|
|
115
|
+
Query(
|
|
116
|
+
alias="resampling",
|
|
117
|
+
description="RasterIO resampling algorithm. Defaults to `nearest`.",
|
|
118
|
+
),
|
|
119
|
+
] = None
|
|
120
|
+
|
|
121
|
+
def __post_init__(self):
|
|
122
|
+
"""Post Init."""
|
|
123
|
+
if self.width and self.height:
|
|
124
|
+
self.max_size = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""titiler.xarray Extensions."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, List, Type
|
|
4
|
+
|
|
5
|
+
import xarray
|
|
6
|
+
from attrs import define
|
|
7
|
+
from fastapi import Depends
|
|
8
|
+
|
|
9
|
+
from titiler.core.dependencies import DefaultDependency
|
|
10
|
+
from titiler.core.factory import FactoryExtension
|
|
11
|
+
from titiler.xarray.dependencies import XarrayIOParams
|
|
12
|
+
from titiler.xarray.factory import TilerFactory
|
|
13
|
+
from titiler.xarray.io import xarray_open_dataset
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@define
|
|
17
|
+
class VariablesExtension(FactoryExtension):
|
|
18
|
+
"""Add /variables endpoint to a Xarray TilerFactory."""
|
|
19
|
+
|
|
20
|
+
# Custom dependency for /variables
|
|
21
|
+
io_dependency: Type[DefaultDependency] = XarrayIOParams
|
|
22
|
+
dataset_opener: Callable[..., xarray.Dataset] = xarray_open_dataset
|
|
23
|
+
|
|
24
|
+
def register(self, factory: TilerFactory):
|
|
25
|
+
"""Register endpoint to the tiler factory."""
|
|
26
|
+
|
|
27
|
+
@factory.router.get(
|
|
28
|
+
"/variables",
|
|
29
|
+
response_model=List[str],
|
|
30
|
+
responses={200: {"description": "Return Xarray Dataset variables."}},
|
|
31
|
+
)
|
|
32
|
+
def variables(
|
|
33
|
+
src_path=Depends(factory.path_dependency),
|
|
34
|
+
io_params=Depends(self.io_dependency),
|
|
35
|
+
):
|
|
36
|
+
"""return available variables."""
|
|
37
|
+
with self.dataset_opener(src_path, **io_params.as_dict()) as ds:
|
|
38
|
+
return list(ds.data_vars) # type: ignore
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""TiTiler.xarray factory."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
import rasterio
|
|
6
|
+
from attrs import define, field
|
|
7
|
+
from fastapi import Body, Depends, Query
|
|
8
|
+
from geojson_pydantic.features import Feature, FeatureCollection
|
|
9
|
+
from geojson_pydantic.geometries import MultiPolygon, Polygon
|
|
10
|
+
from rio_tiler.constants import WGS84_CRS
|
|
11
|
+
from rio_tiler.models import Info
|
|
12
|
+
from typing_extensions import Annotated
|
|
13
|
+
|
|
14
|
+
from titiler.core.dependencies import (
|
|
15
|
+
CoordCRSParams,
|
|
16
|
+
CRSParams,
|
|
17
|
+
DatasetPathParams,
|
|
18
|
+
DefaultDependency,
|
|
19
|
+
DstCRSParams,
|
|
20
|
+
HistogramParams,
|
|
21
|
+
StatisticsParams,
|
|
22
|
+
)
|
|
23
|
+
from titiler.core.factory import TilerFactory as BaseTilerFactory
|
|
24
|
+
from titiler.core.models.responses import InfoGeoJSON, StatisticsGeoJSON
|
|
25
|
+
from titiler.core.resources.responses import GeoJSONResponse, JSONResponse
|
|
26
|
+
from titiler.xarray.dependencies import DatasetParams, PartFeatureParams, XarrayParams
|
|
27
|
+
from titiler.xarray.io import Reader
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@define(kw_only=True)
|
|
31
|
+
class TilerFactory(BaseTilerFactory):
|
|
32
|
+
"""Xarray Tiler Factory."""
|
|
33
|
+
|
|
34
|
+
reader: Type[Reader] = Reader
|
|
35
|
+
|
|
36
|
+
path_dependency: Callable[..., str] = DatasetPathParams
|
|
37
|
+
|
|
38
|
+
reader_dependency: Type[DefaultDependency] = XarrayParams
|
|
39
|
+
|
|
40
|
+
# Indexes/Expression Dependencies (Not layer dependencies for Xarray)
|
|
41
|
+
layer_dependency: Type[DefaultDependency] = DefaultDependency
|
|
42
|
+
|
|
43
|
+
# Dataset Options (nodata, reproject)
|
|
44
|
+
dataset_dependency: Type[DefaultDependency] = DatasetParams
|
|
45
|
+
|
|
46
|
+
# Tile/Tilejson/WMTS Dependencies (Not used in titiler.xarray)
|
|
47
|
+
tile_dependency: Type[DefaultDependency] = DefaultDependency
|
|
48
|
+
|
|
49
|
+
# Statistics/Histogram Dependencies
|
|
50
|
+
stats_dependency: Type[DefaultDependency] = StatisticsParams
|
|
51
|
+
histogram_dependency: Type[DefaultDependency] = HistogramParams
|
|
52
|
+
|
|
53
|
+
img_part_dependency: Type[DefaultDependency] = PartFeatureParams
|
|
54
|
+
|
|
55
|
+
add_viewer: bool = True
|
|
56
|
+
add_part: bool = True
|
|
57
|
+
|
|
58
|
+
# remove some attribute from init
|
|
59
|
+
img_preview_dependency: Type[DefaultDependency] = field(init=False)
|
|
60
|
+
add_preview: bool = field(init=False, default=False)
|
|
61
|
+
|
|
62
|
+
# Custom /info endpoints (adds `show_times` options)
|
|
63
|
+
def info(self):
|
|
64
|
+
"""Register /info endpoint."""
|
|
65
|
+
|
|
66
|
+
@self.router.get(
|
|
67
|
+
"/info",
|
|
68
|
+
response_model=Info,
|
|
69
|
+
response_model_exclude_none=True,
|
|
70
|
+
response_class=JSONResponse,
|
|
71
|
+
responses={200: {"description": "Return dataset's basic info."}},
|
|
72
|
+
)
|
|
73
|
+
def info_endpoint(
|
|
74
|
+
src_path=Depends(self.path_dependency),
|
|
75
|
+
reader_params=Depends(self.reader_dependency),
|
|
76
|
+
show_times: Annotated[
|
|
77
|
+
Optional[bool],
|
|
78
|
+
Query(description="Show info about the time dimension"),
|
|
79
|
+
] = None,
|
|
80
|
+
env=Depends(self.environment_dependency),
|
|
81
|
+
) -> Info:
|
|
82
|
+
"""Return dataset's basic info."""
|
|
83
|
+
with rasterio.Env(**env):
|
|
84
|
+
with self.reader(src_path, **reader_params.as_dict()) as src_dst:
|
|
85
|
+
info = src_dst.info().model_dump()
|
|
86
|
+
if show_times and "time" in src_dst.input.dims:
|
|
87
|
+
times = [str(x.data) for x in src_dst.input.time]
|
|
88
|
+
info["count"] = len(times)
|
|
89
|
+
info["times"] = times
|
|
90
|
+
|
|
91
|
+
return Info(**info)
|
|
92
|
+
|
|
93
|
+
@self.router.get(
|
|
94
|
+
"/info.geojson",
|
|
95
|
+
response_model=InfoGeoJSON,
|
|
96
|
+
response_model_exclude_none=True,
|
|
97
|
+
response_class=GeoJSONResponse,
|
|
98
|
+
responses={
|
|
99
|
+
200: {
|
|
100
|
+
"content": {"application/geo+json": {}},
|
|
101
|
+
"description": "Return dataset's basic info as a GeoJSON feature.",
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
def info_geojson(
|
|
106
|
+
src_path=Depends(self.path_dependency),
|
|
107
|
+
reader_params=Depends(self.reader_dependency),
|
|
108
|
+
show_times: Annotated[
|
|
109
|
+
Optional[bool],
|
|
110
|
+
Query(description="Show info about the time dimension"),
|
|
111
|
+
] = None,
|
|
112
|
+
crs=Depends(CRSParams),
|
|
113
|
+
env=Depends(self.environment_dependency),
|
|
114
|
+
):
|
|
115
|
+
"""Return dataset's basic info as a GeoJSON feature."""
|
|
116
|
+
with rasterio.Env(**env):
|
|
117
|
+
with self.reader(src_path, **reader_params.as_dict()) as src_dst:
|
|
118
|
+
bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS)
|
|
119
|
+
if bounds[0] > bounds[2]:
|
|
120
|
+
pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3])
|
|
121
|
+
pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3])
|
|
122
|
+
geometry = MultiPolygon(
|
|
123
|
+
type="MultiPolygon",
|
|
124
|
+
coordinates=[pl.coordinates, pr.coordinates],
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
geometry = Polygon.from_bounds(*bounds)
|
|
128
|
+
|
|
129
|
+
info = src_dst.info().model_dump()
|
|
130
|
+
if show_times and "time" in src_dst.input.dims:
|
|
131
|
+
times = [str(x.data) for x in src_dst.input.time]
|
|
132
|
+
info["count"] = len(times)
|
|
133
|
+
info["times"] = times
|
|
134
|
+
|
|
135
|
+
return Feature(
|
|
136
|
+
type="Feature",
|
|
137
|
+
bbox=bounds,
|
|
138
|
+
geometry=geometry,
|
|
139
|
+
properties=info,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# custom /statistics endpoints (remove /statistics - GET)
|
|
143
|
+
def statistics(self):
|
|
144
|
+
"""add statistics endpoints."""
|
|
145
|
+
|
|
146
|
+
# POST endpoint
|
|
147
|
+
@self.router.post(
|
|
148
|
+
"/statistics",
|
|
149
|
+
response_model=StatisticsGeoJSON,
|
|
150
|
+
response_model_exclude_none=True,
|
|
151
|
+
response_class=GeoJSONResponse,
|
|
152
|
+
responses={
|
|
153
|
+
200: {
|
|
154
|
+
"content": {"application/geo+json": {}},
|
|
155
|
+
"description": "Return dataset's statistics from feature or featureCollection.",
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
def geojson_statistics(
|
|
160
|
+
geojson: Annotated[
|
|
161
|
+
Union[FeatureCollection, Feature],
|
|
162
|
+
Body(description="GeoJSON Feature or FeatureCollection."),
|
|
163
|
+
],
|
|
164
|
+
src_path=Depends(self.path_dependency),
|
|
165
|
+
reader_params=Depends(self.reader_dependency),
|
|
166
|
+
coord_crs=Depends(CoordCRSParams),
|
|
167
|
+
dst_crs=Depends(DstCRSParams),
|
|
168
|
+
layer_params=Depends(self.layer_dependency),
|
|
169
|
+
dataset_params=Depends(self.dataset_dependency),
|
|
170
|
+
image_params=Depends(self.img_part_dependency),
|
|
171
|
+
post_process=Depends(self.process_dependency),
|
|
172
|
+
stats_params=Depends(self.stats_dependency),
|
|
173
|
+
histogram_params=Depends(self.histogram_dependency),
|
|
174
|
+
env=Depends(self.environment_dependency),
|
|
175
|
+
):
|
|
176
|
+
"""Get Statistics from a geojson feature or featureCollection."""
|
|
177
|
+
fc = geojson
|
|
178
|
+
if isinstance(fc, Feature):
|
|
179
|
+
fc = FeatureCollection(type="FeatureCollection", features=[geojson])
|
|
180
|
+
|
|
181
|
+
with rasterio.Env(**env):
|
|
182
|
+
with self.reader(src_path, **reader_params.as_dict()) as src_dst:
|
|
183
|
+
for feature in fc:
|
|
184
|
+
shape = feature.model_dump(exclude_none=True)
|
|
185
|
+
image = src_dst.feature(
|
|
186
|
+
shape,
|
|
187
|
+
shape_crs=coord_crs or WGS84_CRS,
|
|
188
|
+
dst_crs=dst_crs,
|
|
189
|
+
**layer_params.as_dict(),
|
|
190
|
+
**image_params.as_dict(),
|
|
191
|
+
**dataset_params.as_dict(),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Get the coverage % array
|
|
195
|
+
coverage_array = image.get_coverage_array(
|
|
196
|
+
shape,
|
|
197
|
+
shape_crs=coord_crs or WGS84_CRS,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if post_process:
|
|
201
|
+
image = post_process(image)
|
|
202
|
+
|
|
203
|
+
stats = image.statistics(
|
|
204
|
+
**stats_params.as_dict(),
|
|
205
|
+
hist_options=histogram_params.as_dict(),
|
|
206
|
+
coverage=coverage_array,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
feature.properties = feature.properties or {}
|
|
210
|
+
feature.properties.update({"statistics": stats})
|
|
211
|
+
|
|
212
|
+
return fc.features[0] if isinstance(geojson, Feature) else fc
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""titiler.xarray.io"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
import attr
|
|
7
|
+
import numpy
|
|
8
|
+
import xarray
|
|
9
|
+
from morecantile import TileMatrixSet
|
|
10
|
+
from rio_tiler.constants import WEB_MERCATOR_TMS
|
|
11
|
+
from rio_tiler.io.xarray import XarrayReader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def xarray_open_dataset( # noqa: C901
|
|
15
|
+
src_path: str,
|
|
16
|
+
group: Optional[str] = None,
|
|
17
|
+
decode_times: bool = True,
|
|
18
|
+
) -> xarray.Dataset:
|
|
19
|
+
"""Open Xarray dataset with fsspec.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
src_path (str): dataset path.
|
|
23
|
+
group (Optional, str): path to the netCDF/Zarr group in the given file to open given as a str.
|
|
24
|
+
decode_times (bool): If True, decode times encoded in the standard NetCDF datetime format into datetime objects. Otherwise, leave them encoded as numbers.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
xarray.Dataset
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
import fsspec # noqa
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import gcsfs
|
|
34
|
+
except ImportError: # pragma: nocover
|
|
35
|
+
gcsfs = None # type: ignore
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import s3fs
|
|
39
|
+
except ImportError: # pragma: nocover
|
|
40
|
+
s3fs = None # type: ignore
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import aiohttp
|
|
44
|
+
except ImportError: # pragma: nocover
|
|
45
|
+
aiohttp = None # type: ignore
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import h5netcdf
|
|
49
|
+
except ImportError: # pragma: nocover
|
|
50
|
+
h5netcdf = None # type: ignore
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import zarr
|
|
54
|
+
except ImportError: # pragma: nocover
|
|
55
|
+
zarr = None # type: ignore
|
|
56
|
+
|
|
57
|
+
parsed = urlparse(src_path)
|
|
58
|
+
protocol = parsed.scheme or "file"
|
|
59
|
+
|
|
60
|
+
if any(src_path.lower().endswith(ext) for ext in [".nc", ".nc4"]):
|
|
61
|
+
assert (
|
|
62
|
+
h5netcdf is not None
|
|
63
|
+
), "'h5netcdf' must be installed to read NetCDF dataset"
|
|
64
|
+
|
|
65
|
+
xr_engine = "h5netcdf"
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
assert zarr is not None, "'zarr' must be installed to read Zarr dataset"
|
|
69
|
+
|
|
70
|
+
xr_engine = "zarr"
|
|
71
|
+
|
|
72
|
+
if protocol in ["", "file"]:
|
|
73
|
+
filesystem = fsspec.filesystem(protocol) # type: ignore
|
|
74
|
+
file_handler = (
|
|
75
|
+
filesystem.open(src_path)
|
|
76
|
+
if xr_engine == "h5netcdf"
|
|
77
|
+
else filesystem.get_mapper(src_path)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
elif protocol == "s3":
|
|
81
|
+
assert (
|
|
82
|
+
s3fs is not None
|
|
83
|
+
), "'aiohttp' must be installed to read dataset stored online"
|
|
84
|
+
|
|
85
|
+
s3_filesystem = s3fs.S3FileSystem()
|
|
86
|
+
file_handler = (
|
|
87
|
+
s3_filesystem.open(src_path)
|
|
88
|
+
if xr_engine == "h5netcdf"
|
|
89
|
+
else s3fs.S3Map(root=src_path, s3=s3_filesystem)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
elif protocol == "gs":
|
|
93
|
+
assert (
|
|
94
|
+
gcsfs is not None
|
|
95
|
+
), "'gcsfs' must be installed to read dataset stored in Google Cloud Storage"
|
|
96
|
+
|
|
97
|
+
gcs_filesystem = gcsfs.GCSFileSystem()
|
|
98
|
+
file_handler = (
|
|
99
|
+
gcs_filesystem.open(src_path)
|
|
100
|
+
if xr_engine == "h5netcdf"
|
|
101
|
+
else gcs_filesystem.get_mapper(root=src_path)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
elif protocol in ["http", "https"]:
|
|
105
|
+
assert (
|
|
106
|
+
aiohttp is not None
|
|
107
|
+
), "'aiohttp' must be installed to read dataset stored online"
|
|
108
|
+
|
|
109
|
+
filesystem = fsspec.filesystem(protocol) # type: ignore
|
|
110
|
+
file_handler = (
|
|
111
|
+
filesystem.open(src_path)
|
|
112
|
+
if xr_engine == "h5netcdf"
|
|
113
|
+
else filesystem.get_mapper(src_path)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(f"Unsupported protocol: {protocol}, for {src_path}")
|
|
118
|
+
|
|
119
|
+
# Arguments for xarray.open_dataset
|
|
120
|
+
# Default args
|
|
121
|
+
xr_open_args: Dict[str, Any] = {
|
|
122
|
+
"decode_coords": "all",
|
|
123
|
+
"decode_times": decode_times,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Argument if we're opening a datatree
|
|
127
|
+
if group is not None:
|
|
128
|
+
xr_open_args["group"] = group
|
|
129
|
+
|
|
130
|
+
# NetCDF arguments
|
|
131
|
+
if xr_engine == "h5netcdf":
|
|
132
|
+
xr_open_args.update(
|
|
133
|
+
{
|
|
134
|
+
"engine": "h5netcdf",
|
|
135
|
+
"lock": False,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
ds = xarray.open_dataset(file_handler, **xr_open_args)
|
|
140
|
+
|
|
141
|
+
# Fallback to Zarr
|
|
142
|
+
else:
|
|
143
|
+
ds = xarray.open_zarr(file_handler, **xr_open_args)
|
|
144
|
+
|
|
145
|
+
return ds
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _arrange_dims(da: xarray.DataArray) -> xarray.DataArray:
|
|
149
|
+
"""Arrange coordinates and time dimensions.
|
|
150
|
+
|
|
151
|
+
An rioxarray.exceptions.InvalidDimensionOrder error is raised if the coordinates are not in the correct order time, y, and x.
|
|
152
|
+
See: https://github.com/corteva/rioxarray/discussions/674
|
|
153
|
+
|
|
154
|
+
We conform to using x and y as the spatial dimension names..
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
if "x" not in da.dims and "y" not in da.dims:
|
|
158
|
+
try:
|
|
159
|
+
latitude_var_name = next(
|
|
160
|
+
name
|
|
161
|
+
for name in ["lat", "latitude", "LAT", "LATITUDE", "Lat"]
|
|
162
|
+
if name in da.dims
|
|
163
|
+
)
|
|
164
|
+
longitude_var_name = next(
|
|
165
|
+
name
|
|
166
|
+
for name in ["lon", "longitude", "LON", "LONGITUDE", "Lon"]
|
|
167
|
+
if name in da.dims
|
|
168
|
+
)
|
|
169
|
+
except StopIteration as e:
|
|
170
|
+
raise ValueError(f"Couldn't find X/Y dimensions in {da.dims}") from e
|
|
171
|
+
|
|
172
|
+
da = da.rename({latitude_var_name: "y", longitude_var_name: "x"})
|
|
173
|
+
|
|
174
|
+
if "TIME" in da.dims:
|
|
175
|
+
da = da.rename({"TIME": "time"})
|
|
176
|
+
|
|
177
|
+
if extra_dims := [d for d in da.dims if d not in ["x", "y"]]:
|
|
178
|
+
da = da.transpose(*extra_dims, "y", "x")
|
|
179
|
+
else:
|
|
180
|
+
da = da.transpose("y", "x")
|
|
181
|
+
|
|
182
|
+
# If min/max values are stored in `valid_range` we add them in `valid_min/valid_max`
|
|
183
|
+
vmin, vmax = da.attrs.get("valid_min"), da.attrs.get("valid_max")
|
|
184
|
+
if "valid_range" in da.attrs and not (vmin is not None and vmax is not None):
|
|
185
|
+
valid_range = da.attrs.get("valid_range")
|
|
186
|
+
da.attrs.update({"valid_min": valid_range[0], "valid_max": valid_range[1]})
|
|
187
|
+
|
|
188
|
+
return da
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_variable(
|
|
192
|
+
ds: xarray.Dataset,
|
|
193
|
+
variable: str,
|
|
194
|
+
datetime: Optional[str] = None,
|
|
195
|
+
drop_dim: Optional[str] = None,
|
|
196
|
+
) -> xarray.DataArray:
|
|
197
|
+
"""Get Xarray variable as DataArray.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
ds (xarray.Dataset): Xarray Dataset.
|
|
201
|
+
variable (str): Variable to extract from the Dataset.
|
|
202
|
+
datetime (str, optional): datetime to select from the DataArray.
|
|
203
|
+
drop_dim (str, optional): DataArray dimension to drop in form of `{dimension}={value}`.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
xarray.DataArray: 2D or 3D DataArray.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
da = ds[variable]
|
|
210
|
+
|
|
211
|
+
if drop_dim:
|
|
212
|
+
dim_to_drop, dim_val = drop_dim.split("=")
|
|
213
|
+
da = da.sel({dim_to_drop: dim_val}).drop_vars(dim_to_drop)
|
|
214
|
+
|
|
215
|
+
da = _arrange_dims(da)
|
|
216
|
+
|
|
217
|
+
# Make sure we have a valid CRS
|
|
218
|
+
crs = da.rio.crs or "epsg:4326"
|
|
219
|
+
da = da.rio.write_crs(crs)
|
|
220
|
+
|
|
221
|
+
if crs == "epsg:4326" and (da.x > 180).any():
|
|
222
|
+
# Adjust the longitude coordinates to the -180 to 180 range
|
|
223
|
+
da = da.assign_coords(x=(da.x + 180) % 360 - 180)
|
|
224
|
+
|
|
225
|
+
# Sort the dataset by the updated longitude coordinates
|
|
226
|
+
da = da.sortby(da.x)
|
|
227
|
+
|
|
228
|
+
# TODO: Technically we don't have to select the first time, rio-tiler should handle 3D dataset
|
|
229
|
+
if "time" in da.dims:
|
|
230
|
+
if datetime:
|
|
231
|
+
# TODO: handle time interval
|
|
232
|
+
time_as_str = datetime.split("T")[0]
|
|
233
|
+
if da["time"].dtype == "O":
|
|
234
|
+
da["time"] = da["time"].astype("datetime64[ns]")
|
|
235
|
+
|
|
236
|
+
da = da.sel(
|
|
237
|
+
time=numpy.array(time_as_str, dtype=numpy.datetime64), method="nearest"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
da = da.isel(time=0)
|
|
241
|
+
|
|
242
|
+
assert len(da.dims) in [2, 3], "titiler.xarray can only work with 2D or 3D dataset"
|
|
243
|
+
|
|
244
|
+
return da
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@attr.s
|
|
248
|
+
class Reader(XarrayReader):
|
|
249
|
+
"""Reader: Open Zarr file and access DataArray."""
|
|
250
|
+
|
|
251
|
+
src_path: str = attr.ib()
|
|
252
|
+
variable: str = attr.ib()
|
|
253
|
+
|
|
254
|
+
# xarray.Dataset options
|
|
255
|
+
opener: Callable[..., xarray.Dataset] = attr.ib(default=xarray_open_dataset)
|
|
256
|
+
|
|
257
|
+
group: Optional[str] = attr.ib(default=None)
|
|
258
|
+
decode_times: bool = attr.ib(default=True)
|
|
259
|
+
|
|
260
|
+
# xarray.DataArray options
|
|
261
|
+
datetime: Optional[str] = attr.ib(default=None)
|
|
262
|
+
drop_dim: Optional[str] = attr.ib(default=None)
|
|
263
|
+
|
|
264
|
+
tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
|
|
265
|
+
|
|
266
|
+
ds: xarray.Dataset = attr.ib(init=False)
|
|
267
|
+
input: xarray.DataArray = attr.ib(init=False)
|
|
268
|
+
|
|
269
|
+
_dims: List = attr.ib(init=False, factory=list)
|
|
270
|
+
|
|
271
|
+
def __attrs_post_init__(self):
|
|
272
|
+
"""Set bounds and CRS."""
|
|
273
|
+
self.ds = self.opener(
|
|
274
|
+
self.src_path,
|
|
275
|
+
group=self.group,
|
|
276
|
+
decode_times=self.decode_times,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
self.input = get_variable(
|
|
280
|
+
self.ds,
|
|
281
|
+
self.variable,
|
|
282
|
+
datetime=self.datetime,
|
|
283
|
+
drop_dim=self.drop_dim,
|
|
284
|
+
)
|
|
285
|
+
super().__attrs_post_init__()
|
|
286
|
+
|
|
287
|
+
def close(self):
|
|
288
|
+
"""Close xarray dataset."""
|
|
289
|
+
self.ds.close()
|
|
290
|
+
|
|
291
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
292
|
+
"""Support using with Context Managers."""
|
|
293
|
+
self.close()
|
|
File without changes
|