titiler-core 0.17.3__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.core-0.17.3/PKG-INFO +92 -0
- titiler.core-0.17.3/README.md +65 -0
- titiler.core-0.17.3/pyproject.toml +82 -0
- titiler.core-0.17.3/titiler/core/__init__.py +11 -0
- titiler.core-0.17.3/titiler/core/algorithm/__init__.py +82 -0
- titiler.core-0.17.3/titiler/core/algorithm/base.py +40 -0
- titiler.core-0.17.3/titiler/core/algorithm/dem.py +183 -0
- titiler.core-0.17.3/titiler/core/algorithm/index.py +36 -0
- titiler.core-0.17.3/titiler/core/dependencies.py +613 -0
- titiler.core-0.17.3/titiler/core/errors.py +67 -0
- titiler.core-0.17.3/titiler/core/factory.py +1733 -0
- titiler.core-0.17.3/titiler/core/middleware.py +169 -0
- titiler.core-0.17.3/titiler/core/models/OGC.py +40 -0
- titiler.core-0.17.3/titiler/core/models/__init__.py +1 -0
- titiler.core-0.17.3/titiler/core/models/mapbox.py +42 -0
- titiler.core-0.17.3/titiler/core/models/responses.py +46 -0
- titiler.core-0.17.3/titiler/core/py.typed +0 -0
- titiler.core-0.17.3/titiler/core/resources/__init__.py +1 -0
- titiler.core-0.17.3/titiler/core/resources/enums.py +74 -0
- titiler.core-0.17.3/titiler/core/resources/responses.py +36 -0
- titiler.core-0.17.3/titiler/core/routing.py +93 -0
- titiler.core-0.17.3/titiler/core/templates/map.html +126 -0
- titiler.core-0.17.3/titiler/core/templates/wmts.xml +62 -0
- titiler.core-0.17.3/titiler/core/utils.py +118 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: titiler.core
|
|
3
|
+
Version: 0.17.3
|
|
4
|
+
Summary: A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: COG,STAC,MosaicJSON,Fastapi,Dynamic tile server,GDAL,Rasterio,OGC
|
|
7
|
+
Author-email: Vincent Sarago <vincent@developmentseed.com>
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Project-URL: Changelog, https://developmentseed.org/titiler/release-notes/
|
|
21
|
+
Project-URL: Documentation, https://developmentseed.org/titiler/
|
|
22
|
+
Project-URL: Homepage, https://developmentseed.org/titiler/
|
|
23
|
+
Project-URL: Issues, https://github.com/developmentseed/titiler/issues
|
|
24
|
+
Project-URL: Source, https://github.com/developmentseed/titiler
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
## titiler.core
|
|
28
|
+
|
|
29
|
+
Core of Titiler's application. Contains blocks to create dynamic tile servers.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ python -m pip install -U pip
|
|
35
|
+
|
|
36
|
+
# From Pypi
|
|
37
|
+
$ python -m pip install titiler.core
|
|
38
|
+
|
|
39
|
+
# Or from sources
|
|
40
|
+
$ git clone https://github.com/developmentseed/titiler.git
|
|
41
|
+
$ cd titiler && python -m pip install -e src/titiler/core
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How To
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
from titiler.core.factory import TilerFactory
|
|
49
|
+
|
|
50
|
+
# Create a FastAPI application
|
|
51
|
+
app = FastAPI(
|
|
52
|
+
description="A lightweight Cloud Optimized GeoTIFF tile server",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Create a set of COG endpoints
|
|
56
|
+
cog = TilerFactory()
|
|
57
|
+
|
|
58
|
+
# Register the COG endpoints to the application
|
|
59
|
+
app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [titiler.application](../application) for a full example.
|
|
63
|
+
|
|
64
|
+
## Package structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
titiler/
|
|
68
|
+
└── core/
|
|
69
|
+
├── tests/ - Tests suite
|
|
70
|
+
└── titiler/core/ - `core` namespace package
|
|
71
|
+
├── algorithm/
|
|
72
|
+
| ├── base.py - ABC Base Class for custom algorithms
|
|
73
|
+
| ├── dem.py - Elevation data related algorithms
|
|
74
|
+
| └── index.py - Simple band index algorithms
|
|
75
|
+
├── models/
|
|
76
|
+
| ├── response.py - Titiler's response models
|
|
77
|
+
| ├── mapbox.py - Mapbox TileJSON pydantic model
|
|
78
|
+
| └── OGC.py - Open GeoSpatial Consortium pydantic models (TileMatrixSets...)
|
|
79
|
+
├── resources/
|
|
80
|
+
| ├── enums.py - Titiler's enumerations (e.g MediaType)
|
|
81
|
+
| └── responses.py - Custom Starlette's responses
|
|
82
|
+
├── templates/
|
|
83
|
+
| ├── map.html - Simple Map viewer (built with leaflet)
|
|
84
|
+
| └── wmts.xml - OGC WMTS document template
|
|
85
|
+
├── dependencies.py - Titiler FastAPI's dependencies
|
|
86
|
+
├── errors.py - Errors handler factory
|
|
87
|
+
├── middleware.py - Starlette middlewares
|
|
88
|
+
├── factory.py - Dynamic tiler endpoints factories
|
|
89
|
+
├── routing.py - Custom APIRoute class
|
|
90
|
+
└── utils.py - Titiler utility functions
|
|
91
|
+
```
|
|
92
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
## titiler.core
|
|
2
|
+
|
|
3
|
+
Core of Titiler's application. Contains blocks to create dynamic tile servers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
$ python -m pip install -U pip
|
|
9
|
+
|
|
10
|
+
# From Pypi
|
|
11
|
+
$ python -m pip install titiler.core
|
|
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
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## How To
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
from titiler.core.factory import TilerFactory
|
|
23
|
+
|
|
24
|
+
# Create a FastAPI application
|
|
25
|
+
app = FastAPI(
|
|
26
|
+
description="A lightweight Cloud Optimized GeoTIFF tile server",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create a set of COG endpoints
|
|
30
|
+
cog = TilerFactory()
|
|
31
|
+
|
|
32
|
+
# Register the COG endpoints to the application
|
|
33
|
+
app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"])
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See [titiler.application](../application) for a full example.
|
|
37
|
+
|
|
38
|
+
## Package structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
titiler/
|
|
42
|
+
└── core/
|
|
43
|
+
├── tests/ - Tests suite
|
|
44
|
+
└── titiler/core/ - `core` namespace package
|
|
45
|
+
├── algorithm/
|
|
46
|
+
| ├── base.py - ABC Base Class for custom algorithms
|
|
47
|
+
| ├── dem.py - Elevation data related algorithms
|
|
48
|
+
| └── index.py - Simple band index algorithms
|
|
49
|
+
├── models/
|
|
50
|
+
| ├── response.py - Titiler's response models
|
|
51
|
+
| ├── mapbox.py - Mapbox TileJSON pydantic model
|
|
52
|
+
| └── OGC.py - Open GeoSpatial Consortium pydantic models (TileMatrixSets...)
|
|
53
|
+
├── resources/
|
|
54
|
+
| ├── enums.py - Titiler's enumerations (e.g MediaType)
|
|
55
|
+
| └── responses.py - Custom Starlette's responses
|
|
56
|
+
├── templates/
|
|
57
|
+
| ├── map.html - Simple Map viewer (built with leaflet)
|
|
58
|
+
| └── wmts.xml - OGC WMTS document template
|
|
59
|
+
├── dependencies.py - Titiler FastAPI's dependencies
|
|
60
|
+
├── errors.py - Errors handler factory
|
|
61
|
+
├── middleware.py - Starlette middlewares
|
|
62
|
+
├── factory.py - Dynamic tiler endpoints factories
|
|
63
|
+
├── routing.py - Custom APIRoute class
|
|
64
|
+
└── utils.py - Titiler utility functions
|
|
65
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "titiler.core"
|
|
3
|
+
description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL."
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.8"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Vincent Sarago", email = "vincent@developmentseed.com" },
|
|
8
|
+
]
|
|
9
|
+
keywords = [
|
|
10
|
+
"COG",
|
|
11
|
+
"STAC",
|
|
12
|
+
"MosaicJSON",
|
|
13
|
+
"Fastapi",
|
|
14
|
+
"Dynamic tile server",
|
|
15
|
+
"GDAL",
|
|
16
|
+
"Rasterio",
|
|
17
|
+
"OGC",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Intended Audience :: Information Technology",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
30
|
+
]
|
|
31
|
+
dynamic = []
|
|
32
|
+
dependencies = [
|
|
33
|
+
"fastapi>=0.107.0",
|
|
34
|
+
"geojson-pydantic>=1.0,<2.0",
|
|
35
|
+
"jinja2>=2.11.2,<4.0.0",
|
|
36
|
+
"numpy",
|
|
37
|
+
"pydantic~=2.0",
|
|
38
|
+
"rasterio",
|
|
39
|
+
"rio-tiler>=6.3.0,<7.0",
|
|
40
|
+
"morecantile>=5.0,<6.0",
|
|
41
|
+
"simplejson",
|
|
42
|
+
"typing_extensions>=4.6.1",
|
|
43
|
+
]
|
|
44
|
+
version = "0.17.3"
|
|
45
|
+
|
|
46
|
+
[project.license]
|
|
47
|
+
text = "MIT"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
test = [
|
|
51
|
+
"pytest",
|
|
52
|
+
"pytest-cov",
|
|
53
|
+
"pytest-asyncio",
|
|
54
|
+
"httpx",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[project.urls]
|
|
58
|
+
Homepage = "https://developmentseed.org/titiler/"
|
|
59
|
+
Documentation = "https://developmentseed.org/titiler/"
|
|
60
|
+
Issues = "https://github.com/developmentseed/titiler/issues"
|
|
61
|
+
Source = "https://github.com/developmentseed/titiler"
|
|
62
|
+
Changelog = "https://developmentseed.org/titiler/release-notes/"
|
|
63
|
+
|
|
64
|
+
[build-system]
|
|
65
|
+
requires = [
|
|
66
|
+
"pdm-pep517",
|
|
67
|
+
]
|
|
68
|
+
build-backend = "pdm.pep517.api"
|
|
69
|
+
|
|
70
|
+
[tool.pdm.version]
|
|
71
|
+
source = "file"
|
|
72
|
+
path = "titiler/core/__init__.py"
|
|
73
|
+
|
|
74
|
+
[tool.pdm.build]
|
|
75
|
+
includes = [
|
|
76
|
+
"titiler/core",
|
|
77
|
+
]
|
|
78
|
+
excludes = [
|
|
79
|
+
"tests/",
|
|
80
|
+
"**/.mypy_cache",
|
|
81
|
+
"**/.DS_Store",
|
|
82
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""titiler.core.algorithm."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from copy import copy
|
|
5
|
+
from typing import Dict, List, Literal, Optional, Type
|
|
6
|
+
|
|
7
|
+
import attr
|
|
8
|
+
from fastapi import HTTPException, Query
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from titiler.core.algorithm.base import AlgorithmMetadata, BaseAlgorithm # noqa
|
|
13
|
+
from titiler.core.algorithm.dem import Contours, HillShade, TerrainRGB, Terrarium
|
|
14
|
+
from titiler.core.algorithm.index import NormalizedIndex
|
|
15
|
+
|
|
16
|
+
default_algorithms: Dict[str, Type[BaseAlgorithm]] = {
|
|
17
|
+
"hillshade": HillShade,
|
|
18
|
+
"contours": Contours,
|
|
19
|
+
"normalizedIndex": NormalizedIndex,
|
|
20
|
+
"terrarium": Terrarium,
|
|
21
|
+
"terrainrgb": TerrainRGB,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@attr.s(frozen=True)
|
|
26
|
+
class Algorithms:
|
|
27
|
+
"""Algorithms."""
|
|
28
|
+
|
|
29
|
+
data: Dict[str, Type[BaseAlgorithm]] = attr.ib()
|
|
30
|
+
|
|
31
|
+
def get(self, name: str) -> BaseAlgorithm:
|
|
32
|
+
"""Fetch a TMS."""
|
|
33
|
+
if name not in self.data:
|
|
34
|
+
raise KeyError(f"Invalid name: {name}")
|
|
35
|
+
|
|
36
|
+
return self.data[name]
|
|
37
|
+
|
|
38
|
+
def list(self) -> List[str]:
|
|
39
|
+
"""List registered Algorithm."""
|
|
40
|
+
return list(self.data.keys())
|
|
41
|
+
|
|
42
|
+
def register(
|
|
43
|
+
self,
|
|
44
|
+
algorithms: Dict[str, BaseAlgorithm],
|
|
45
|
+
overwrite: bool = False,
|
|
46
|
+
) -> "Algorithms":
|
|
47
|
+
"""Register Algorithm(s)."""
|
|
48
|
+
for name, _algo in algorithms.items():
|
|
49
|
+
if name in self.data and not overwrite:
|
|
50
|
+
raise Exception(f"{name} is already a registered. Use overwrite=True.")
|
|
51
|
+
|
|
52
|
+
return Algorithms({**self.data, **algorithms})
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def dependency(self):
|
|
56
|
+
"""FastAPI PostProcess dependency."""
|
|
57
|
+
|
|
58
|
+
def post_process(
|
|
59
|
+
algorithm: Annotated[
|
|
60
|
+
Literal[tuple(self.data.keys())],
|
|
61
|
+
Query(description="Algorithm name"),
|
|
62
|
+
] = None,
|
|
63
|
+
algorithm_params: Annotated[
|
|
64
|
+
Optional[str],
|
|
65
|
+
Query(description="Algorithm parameter"),
|
|
66
|
+
] = None,
|
|
67
|
+
) -> Optional[BaseAlgorithm]:
|
|
68
|
+
"""Data Post-Processing options."""
|
|
69
|
+
kwargs = json.loads(algorithm_params) if algorithm_params else {}
|
|
70
|
+
if algorithm:
|
|
71
|
+
try:
|
|
72
|
+
return self.get(algorithm)(**kwargs)
|
|
73
|
+
|
|
74
|
+
except ValidationError as e:
|
|
75
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return post_process
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
algorithms = Algorithms(copy(default_algorithms)) # noqa
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Algorithm base class."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from typing import Dict, Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from rio_tiler.models import ImageData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseAlgorithm(BaseModel, metaclass=abc.ABCMeta):
|
|
11
|
+
"""Algorithm baseclass.
|
|
12
|
+
|
|
13
|
+
Note: attribute starting with `input_` or `output_` are considered as metadata
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# metadata
|
|
18
|
+
input_nbands: Optional[int] = None
|
|
19
|
+
output_nbands: Optional[int] = None
|
|
20
|
+
output_dtype: Optional[str] = None
|
|
21
|
+
output_min: Optional[Sequence] = None
|
|
22
|
+
output_max: Optional[Sequence] = None
|
|
23
|
+
|
|
24
|
+
model_config = {"extra": "allow"}
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
28
|
+
"""Apply algorithm"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AlgorithmMetadata(BaseModel):
|
|
33
|
+
"""Algorithm metadata."""
|
|
34
|
+
|
|
35
|
+
title: Optional[str] = None
|
|
36
|
+
description: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
inputs: Dict
|
|
39
|
+
outputs: Dict
|
|
40
|
+
parameters: Dict
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""titiler.core.algorithm DEM."""
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from rasterio import windows
|
|
6
|
+
from rio_tiler.colormap import apply_cmap, cmap
|
|
7
|
+
from rio_tiler.models import ImageData
|
|
8
|
+
from rio_tiler.utils import linear_rescale
|
|
9
|
+
|
|
10
|
+
from titiler.core.algorithm.base import BaseAlgorithm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HillShade(BaseAlgorithm):
|
|
14
|
+
"""Hillshade."""
|
|
15
|
+
|
|
16
|
+
title: str = "Hillshade"
|
|
17
|
+
description: str = "Create hillshade from DEM dataset."
|
|
18
|
+
|
|
19
|
+
# parameters
|
|
20
|
+
azimuth: int = Field(90, ge=0, le=360)
|
|
21
|
+
angle_altitude: float = Field(90.0, ge=-90.0, le=90.0)
|
|
22
|
+
buffer: int = Field(3, ge=0, le=99)
|
|
23
|
+
|
|
24
|
+
# metadata
|
|
25
|
+
input_nbands: int = 1
|
|
26
|
+
output_nbands: int = 1
|
|
27
|
+
output_dtype: str = "uint8"
|
|
28
|
+
|
|
29
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
30
|
+
"""Create hillshade from DEM dataset."""
|
|
31
|
+
x, y = numpy.gradient(img.array[0])
|
|
32
|
+
slope = numpy.pi / 2.0 - numpy.arctan(numpy.sqrt(x * x + y * y))
|
|
33
|
+
aspect = numpy.arctan2(-x, y)
|
|
34
|
+
azimuthrad = self.azimuth * numpy.pi / 180.0
|
|
35
|
+
altituderad = self.angle_altitude * numpy.pi / 180.0
|
|
36
|
+
shaded = numpy.sin(altituderad) * numpy.sin(slope) + numpy.cos(
|
|
37
|
+
altituderad
|
|
38
|
+
) * numpy.cos(slope) * numpy.cos(azimuthrad - aspect)
|
|
39
|
+
data = 255 * (shaded + 1) / 2
|
|
40
|
+
|
|
41
|
+
bounds = img.bounds
|
|
42
|
+
if self.buffer:
|
|
43
|
+
data = data[self.buffer : -self.buffer, self.buffer : -self.buffer]
|
|
44
|
+
|
|
45
|
+
window = windows.Window(
|
|
46
|
+
col_off=self.buffer,
|
|
47
|
+
row_off=self.buffer,
|
|
48
|
+
width=data.shape[1],
|
|
49
|
+
height=data.shape[0],
|
|
50
|
+
)
|
|
51
|
+
bounds = windows.bounds(window, img.transform)
|
|
52
|
+
|
|
53
|
+
return ImageData(
|
|
54
|
+
data.astype(self.output_dtype),
|
|
55
|
+
assets=img.assets,
|
|
56
|
+
crs=img.crs,
|
|
57
|
+
bounds=bounds,
|
|
58
|
+
band_names=["hillshade"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Contours(BaseAlgorithm):
|
|
63
|
+
"""Contours.
|
|
64
|
+
|
|
65
|
+
Original idea from https://custom-scripts.sentinel-hub.com/dem/contour-lines/
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
title: str = "Contours"
|
|
69
|
+
description: str = "Create contours from DEM dataset."
|
|
70
|
+
|
|
71
|
+
# parameters
|
|
72
|
+
increment: int = Field(35, ge=0, le=999)
|
|
73
|
+
thickness: int = Field(1, ge=0, le=10)
|
|
74
|
+
minz: int = Field(-12000, ge=-99999, le=99999)
|
|
75
|
+
maxz: int = Field(8000, ge=-99999, le=99999)
|
|
76
|
+
|
|
77
|
+
# metadata
|
|
78
|
+
input_nbands: int = 1
|
|
79
|
+
output_nbands: int = 3
|
|
80
|
+
output_dtype: str = "uint8"
|
|
81
|
+
|
|
82
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
83
|
+
"""Add contours."""
|
|
84
|
+
data = img.data
|
|
85
|
+
|
|
86
|
+
# Apply rescaling for minz,maxz to 1->255 and apply Terrain colormap
|
|
87
|
+
arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype(
|
|
88
|
+
self.output_dtype
|
|
89
|
+
)
|
|
90
|
+
arr, _ = apply_cmap(arr, cmap.get("terrain"))
|
|
91
|
+
|
|
92
|
+
# set black (0) for contour lines
|
|
93
|
+
arr = numpy.where(data % self.increment < self.thickness, 0, arr)
|
|
94
|
+
|
|
95
|
+
data = numpy.ma.MaskedArray(arr)
|
|
96
|
+
data.mask = ~img.mask
|
|
97
|
+
|
|
98
|
+
return ImageData(
|
|
99
|
+
data,
|
|
100
|
+
assets=img.assets,
|
|
101
|
+
crs=img.crs,
|
|
102
|
+
bounds=img.bounds,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Terrarium(BaseAlgorithm):
|
|
107
|
+
"""Encode DEM into RGB (Mapzen Terrarium)."""
|
|
108
|
+
|
|
109
|
+
title: str = "Terrarium"
|
|
110
|
+
description: str = "Encode DEM into RGB (Mapzen Terrarium)."
|
|
111
|
+
|
|
112
|
+
# metadata
|
|
113
|
+
input_nbands: int = 1
|
|
114
|
+
output_nbands: int = 3
|
|
115
|
+
output_dtype: str = "uint8"
|
|
116
|
+
|
|
117
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
118
|
+
"""Encode DEM into RGB."""
|
|
119
|
+
data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0)
|
|
120
|
+
r = data / 256
|
|
121
|
+
g = data % 256
|
|
122
|
+
b = (data * 256) % 256
|
|
123
|
+
|
|
124
|
+
return ImageData(
|
|
125
|
+
numpy.ma.stack([r, g, b]).astype(self.output_dtype),
|
|
126
|
+
assets=img.assets,
|
|
127
|
+
crs=img.crs,
|
|
128
|
+
bounds=img.bounds,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TerrainRGB(BaseAlgorithm):
|
|
133
|
+
"""Encode DEM into RGB (Mapbox Terrain RGB)."""
|
|
134
|
+
|
|
135
|
+
title: str = "Terrarium"
|
|
136
|
+
description: str = "Encode DEM into RGB (Mapbox Terrain RGB)."
|
|
137
|
+
|
|
138
|
+
# parameters
|
|
139
|
+
interval: float = Field(0.1, ge=0.0, le=1.0)
|
|
140
|
+
baseval: float = Field(-10000.0, ge=-99999.0, le=99999.0)
|
|
141
|
+
|
|
142
|
+
# metadata
|
|
143
|
+
input_nbands: int = 1
|
|
144
|
+
output_nbands: int = 3
|
|
145
|
+
output_dtype: str = "uint8"
|
|
146
|
+
|
|
147
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
148
|
+
"""Encode DEM into RGB (Mapbox Terrain RGB).
|
|
149
|
+
|
|
150
|
+
Code from https://github.com/mapbox/rio-rgbify/blob/master/rio_rgbify/encoders.py (MIT)
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def _range_check(datarange):
|
|
155
|
+
"""
|
|
156
|
+
Utility to check if data range is outside of precision for 3 digit base 256
|
|
157
|
+
"""
|
|
158
|
+
maxrange = 256**3
|
|
159
|
+
|
|
160
|
+
return datarange > maxrange
|
|
161
|
+
|
|
162
|
+
round_digits = 0
|
|
163
|
+
|
|
164
|
+
data = img.array[0].astype(numpy.float64)
|
|
165
|
+
data -= self.baseval
|
|
166
|
+
data /= self.interval
|
|
167
|
+
|
|
168
|
+
data = numpy.around(data / 2**round_digits) * 2**round_digits
|
|
169
|
+
|
|
170
|
+
datarange = data.max() - data.min()
|
|
171
|
+
if _range_check(datarange):
|
|
172
|
+
raise ValueError(f"Data of {datarange} larger than 256 ** 3")
|
|
173
|
+
|
|
174
|
+
r = ((((data // 256) // 256) / 256) - (((data // 256) // 256) // 256)) * 256
|
|
175
|
+
g = (((data // 256) / 256) - ((data // 256) // 256)) * 256
|
|
176
|
+
b = ((data / 256) - (data // 256)) * 256
|
|
177
|
+
|
|
178
|
+
return ImageData(
|
|
179
|
+
numpy.ma.stack([r, g, b]).astype(self.output_dtype),
|
|
180
|
+
assets=img.assets,
|
|
181
|
+
crs=img.crs,
|
|
182
|
+
bounds=img.bounds,
|
|
183
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""titiler.core.algorithm Normalized Index."""
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from rio_tiler.models import ImageData
|
|
7
|
+
|
|
8
|
+
from titiler.core.algorithm.base import BaseAlgorithm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NormalizedIndex(BaseAlgorithm):
|
|
12
|
+
"""Normalized Difference Index."""
|
|
13
|
+
|
|
14
|
+
title: str = "Normalized Difference Index"
|
|
15
|
+
description: str = "Compute normalized difference index from two bands."
|
|
16
|
+
|
|
17
|
+
# metadata
|
|
18
|
+
input_nbands: int = 2
|
|
19
|
+
output_nbands: int = 1
|
|
20
|
+
output_dtype: str = "float32"
|
|
21
|
+
output_min: Sequence[float] = [-1.0]
|
|
22
|
+
output_max: Sequence[float] = [1.0]
|
|
23
|
+
|
|
24
|
+
def __call__(self, img: ImageData) -> ImageData:
|
|
25
|
+
"""Normalized difference."""
|
|
26
|
+
b1 = img.array[0].astype("float32")
|
|
27
|
+
b2 = img.array[1].astype("float32")
|
|
28
|
+
arr = numpy.ma.MaskedArray((b2 - b1) / (b2 + b1), dtype=self.output_dtype)
|
|
29
|
+
bnames = img.band_names
|
|
30
|
+
return ImageData(
|
|
31
|
+
arr,
|
|
32
|
+
assets=img.assets,
|
|
33
|
+
crs=img.crs,
|
|
34
|
+
bounds=img.bounds,
|
|
35
|
+
band_names=[f"({bnames[1]} - {bnames[0]}) / ({bnames[1]} + {bnames[0]})"],
|
|
36
|
+
)
|