From aa6b3228d82e039245a6f89af76df595b8ac6ddc Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 15:32:18 +0200 Subject: [PATCH 01/19] Fix TerrainRGB algorithm and param user-controlled nodata-height Added two params `use_nodata_height and nodata_height` (or could have used a field which cold be either undefined or that height value? --- src/titiler/core/titiler/core/algorithm/dem.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 8c5a2ed2c..302a55ddc 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -191,6 +191,8 @@ class TerrainRGB(BaseAlgorithm): # parameters interval: float = Field(0.1, ge=0.0, le=1.0) baseval: float = Field(-10000.0, ge=-99999.0, le=99999.0) + use_nodata_height: bool = Field(False) + nodata_height: float = Field(0.0, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 @@ -224,9 +226,13 @@ def _range_check(datarange): if _range_check(datarange): raise ValueError(f"Data of {datarange} larger than 256 ** 3") - r = ((((data // 256) // 256) / 256) - (((data // 256) // 256) // 256)) * 256 - g = (((data // 256) / 256) - ((data // 256) // 256)) * 256 - b = ((data / 256) - (data // 256)) * 256 + if self.use_nodata_height: + data[img.array.mask[0]] = (0 - self.baseval) / self.interval + + data_int32 = data.astype(numpy.int32) + b = (data_int32) & 0xff + g = (data_int32 >> 8) & 0xff + r = (data_int32 >> 16) & 0xff return ImageData( numpy.ma.stack([r, g, b]).astype(self.output_dtype), From ba99a31b39c229097bf5dfdf6424c935ad2ca4a9 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 15:44:24 +0200 Subject: [PATCH 02/19] Make use of user-controlled height in terrainrgb --- src/titiler/core/titiler/core/algorithm/dem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 302a55ddc..9daeb6aa9 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -227,7 +227,7 @@ def _range_check(datarange): raise ValueError(f"Data of {datarange} larger than 256 ** 3") if self.use_nodata_height: - data[img.array.mask[0]] = (0 - self.baseval) / self.interval + data[img.array.mask[0]] = (self.nodata_height - self.baseval) / self.interval data_int32 = data.astype(numpy.int32) b = (data_int32) & 0xff From b364abb36fa1e9f59341c9878f7d8efe6397e58c Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 15:44:41 +0200 Subject: [PATCH 03/19] Add user-controlled nodata-height for terrarium as well --- src/titiler/core/titiler/core/algorithm/dem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 9daeb6aa9..7afe5e837 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -161,6 +161,8 @@ class Terrarium(BaseAlgorithm): title: str = "Terrarium" description: str = "Encode DEM into RGB (Mapzen Terrarium)." + use_nodata_height: bool = Field(False) + nodata_height: float = Field(0.0, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 @@ -170,6 +172,8 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) + if self.use_nodata_height: + data[img.array.mask[0]] = numpy.clip(self.nodata_height + 32768.0, 0.0, 65535.0) r = data / 256 g = data % 256 b = (data * 256) % 256 From d4461f8c8840c256c52efc566acb24a1a602b3cc Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 15:53:51 +0200 Subject: [PATCH 04/19] Add z_exaggeration parameter to hillshade/slope algorithms defaults to 1, applied to gradients directly --- src/titiler/core/titiler/core/algorithm/dem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 7afe5e837..7e5efc701 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -22,6 +22,7 @@ class HillShade(BaseAlgorithm): azimuth: int = Field(45, ge=0, le=360) angle_altitude: float = Field(45.0, ge=-90.0, le=90.0) buffer: int = Field(3, ge=0, le=99) + z_exaggeration: float = Field(1., ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -31,6 +32,8 @@ class HillShade(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Create hillshade from DEM dataset.""" x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration slope = numpy.pi / 2.0 - numpy.arctan(numpy.sqrt(x * x + y * y)) aspect = numpy.arctan2(-x, y) azimuth = 360.0 - self.azimuth @@ -71,6 +74,7 @@ class Slope(BaseAlgorithm): # parameters buffer: int = Field(3, ge=0, le=99, description="Buffer size for edge effects") + z_exaggeration: float = Field(1., ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -86,6 +90,8 @@ def __call__(self, img: ImageData) -> ImageData: pixel_size_y = abs(img.transform[4]) x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration dx = x / pixel_size_x dy = y / pixel_size_y From d0380b183d7d6ca40b3b56deaf6c4b56c53e4278 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:03:32 +0200 Subject: [PATCH 05/19] Add slope in algorithms doc --- docs/src/advanced/Algorithms.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/advanced/Algorithms.md b/docs/src/advanced/Algorithms.md index 8f65c7860..c6477c71a 100644 --- a/docs/src/advanced/Algorithms.md +++ b/docs/src/advanced/Algorithms.md @@ -8,6 +8,7 @@ We added a set of custom algorithms: - `hillshade`: Create hillshade from elevation dataset - `contours`: Create contours lines (raster) from elevation dataset +- `slope`: Create degrees of slope from elevation dataset - `terrarium`: Mapzen's format to encode elevation value in RGB values (https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) - `terrainrgb`: Mapbox's format to encode elevation value in RGB values (https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/) - `normalizedIndex`: Normalized Difference Index (e.g NDVI) From 915a50f61dea810708c6de85c212d13b3344b1aa Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:05:44 +0200 Subject: [PATCH 06/19] Make terrainRGB/terrarium docs links instead of plain urls --- docs/src/advanced/Algorithms.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/advanced/Algorithms.md b/docs/src/advanced/Algorithms.md index c6477c71a..2f93a2a41 100644 --- a/docs/src/advanced/Algorithms.md +++ b/docs/src/advanced/Algorithms.md @@ -9,8 +9,8 @@ We added a set of custom algorithms: - `hillshade`: Create hillshade from elevation dataset - `contours`: Create contours lines (raster) from elevation dataset - `slope`: Create degrees of slope from elevation dataset -- `terrarium`: Mapzen's format to encode elevation value in RGB values (https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) -- `terrainrgb`: Mapbox's format to encode elevation value in RGB values (https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/) +- `terrarium`: [Mapzen's format]((https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium)) to encode elevation value in RGB values `elevation = (red * 256 + green + blue / 256) - 32768` +- `terrainrgb`: [Mapbox](https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/)/[Maptiler](https://docs.maptiler.com/guides/map-tilling-hosting/data-hosting/rgb-terrain-by-maptiler/)'s format to encode elevation value in RGB values `elevation = -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1)` - `normalizedIndex`: Normalized Difference Index (e.g NDVI) - `cast`: Cast data to integer - `floor`: Round data to the smallest integer From 6a194edaa85dacaa11887da885d539da08185bfb Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:05:57 +0200 Subject: [PATCH 07/19] Add parameters hints to hillshade/contours --- docs/src/advanced/Algorithms.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/advanced/Algorithms.md b/docs/src/advanced/Algorithms.md index 2f93a2a41..505ce57bb 100644 --- a/docs/src/advanced/Algorithms.md +++ b/docs/src/advanced/Algorithms.md @@ -6,8 +6,8 @@ The algorithms are meant to overcome the limitation of `expression` (using [nume We added a set of custom algorithms: -- `hillshade`: Create hillshade from elevation dataset -- `contours`: Create contours lines (raster) from elevation dataset +- `hillshade`: Create hillshade from elevation dataset (parameters: azimuth (45), angle_altitude(45)) +- `contours`: Create contours lines (raster) from elevation dataset (parameters: increment (35), thickness (1)) - `slope`: Create degrees of slope from elevation dataset - `terrarium`: [Mapzen's format]((https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium)) to encode elevation value in RGB values `elevation = (red * 256 + green + blue / 256) - 32768` - `terrainrgb`: [Mapbox](https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/)/[Maptiler](https://docs.maptiler.com/guides/map-tilling-hosting/data-hosting/rgb-terrain-by-maptiler/)'s format to encode elevation value in RGB values `elevation = -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1)` From 722e48ec3095873142caa04fa00405119dc1d963 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:35:28 +0200 Subject: [PATCH 08/19] nodata_height optional Co-authored-by: Vincent Sarago --- src/titiler/core/titiler/core/algorithm/dem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 7e5efc701..e40d98a6f 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -201,8 +201,7 @@ class TerrainRGB(BaseAlgorithm): # parameters interval: float = Field(0.1, ge=0.0, le=1.0) baseval: float = Field(-10000.0, ge=-99999.0, le=99999.0) - use_nodata_height: bool = Field(False) - nodata_height: float = Field(0.0, ge=-99999.0, le=99999.0) + nodata_height: Optional[float] = Field(None, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 From ce7e2272b460fbfdf851fc1c788772a8f72c3caa Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:35:44 +0200 Subject: [PATCH 09/19] check nodata_height not None Co-authored-by: Vincent Sarago --- src/titiler/core/titiler/core/algorithm/dem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index e40d98a6f..ea073fc38 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -235,7 +235,7 @@ def _range_check(datarange): if _range_check(datarange): raise ValueError(f"Data of {datarange} larger than 256 ** 3") - if self.use_nodata_height: + if self.nodata_height is not None: data[img.array.mask[0]] = (self.nodata_height - self.baseval) / self.interval data_int32 = data.astype(numpy.int32) From ee2ae5c2f26ead5796e21c38ec493eb6e7937b6a Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:35:54 +0200 Subject: [PATCH 10/19] Update src/titiler/core/titiler/core/algorithm/dem.py Co-authored-by: Vincent Sarago --- src/titiler/core/titiler/core/algorithm/dem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index ea073fc38..bc4816429 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -167,8 +167,7 @@ class Terrarium(BaseAlgorithm): title: str = "Terrarium" description: str = "Encode DEM into RGB (Mapzen Terrarium)." - use_nodata_height: bool = Field(False) - nodata_height: float = Field(0.0, ge=-99999.0, le=99999.0) + nodata_height: Optional[float] = Field(None, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 From dd7c6f9937a9c5fa48766b05a8e8e49c8e14b00a Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:36:00 +0200 Subject: [PATCH 11/19] Update src/titiler/core/titiler/core/algorithm/dem.py Co-authored-by: Vincent Sarago --- src/titiler/core/titiler/core/algorithm/dem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index bc4816429..ce73fdc12 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -177,7 +177,7 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) - if self.use_nodata_height: + if self.nodata_height is not None: data[img.array.mask[0]] = numpy.clip(self.nodata_height + 32768.0, 0.0, 65535.0) r = data / 256 g = data % 256 From bcd84c935a07874b4b5a8b7407827a99d15872e8 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 16:41:31 +0200 Subject: [PATCH 12/19] from typing import Optional --- src/titiler/core/titiler/core/algorithm/dem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index ce73fdc12..ce985e2f6 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -1,6 +1,7 @@ """titiler.core.algorithm DEM.""" import numpy +from typing import Optional from pydantic import Field from rasterio import windows from rio_tiler.colormap import apply_cmap, cmap From 31d5277cf3657a39f84ed9d533367d0b8309aff1 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 18:23:19 +0200 Subject: [PATCH 13/19] WIP /cog/viewer algorithms inputs Adding algorithm params dynamically based on the /algorithms endpoint, stored in scope, and params updated on change of selected algorithm --- .../extensions/templates/cog_viewer.html | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index 04d37554b..c46ebb5f9 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -256,6 +256,16 @@
+ + +
+
Algorithm
+
+ +
+
+
@@ -338,7 +348,8 @@ dataset_statistics: undefined, data_type: undefined, band_descriptions: undefined, - colormap: undefined + colormap: undefined, + algorithms: undefined, } const tilejson_endpoint = '{{ tilejson_endpoint }}' @@ -841,7 +852,6 @@ throw new Error('Network response was not ok.') }) .then(data => { - console.log(data) scope.data_type = data.properties.dtype scope.colormap = data.properties.colormap @@ -910,6 +920,65 @@ .catch(err => { console.warn(err) }) + + + // fetch(`/colorMaps`) then /colorMaps/accent?format=png but one request per colormap pretty heavy + fetch(`/algorithms`) + .then(res => { + if (res.ok) return res.json() + throw new Error('Network response was not ok.') + }) + .then(algorithms => { + console.log('ALGORITHMS', algorithms) + scope.algorithms = algorithms + const algorithmSelector = document.getElementById('algorithm-selector'); + for (const algo_id in algorithms) { + const algo = algorithms[algo_id] + console.log(algo) + const opt = document.createElement('option'); + opt.value = algo_id; + opt.innerHTML = algo['title']; + algorithmSelector.appendChild(opt); + } + algorithmSelector.addEventListener('change', updateAlgorithmParams) + }) + .catch(err => { + console.warn(err) + }) +} + +function updateAlgorithmParams() { + const algorithmSelector = document.getElementById('algorithm-selector'); + console.log('algorithmSelector', algorithmSelector) + const selected = algorithmSelector.selectedOptions[0].value; + console.log("selected", selected) + const params = scope.algorithms[selected]['parameters']; + console.log(params) + + // Recreate div to host params + const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params') + if (paramsElOld) { + paramsElOld.remove() + } + // Reproduce the div + form from lat/lon inputs + const paramsEl = document.createElement('div'); + paramsEl.className = 'px6 py6 w-full' + paramsEl.id = 'algorithm-params'; + algorithmSelector.parentNode.appendChild(paramsEl) + const paramsForm = document.createElement('form'); + paramsForm.className = 'grid grid--gut12 mx12 mt12' + paramsForm.id = 'algorithm-params-form'; + paramsEl.appendChild(paramsForm) + for (const param_id in params) { + const param = params[param_id] + const paramEl = document.createElement('div'); + paramEl.className = 'row' + paramsForm.appendChild(paramEl); + paramEl.innerHTML = ` + + ${param.default} + ` + } } document.getElementById('launch').addEventListener('click', () => { From bf094ae74afbc4103573698d88f77c094a435eb9 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 1 Apr 2025 18:54:36 +0200 Subject: [PATCH 14/19] Final touch-ups to /cog/viewer for algorithms visualization Uses number inputs if param is integer or number, otherwise text input (eg nodata-height which can be either null or number) Updates tilejson url when algorithm or its params are changed --- .../extensions/templates/cog_viewer.html | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index c46ebb5f9..dae5aa211 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -260,10 +260,11 @@
Algorithm
-
- + -
+
@@ -490,11 +491,20 @@ params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}` } + const algorithmSelector = document.getElementById('algorithm-selector'); + const algorithm = algorithmSelector.selectedOptions[0].value; + if (algorithm !== 'no-algo') { + params.algorithm = algorithm + params.algorithm_params = JSON.stringify({}) + } + // const params = scope.algorithms[selected]['parameters']; + const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex] if (cmap.value !== 'b&w') params.colormap_name = cmap.value const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&') let url = `${tilejson_endpoint}?${url_params}` + console.log(url) setMapLayers(url) if (scope.dataset_statistics) addHisto1Band() } @@ -929,56 +939,54 @@ throw new Error('Network response was not ok.') }) .then(algorithms => { - console.log('ALGORITHMS', algorithms) - scope.algorithms = algorithms + // console.log('ALGORITHMS', algorithms) + scope.algorithms = algorithms; const algorithmSelector = document.getElementById('algorithm-selector'); for (const algo_id in algorithms) { - const algo = algorithms[algo_id] - console.log(algo) + const algo = algorithms[algo_id]; const opt = document.createElement('option'); opt.value = algo_id; opt.innerHTML = algo['title']; algorithmSelector.appendChild(opt); } - algorithmSelector.addEventListener('change', updateAlgorithmParams) + algorithmSelector.addEventListener('change', updateAlgorithmParams); }) .catch(err => { - console.warn(err) + console.warn(err); }) } function updateAlgorithmParams() { const algorithmSelector = document.getElementById('algorithm-selector'); - console.log('algorithmSelector', algorithmSelector) const selected = algorithmSelector.selectedOptions[0].value; - console.log("selected", selected) const params = scope.algorithms[selected]['parameters']; - console.log(params) // Recreate div to host params - const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params') + const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params'); if (paramsElOld) { - paramsElOld.remove() + paramsElOld.remove(); } // Reproduce the div + form from lat/lon inputs const paramsEl = document.createElement('div'); - paramsEl.className = 'px6 py6 w-full' + paramsEl.className = 'grid px12 py12 w-full grid'; paramsEl.id = 'algorithm-params'; - algorithmSelector.parentNode.appendChild(paramsEl) + algorithmSelector.parentNode.appendChild(paramsEl); const paramsForm = document.createElement('form'); - paramsForm.className = 'grid grid--gut12 mx12 mt12' + paramsForm.className = 'grid grid--gut12 mx12 mt12'; paramsForm.id = 'algorithm-params-form'; - paramsEl.appendChild(paramsForm) + paramsEl.appendChild(paramsForm); for (const param_id in params) { - const param = params[param_id] + const param = params[param_id]; const paramEl = document.createElement('div'); - paramEl.className = 'row' + paramEl.className = 'row'; paramsForm.appendChild(paramEl); paramEl.innerHTML = ` - ${param.default} - ` + + `; } + // Update to react to params change + updateViz(); } document.getElementById('launch').addEventListener('click', () => { From 332ad40953ca587ee8015644bf6df57875ce3eb5 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 2 Apr 2025 12:14:58 +0200 Subject: [PATCH 15/19] Revert "Final touch-ups to /cog/viewer for algorithms visualization" This reverts commit bf094ae74afbc4103573698d88f77c094a435eb9. --- .../extensions/templates/cog_viewer.html | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index dae5aa211..c46ebb5f9 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -260,11 +260,10 @@
Algorithm
-
- - +
@@ -491,20 +490,11 @@ params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}` } - const algorithmSelector = document.getElementById('algorithm-selector'); - const algorithm = algorithmSelector.selectedOptions[0].value; - if (algorithm !== 'no-algo') { - params.algorithm = algorithm - params.algorithm_params = JSON.stringify({}) - } - // const params = scope.algorithms[selected]['parameters']; - const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex] if (cmap.value !== 'b&w') params.colormap_name = cmap.value const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&') let url = `${tilejson_endpoint}?${url_params}` - console.log(url) setMapLayers(url) if (scope.dataset_statistics) addHisto1Band() } @@ -939,54 +929,56 @@ throw new Error('Network response was not ok.') }) .then(algorithms => { - // console.log('ALGORITHMS', algorithms) - scope.algorithms = algorithms; + console.log('ALGORITHMS', algorithms) + scope.algorithms = algorithms const algorithmSelector = document.getElementById('algorithm-selector'); for (const algo_id in algorithms) { - const algo = algorithms[algo_id]; + const algo = algorithms[algo_id] + console.log(algo) const opt = document.createElement('option'); opt.value = algo_id; opt.innerHTML = algo['title']; algorithmSelector.appendChild(opt); } - algorithmSelector.addEventListener('change', updateAlgorithmParams); + algorithmSelector.addEventListener('change', updateAlgorithmParams) }) .catch(err => { - console.warn(err); + console.warn(err) }) } function updateAlgorithmParams() { const algorithmSelector = document.getElementById('algorithm-selector'); + console.log('algorithmSelector', algorithmSelector) const selected = algorithmSelector.selectedOptions[0].value; + console.log("selected", selected) const params = scope.algorithms[selected]['parameters']; + console.log(params) // Recreate div to host params - const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params'); + const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params') if (paramsElOld) { - paramsElOld.remove(); + paramsElOld.remove() } // Reproduce the div + form from lat/lon inputs const paramsEl = document.createElement('div'); - paramsEl.className = 'grid px12 py12 w-full grid'; + paramsEl.className = 'px6 py6 w-full' paramsEl.id = 'algorithm-params'; - algorithmSelector.parentNode.appendChild(paramsEl); + algorithmSelector.parentNode.appendChild(paramsEl) const paramsForm = document.createElement('form'); - paramsForm.className = 'grid grid--gut12 mx12 mt12'; + paramsForm.className = 'grid grid--gut12 mx12 mt12' paramsForm.id = 'algorithm-params-form'; - paramsEl.appendChild(paramsForm); + paramsEl.appendChild(paramsForm) for (const param_id in params) { - const param = params[param_id]; + const param = params[param_id] const paramEl = document.createElement('div'); - paramEl.className = 'row'; + paramEl.className = 'row' paramsForm.appendChild(paramEl); paramEl.innerHTML = ` - - `; + ${param.default} + ` } - // Update to react to params change - updateViz(); } document.getElementById('launch').addEventListener('click', () => { From 480652ed94539ff10703c7ba1dbf12479ea5e31c Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 2 Apr 2025 12:15:02 +0200 Subject: [PATCH 16/19] Revert "WIP /cog/viewer algorithms inputs" This reverts commit 31d5277cf3657a39f84ed9d533367d0b8309aff1. --- .../extensions/templates/cog_viewer.html | 73 +------------------ 1 file changed, 2 insertions(+), 71 deletions(-) diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index c46ebb5f9..04d37554b 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -256,16 +256,6 @@
- - -
-
Algorithm
-
- -
-
-
@@ -348,8 +338,7 @@ dataset_statistics: undefined, data_type: undefined, band_descriptions: undefined, - colormap: undefined, - algorithms: undefined, + colormap: undefined } const tilejson_endpoint = '{{ tilejson_endpoint }}' @@ -852,6 +841,7 @@ throw new Error('Network response was not ok.') }) .then(data => { + console.log(data) scope.data_type = data.properties.dtype scope.colormap = data.properties.colormap @@ -920,65 +910,6 @@ .catch(err => { console.warn(err) }) - - - // fetch(`/colorMaps`) then /colorMaps/accent?format=png but one request per colormap pretty heavy - fetch(`/algorithms`) - .then(res => { - if (res.ok) return res.json() - throw new Error('Network response was not ok.') - }) - .then(algorithms => { - console.log('ALGORITHMS', algorithms) - scope.algorithms = algorithms - const algorithmSelector = document.getElementById('algorithm-selector'); - for (const algo_id in algorithms) { - const algo = algorithms[algo_id] - console.log(algo) - const opt = document.createElement('option'); - opt.value = algo_id; - opt.innerHTML = algo['title']; - algorithmSelector.appendChild(opt); - } - algorithmSelector.addEventListener('change', updateAlgorithmParams) - }) - .catch(err => { - console.warn(err) - }) -} - -function updateAlgorithmParams() { - const algorithmSelector = document.getElementById('algorithm-selector'); - console.log('algorithmSelector', algorithmSelector) - const selected = algorithmSelector.selectedOptions[0].value; - console.log("selected", selected) - const params = scope.algorithms[selected]['parameters']; - console.log(params) - - // Recreate div to host params - const paramsElOld = Array.from(algorithmSelector.parentNode.children).find(el => el.id == 'algorithm-params') - if (paramsElOld) { - paramsElOld.remove() - } - // Reproduce the div + form from lat/lon inputs - const paramsEl = document.createElement('div'); - paramsEl.className = 'px6 py6 w-full' - paramsEl.id = 'algorithm-params'; - algorithmSelector.parentNode.appendChild(paramsEl) - const paramsForm = document.createElement('form'); - paramsForm.className = 'grid grid--gut12 mx12 mt12' - paramsForm.id = 'algorithm-params-form'; - paramsEl.appendChild(paramsForm) - for (const param_id in params) { - const param = params[param_id] - const paramEl = document.createElement('div'); - paramEl.className = 'row' - paramsForm.appendChild(paramEl); - paramEl.innerHTML = ` - - ${param.default} - ` - } } document.getElementById('launch').addEventListener('click', () => { From 11667948b2dfdeac92416b4993e890934561b098 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 2 Apr 2025 15:57:45 +0200 Subject: [PATCH 17/19] Add nodata_height coverage to tests --- src/titiler/core/tests/test_algorithms.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index 3628e33f4..9ffb0043c 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -106,6 +106,25 @@ def main(algorithm=Depends(default_algorithms.dependency)): elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) + # # test nodata_height + # # MAPBOX Terrain RGB + # response = client.get("/", params={"algorithm": "terrainrgb", "algorithm_params":json.dumps({"nodata_height":10.0})}) + # assert response.status_code == 200 + # with MemoryFile(response.content) as mem: + # with mem.open() as dst: + # data = dst.read().astype(numpy.float64) + # elevation = -10000 + (((data[0] * 256 * 256) + (data[1] * 256) + data[2]) * 0.1) + # numpy.testing.assert_array_equal(elevation, arr[0]) + + # # TILEZEN Terrarium + # response = client.get("/", params={"algorithm": "terrarium", "algorithm_params":json.dumps({"nodata_height":10.0})}) + # assert response.status_code == 200 + # with MemoryFile(response.content) as mem: + # with mem.open() as dst: + # data = dst.read().astype(numpy.float64) + # elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 + # numpy.testing.assert_array_equal(elevation, arr[0]) + def test_normalized_index(): """test ndi.""" @@ -236,6 +255,14 @@ def test_terrarium(): assert out.array.dtype == "uint8" assert out.array[0, 0, 0] is numpy.ma.masked + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrarium")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0,:,:]] + masked_height = (masked[0] * 256 + masked[1] + masked[2] / 256) - 32768 + numpy.testing.assert_array_equal(masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool")) + def test_terrainrgb(): """test terrainrgb.""" @@ -259,6 +286,14 @@ def test_terrainrgb(): assert out.array.dtype == "uint8" assert out.array[0, 0, 0] is numpy.ma.masked + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrainrgb")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0,:,:]] + masked_height = -10000 + (((masked[0] * 256 * 256) + (masked[1] * 256) + masked[2]) * 0.1) + numpy.testing.assert_array_equal(masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool")) + def test_ops(): """test ops: cast, ceil and floor.""" From e304387ede7f846b25547d608e60e58ba135ef75 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 2 Apr 2025 17:38:20 +0200 Subject: [PATCH 18/19] Discard solution 1 for testing terrainrgb and terrarium --- src/titiler/core/tests/test_algorithms.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index 9ffb0043c..759879616 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -106,26 +106,6 @@ def main(algorithm=Depends(default_algorithms.dependency)): elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) - # # test nodata_height - # # MAPBOX Terrain RGB - # response = client.get("/", params={"algorithm": "terrainrgb", "algorithm_params":json.dumps({"nodata_height":10.0})}) - # assert response.status_code == 200 - # with MemoryFile(response.content) as mem: - # with mem.open() as dst: - # data = dst.read().astype(numpy.float64) - # elevation = -10000 + (((data[0] * 256 * 256) + (data[1] * 256) + data[2]) * 0.1) - # numpy.testing.assert_array_equal(elevation, arr[0]) - - # # TILEZEN Terrarium - # response = client.get("/", params={"algorithm": "terrarium", "algorithm_params":json.dumps({"nodata_height":10.0})}) - # assert response.status_code == 200 - # with MemoryFile(response.content) as mem: - # with mem.open() as dst: - # data = dst.read().astype(numpy.float64) - # elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 - # numpy.testing.assert_array_equal(elevation, arr[0]) - - def test_normalized_index(): """test ndi.""" algo = default_algorithms.get("normalizedIndex")() From 3f469d3fa662be67589b99cf85ac671bd9ab35df Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 2 Apr 2025 20:49:03 +0200 Subject: [PATCH 19/19] pre-commit run --all-files --- src/titiler/core/tests/test_algorithms.py | 17 +++++++++---- .../core/titiler/core/algorithm/dem.py | 25 +++++++++++-------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index 759879616..4be542cf5 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -106,6 +106,7 @@ def main(algorithm=Depends(default_algorithms.dependency)): elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) + def test_normalized_index(): """test ndi.""" algo = default_algorithms.get("normalizedIndex")() @@ -239,9 +240,11 @@ def test_terrarium(): nodata_height = 10.0 algo = default_algorithms.get("terrarium")(nodata_height=nodata_height) out = algo(img) - masked = out.array[:, arr.mask[0,:,:]] + masked = out.array[:, arr.mask[0, :, :]] masked_height = (masked[0] * 256 + masked[1] + masked[2] / 256) - 32768 - numpy.testing.assert_array_equal(masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool")) + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) def test_terrainrgb(): @@ -270,9 +273,13 @@ def test_terrainrgb(): nodata_height = 10.0 algo = default_algorithms.get("terrainrgb")(nodata_height=nodata_height) out = algo(img) - masked = out.array[:, arr.mask[0,:,:]] - masked_height = -10000 + (((masked[0] * 256 * 256) + (masked[1] * 256) + masked[2]) * 0.1) - numpy.testing.assert_array_equal(masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool")) + masked = out.array[:, arr.mask[0, :, :]] + masked_height = -10000 + ( + ((masked[0] * 256 * 256) + (masked[1] * 256) + masked[2]) * 0.1 + ) + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) def test_ops(): diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index ce985e2f6..571315af5 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -1,7 +1,8 @@ """titiler.core.algorithm DEM.""" -import numpy from typing import Optional + +import numpy from pydantic import Field from rasterio import windows from rio_tiler.colormap import apply_cmap, cmap @@ -23,7 +24,7 @@ class HillShade(BaseAlgorithm): azimuth: int = Field(45, ge=0, le=360) angle_altitude: float = Field(45.0, ge=-90.0, le=90.0) buffer: int = Field(3, ge=0, le=99) - z_exaggeration: float = Field(1., ge=1e-6, le=1e6) + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -75,7 +76,7 @@ class Slope(BaseAlgorithm): # parameters buffer: int = Field(3, ge=0, le=99, description="Buffer size for edge effects") - z_exaggeration: float = Field(1., ge=1e-6, le=1e6) + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -178,8 +179,10 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) - if self.nodata_height is not None: - data[img.array.mask[0]] = numpy.clip(self.nodata_height + 32768.0, 0.0, 65535.0) + if self.nodata_height is not None: + data[img.array.mask[0]] = numpy.clip( + self.nodata_height + 32768.0, 0.0, 65535.0 + ) r = data / 256 g = data % 256 b = (data * 256) % 256 @@ -235,13 +238,15 @@ def _range_check(datarange): if _range_check(datarange): raise ValueError(f"Data of {datarange} larger than 256 ** 3") - if self.nodata_height is not None: - data[img.array.mask[0]] = (self.nodata_height - self.baseval) / self.interval + if self.nodata_height is not None: + data[img.array.mask[0]] = ( + self.nodata_height - self.baseval + ) / self.interval data_int32 = data.astype(numpy.int32) - b = (data_int32) & 0xff - g = (data_int32 >> 8) & 0xff - r = (data_int32 >> 16) & 0xff + b = (data_int32) & 0xFF + g = (data_int32 >> 8) & 0xFF + r = (data_int32 >> 16) & 0xFF return ImageData( numpy.ma.stack([r, g, b]).astype(self.output_dtype),