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.
@@ -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,11 @@
1
+ """titiler.core"""
2
+
3
+ __version__ = "0.17.3"
4
+
5
+ from . import dependencies, errors, factory, routing # noqa
6
+ from .factory import ( # noqa
7
+ BaseTilerFactory,
8
+ MultiBandTilerFactory,
9
+ MultiBaseTilerFactory,
10
+ TilerFactory,
11
+ )
@@ -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
+ )