From f977943924317023c84edbf14970d2c87f6303d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ir=C3=A9n=C3=A9e=20Dubourg?= Date: Tue, 4 Feb 2025 14:49:22 +0100 Subject: [PATCH 1/2] fix(RasterTile): partial loading for raster tiles --- src/Process/LayeredMaterialNodeProcessing.js | 20 +++-- src/Provider/DataSourceProvider.js | 18 +++- src/Renderer/RasterTile.js | 89 +++++++++++++++----- 3 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/Process/LayeredMaterialNodeProcessing.js b/src/Process/LayeredMaterialNodeProcessing.js index e53b513111..609ac75305 100644 --- a/src/Process/LayeredMaterialNodeProcessing.js +++ b/src/Process/LayeredMaterialNodeProcessing.js @@ -40,9 +40,15 @@ function buildCommand(view, layer, extentsSource, extentsDestination, requester) requester, priority: materialCommandQueuePriorityFunction(requester.material), earlyDropFunction: refinementCommandCancellationFn, + partialLoading: true, }; } +function computePitchs(textures, extentsDestination) { + return extentsDestination + .map((ext, i) => (ext.offsetToParent(textures[i].extent))); +} + export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { const material = node.material; if (!parent || !material) { @@ -139,14 +145,16 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { const command = buildCommand(context.view, layer, extentsSource, extentsDestination, node); return context.scheduler.execute(command).then( - (result) => { + (results) => { // Does nothing if the layer has been removed while command was being or waiting to be executed if (!node.layerUpdateState[layer.id]) { return; } + const textures = results.map((texture, index) => (texture != null ? texture : + { isTexture: false, extent: extentsDestination[index] })); // TODO: Handle error : result is undefined in provider. throw error - const pitchs = extentsDestination.map((ext, i) => ext.offsetToParent(result[i].extent, nodeLayer.offsetScales[i])); - nodeLayer.setTextures(result, pitchs); + const pitchs = computePitchs(textures, extentsDestination); + nodeLayer.setTextures(textures, pitchs); node.layerUpdateState[layer.id].success(); }, err => handlingError(err, node, layer, targetLevel, context.view)); @@ -212,7 +220,7 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) const command = buildCommand(context.view, layer, extentsSource, extentsDestination, node); return context.scheduler.execute(command).then( - (result) => { + (results) => { // Does nothing if the layer has been removed while command was being or waiting to be executed if (!node.layerUpdateState[layer.id]) { return; @@ -225,8 +233,8 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) node.layerUpdateState[layer.id].noMoreUpdatePossible(); return; } - const pitchs = extentsDestination.map((ext, i) => ext.offsetToParent(result[i].extent, nodeLayer.offsetScales[i])); - nodeLayer.setTextures(result, pitchs); + const pitchs = computePitchs(results, extentsDestination); + nodeLayer.setTextures(results, pitchs); node.layerUpdateState[layer.id].success(); }, err => handlingError(err, node, layer, targetLevel, context.view)); diff --git a/src/Provider/DataSourceProvider.js b/src/Provider/DataSourceProvider.js index 442a7f12fe..3f5a734cfb 100644 --- a/src/Provider/DataSourceProvider.js +++ b/src/Provider/DataSourceProvider.js @@ -3,7 +3,23 @@ export default { const layer = command.layer; const src = command.extentsSource; const dst = command.extentsDestination || src; + const promises = src.map((from, i) => (layer.getData(from, dst[i]))); - return Promise.all(src.map((from, i) => (layer.getData(from, dst[i])))); + // partialLoading sets the return promise as fulfilled if at least one sub-promise is fulfilled + // It waits until all promises are resolved + if (command.partialLoading) { + return Promise.allSettled(promises) + .then((results) => { + const anyFulfilledPromise = results.find(promise => promise.status === 'fulfilled'); + if (!anyFulfilledPromise) { + // All promises failed -> reject + return Promise.reject(new Error('Failed to load any data')); + } + return results.map(prom => (prom.value ? prom.value : null)); + }); + } + + // Without partialLoading, the return promise is rejected as soon as any sub-promise is rejected + return Promise.all(promises); }, }; diff --git a/src/Renderer/RasterTile.js b/src/Renderer/RasterTile.js index af87699426..ac26ec4a11 100644 --- a/src/Renderer/RasterTile.js +++ b/src/Renderer/RasterTile.js @@ -61,12 +61,13 @@ class RasterTile extends THREE.EventDispatcher { initFromParent(parent, extents) { if (parent && parent.level > this.level) { let index = 0; - for (const c of extents) { - for (const texture of parent.textures) { - if (c.isInside(texture.extent)) { - this.setTexture(index++, texture, c.offsetToParent(texture.extent)); - break; - } + const sortedParentTextures = this.sortBestParentTextures(parent.textures); + for (const childExtent of extents) { + const matchingParentTexture = sortedParentTextures + .find(parentTexture => parentTexture && childExtent.isInside(parentTexture.extent)); + if (matchingParentTexture) { + this.setTexture(index++, matchingParentTexture, + childExtent.offsetToParent(matchingParentTexture.extent)); } } @@ -78,6 +79,32 @@ class RasterTile extends THREE.EventDispatcher { } } + sortBestParentTextures(textures) { + const sortByValidity = (a, b) => { + if (a.isTexture === b.isTexture) { + return 0; + } else { + return a.isTexture ? -1 : 1; + } + }; + const sortByZoom = (a, b) => b.extent.zoom - a.extent.zoom; + + return textures.toSorted((a, b) => sortByValidity(a, b) || sortByZoom(a, b)); + } + + disposeRedrawnTextures(newTextures) { + const validTextureIndexes = newTextures + .map((texture, index) => (this.shouldWriteTextureAtIndex(index, texture) ? index : -1)) + .filter(index => index !== -1); + + if (validTextureIndexes.length === newTextures.length) { + // Dispose the whole tile when all textures are overwritten + this.dispose(false); + } else { + this.disposeAtIndexes(validTextureIndexes); + } + } + dispose(removeEvent = true) { if (removeEvent) { this.layer.removeEventListener('visible-property-changed', this._handlerCBEvent); @@ -86,30 +113,43 @@ class RasterTile extends THREE.EventDispatcher { this._listeners = {}; } // TODO: WARNING verify if textures to dispose aren't attached with ancestor - for (const texture of this.textures) { - if (texture.isTexture) { + // Dispose all textures + this.disposeAtIndexes(this.textures.keys()); + this.textures = []; + this.offsetScales = []; + this.level = EMPTY_TEXTURE_ZOOM; + } + + disposeAtIndexes(indexes = null) { + for (const index of indexes) { + const texture = this.textures[index]; + if (texture && texture.isTexture) { texture.dispose(); } } - this.level = EMPTY_TEXTURE_ZOOM; - this.textures = []; - this.offsetScales = []; this.material.layersNeedUpdate = true; } setTexture(index, texture, offsetScale) { - this.level = (texture && texture.extent && (index == 0)) ? texture.extent.zoom : this.level; - this.textures[index] = texture || null; - this.offsetScales[index] = offsetScale; - this.material.layersNeedUpdate = true; + if (this.shouldWriteTextureAtIndex(index, texture)) { + this.level = (texture && texture.extent) ? texture.extent.zoom : this.level; + this.textures[index] = texture || null; + this.offsetScales[index] = offsetScale; + this.material.layersNeedUpdate = true; + } } setTextures(textures, pitchs) { - this.dispose(false); + this.disposeRedrawnTextures(textures); for (let i = 0, il = textures.length; i < il; ++i) { this.setTexture(i, textures[i], pitchs[i]); } } + + shouldWriteTextureAtIndex(index, texture) { + // Do not apply noData texture if current texture is valid + return !this.textures[index] || texture && texture.isTexture; + } } export default RasterTile; @@ -182,8 +222,12 @@ export class RasterElevationTile extends RasterTile { } setTextures(textures, offsetScales) { + const anyValidTexture = textures.find(texture => texture != null); + if (!anyValidTexture) { + return; + } const currentLevel = this.level; - this.replaceNoDataValueFromTexture(textures[0]); + this.replaceNoDataValueFromTexture(anyValidTexture); super.setTextures(textures, offsetScales); this.updateMinMaxElevation(); if (currentLevel !== this.level) { @@ -192,10 +236,11 @@ export class RasterElevationTile extends RasterTile { } updateMinMaxElevation() { - if (this.textures[0] && !this.layer.useColorTextureElevation) { + const firstValidIndex = this.textures.findIndex(texture => texture.isTexture); + if (firstValidIndex !== -1 && !this.layer.useColorTextureElevation) { const { min, max } = computeMinMaxElevation( - this.textures[0], - this.offsetScales[0], + this.textures[firstValidIndex], + this.offsetScales[firstValidIndex], { noDataValue: this.layer.noDataValue, zmin: this.layer.zmin, @@ -214,11 +259,11 @@ export class RasterElevationTile extends RasterTile { return; } // replace no data value with parent texture value or 0 (if no significant value found). - const parentTexture = this.textures[0]; + const parentTexture = this.textures.find(texture => texture != null); const parentDataElevation = parentTexture && parentTexture.image && parentTexture.image.data; const dataElevation = texture.image && texture.image.data; - if (dataElevation && !checkNodeElevationTextureValidity(dataElevation, nodatavalue)) { + if (parentDataElevation && dataElevation && !checkNodeElevationTextureValidity(dataElevation, nodatavalue)) { insertSignificantValuesFromParent(dataElevation, parentDataElevation && dataParent(texture, parentTexture, parentDataElevation, pitch), nodatavalue); } } From 2e136c8a879e86687dbc4b6647048f63b6e78be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ir=C3=A9n=C3=A9e=20Dubourg?= Date: Tue, 4 Feb 2025 15:08:19 +0100 Subject: [PATCH 2/2] fix(RasterTile): fix deepScan issues --- src/Renderer/RasterTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer/RasterTile.js b/src/Renderer/RasterTile.js index ac26ec4a11..812beef8fb 100644 --- a/src/Renderer/RasterTile.js +++ b/src/Renderer/RasterTile.js @@ -120,7 +120,7 @@ class RasterTile extends THREE.EventDispatcher { this.level = EMPTY_TEXTURE_ZOOM; } - disposeAtIndexes(indexes = null) { + disposeAtIndexes(indexes) { for (const index of indexes) { const texture = this.textures[index]; if (texture && texture.isTexture) { @@ -263,7 +263,7 @@ export class RasterElevationTile extends RasterTile { const parentDataElevation = parentTexture && parentTexture.image && parentTexture.image.data; const dataElevation = texture.image && texture.image.data; - if (parentDataElevation && dataElevation && !checkNodeElevationTextureValidity(dataElevation, nodatavalue)) { + if (dataElevation && !checkNodeElevationTextureValidity(dataElevation, nodatavalue)) { insertSignificantValuesFromParent(dataElevation, parentDataElevation && dataParent(texture, parentTexture, parentDataElevation, pitch), nodatavalue); } }