From f8e5cff117e1be53889c2ca93a1efd407f7487b6 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Wed, 16 Oct 2024 16:31:08 +0200 Subject: [PATCH] Feature/Optimization: cache can be created by a callback (async or sync), to avoid premature data creation --- src/tile.js | 64 ++++++++++++++++++++++++++++-------------------- src/tilecache.js | 30 ++++++++++++++++++++--- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/tile.js b/src/tile.js index 9b9604b8..263791ea 100644 --- a/src/tile.js +++ b/src/tile.js @@ -423,8 +423,9 @@ $.Tile.prototype = { * @deprecated */ set context2D(value) { - $.console.error("[Tile.context2D] property has been deprecated. Use [Tile.setData] instead."); + $.console.error("[Tile.context2D] property has been deprecated. Use [Tile.setData] within dedicated update event instead."); this.setData(value, "context2d"); + this.updateRenderTarget(); }, /** @@ -473,26 +474,7 @@ $.Tile.prototype = { if (!this.tiledImage) { return $.Promise.resolve(); //async can access outside its lifetime } - - $.console.assert("TIle.getData requires type argument! got '%s'.", type); - - //we return the data synchronously immediatelly (undefined if conversion happens) - const cache = this.getCache(this._wcKey); - if (!cache) { - const targetCopyKey = this.__restore ? this.originalCacheKey : this.cacheKey; - const origCache = this.getCache(targetCopyKey); - if (!origCache) { - $.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey); - } - - //todo consider calling addCache with callback, which can avoid creating data item only to just discard it - // in case we addCache with existing key and the current tile just gets attached as a reference - // .. or explicitly check that such cache does not exist globally (now checking only locally) - return origCache.getDataAs(type, true).then(data => { - return this.addCache(this._wcKey, data, type, false, false).await(); - }); - } - return cache.getDataAs(type, false); + return this._getOrCreateWorkingCacheData(type); }, /** @@ -521,10 +503,10 @@ $.Tile.prototype = { return null; //async context can access the tile outside its lifetime } - const cache = this.getCache(this._wcKey); + let cache = this.getCache(this._wcKey); if (!cache) { - $.console.error("[Tile::setData] You cannot set data without calling tile.getData()! The working cache is not initialized!"); - return $.Promise.resolve(); + this._getOrCreateWorkingCacheData(undefined); + cache = this.getCache(this._wcKey); } return cache.setDataAs(value, type); }, @@ -611,8 +593,11 @@ $.Tile.prototype = { * @param {string} key cache key, must be unique (we recommend re-using this.cacheTile * value and extend it with some another unique content, by default overrides the existing * main cache used for drawing, if not existing. - * @param {*} data data to cache - this data will be IGNORED if cache already exists! - * @param {string} [type=undefined] data type, will be guessed if not provided + * @param {*} data this data will be IGNORED if cache already exists; therefore if + * `typeof data === 'function'` holds (both async and normal functions), the data is called to obtain + * the data item: this is an optimization to load data only when necessary. + * @param {string} [type=undefined] data type, will be guessed if not provided (not recommended), + * if data is a callback the type is a mandatory field, not setting it results in undefined behaviour * @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey * @param [_safely=true] private * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. @@ -628,6 +613,9 @@ $.Tile.prototype = { "Automated deduction is potentially unsafe: prefer specification of data type explicitly."); this.__typeWarningReported = true; } + if (typeof data === 'function') { + $.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is " + type); + } type = $.convertor.guessType(data); } @@ -677,6 +665,30 @@ $.Tile.prototype = { // as drawers request data for drawing }, + /** + * Initializes working cache if it does not exist. + * @param {string|undefined} type initial cache type to create + * @return {OpenSeadragon.Promise} data-awaiting promise with the cache data + * @private + */ + _getOrCreateWorkingCacheData: function (type) { + const cache = this.getCache(this._wcKey); + if (!cache) { + const targetCopyKey = this.__restore ? this.originalCacheKey : this.cacheKey; + const origCache = this.getCache(targetCopyKey); + if (!origCache) { + $.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey); + } + // Here ensure type is defined, rquired by data callbacks + type = type || origCache.type; + + // Here we use extensively ability to call addCache with callback: working cache is created only if not + // already in memory (=> shared). + return this.addCache(this._wcKey, () => origCache.getDataAs(type, true), type, false, false).await(); + } + return cache.getDataAs(type, false); + }, + /** * Get the number of caches available to this tile * @returns {number} number of caches diff --git a/src/tilecache.js b/src/tilecache.js index f6667bb9..3a941170 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -75,7 +75,7 @@ /** * Await ongoing process so that we get cache ready on callback. - * @returns {Promise} + * @returns {OpenSeadragon.Promise} */ await() { if (!this._promise) { //if not cache loaded, do not fail @@ -403,9 +403,24 @@ // first come first served, data for existing tiles is NOT overridden if (this._tiles.length < 1) { + // Since we IGNORE new data if already initialized, we support 'data getter' + if (typeof data === 'function') { + data = data(); + } + + // If we receive async callback, we consume the async state + if (data instanceof $.Promise) { + this._promise = data.then(d => { + this._data = d; + return d; + }); + this._data = null; + } else { + this._promise = $.Promise.resolve(data); + this._data = data; + } + this._type = type; - this._promise = $.Promise.resolve(data); - this._data = data; this.loaded = true; this._tiles.push(tile); } else if (!this._tiles.includes(tile)) { @@ -734,7 +749,8 @@ * @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache. * Used if options.cacheKey not set. * @param {Image} options.image - The image of the tile to cache. Deprecated. - * @param {*} options.data - The data of the tile to cache. + * @param {*} options.data - The data of the tile to cache. If `typeof data === 'function'` holds, + * the data is called to obtain the data item: this is an optimization to load data only when necessary. * @param {string} [options.dataType] - The data type of the tile to cache. Required. * @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this * function will release an old tile. The cutoff option specifies a tile level at or below which @@ -777,6 +793,12 @@ if (!options.dataType) { $.console.error("[TileCache.cacheTile] options.dataType is newly required. " + "For easier use of the cache system, use the tile instance API."); + + // We need to force data acquisition now to guess the type + if (typeof options.data === 'function') { + $.console.error("[TileCache.cacheTile] options.dataType is mandatory " + + " when data item is a callback!"); + } options.dataType = $.convertor.guessType(options.data); }