diff --git a/src/tile.js b/src/tile.js index eeaf5de2..b86c1684 100644 --- a/src/tile.js +++ b/src/tile.js @@ -33,7 +33,6 @@ */ (function( $ ){ -let _workingCacheIdDealer = 0; /** * @class Tile @@ -252,16 +251,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @private */ this._caches = {}; - /** - * Static Working Cache key to keep cached object (for swapping) when executing modifications. - * Uses unique ID to prevent sharing between other tiles: - * - if some tile initiates processing, all other tiles usually are skipped if they share the data - * - if someone tries to bypass sharing and process all tiles that share data, working caches would collide - * Note that $.now() is not sufficient, there might be tile created in the same millisecond. - * @member {String} - * @private - */ - this._wcKey = `w${_workingCacheIdDealer++}://` + this.originalCacheKey; /** * Processing flag, exempt the tile from removal when there are ongoing updates * @member {Boolean|Number} @@ -273,14 +262,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @private */ this.lastProcess = 0; - /** - * Transforming flag, exempt the tile from any processing since it is being transformed to a drawer-compatible - * format. This process cannot be paused and the tile cannot be touched during the process. Used for tile-locking - * in the data invalidation routine. - * @member {Boolean|Number} - * @private - */ - this.transforming = false; }; /** @lends OpenSeadragon.Tile.prototype */ @@ -420,7 +401,7 @@ $.Tile.prototype = { * @returns {?Image} */ getImage: function() { - $.console.error("[Tile.getImage] property has been deprecated. Use [Tile.getData] instead."); + $.console.error("[Tile.getImage] property has been deprecated. Use 'tile-invalidated' routine event instead."); //this method used to ensure the underlying data model conformed to given type - convert instead of getData() const cache = this.getCache(this.cacheKey); if (!cache) { @@ -445,10 +426,10 @@ $.Tile.prototype = { /** * Get the CanvasRenderingContext2D instance for tile image data drawn * onto Canvas if enabled and available - * @returns {?CanvasRenderingContext2D} + * @returns {CanvasRenderingContext2D|undefined} */ getCanvasContext: function() { - $.console.error("[Tile.getCanvasContext] property has been deprecated. Use [Tile.getData] instead."); + $.console.error("[Tile.getCanvasContext] property has been deprecated. Use 'tile-invalidated' routine event instead."); //this method used to ensure the underlying data model conformed to given type - convert instead of getData() const cache = this.getCache(this.cacheKey); if (!cache) { @@ -464,7 +445,7 @@ $.Tile.prototype = { * @type {CanvasRenderingContext2D} */ get context2D() { - $.console.error("[Tile.context2D] property has been deprecated. Use [Tile.getData] instead."); + $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead."); return this.getCanvasContext(); }, @@ -473,9 +454,12 @@ $.Tile.prototype = { * @deprecated */ set context2D(value) { - $.console.error("[Tile.context2D] property has been deprecated. Use [Tile.setData] within dedicated update event instead."); - this.setData(value, "context2d"); - this.updateRenderTarget(); + $.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead."); + const cache = this._caches[this.cacheKey]; + if (cache) { + this.removeCache(this.cacheKey); + } + this.addCache(this.cacheKey, value, 'context2d', true, false); }, /** @@ -510,128 +494,12 @@ $.Tile.prototype = { }, /** - * Get the data to render for this tile. If no conversion is necessary, get a reference. Else, get a copy - * of the data as desired type. This means that data modification _might_ be reflected on the tile, but - * it is not guaranteed. Use tile.setData() to ensure changes are reflected. - * @param {string} type data type to require - * @return {OpenSeadragon.Promise<*>} data in the desired type, or resolved promise with udnefined if the - * associated cache object is out of its lifespan - */ - getData: function(type) { - if (!this.tiledImage) { - return $.Promise.resolve(); //async can access outside its lifetime - } - return this._getOrCreateWorkingCacheData(type); - }, - - /** - * Restore the original data data for this tile - * @param {boolean} freeIfUnused if true, restoration frees cache along the way of the tile lifecycle - */ - restore: function(freeIfUnused = true) { - if (!this.tiledImage) { - return; //async context can access the tile outside its lifetime - } - - this.__restoreRequestedFree = freeIfUnused; - if (this.originalCacheKey !== this.cacheKey) { - this.__restore = true; - } - // Somebody has called restore on this tile, make sure we delete working cache in case there was some - this.removeCache(this._wcKey, true); - }, - - /** - * Set main cache data - * @param {*} value - * @param {?string} type data type to require - * @return {OpenSeadragon.Promise<*>} - */ - setData: function(value, type) { - if (!this.tiledImage) { - return Promise.resolve(); //async context can access the tile outside its lifetime - } - - let cache = this.getCache(this._wcKey); - if (!cache) { - this._getOrCreateWorkingCacheData(undefined); - cache = this.getCache(this._wcKey); - } - return cache.setDataAs(value, type); - }, - - - /** - * Optimizazion: prepare target cache for subsequent use in rendering, and perform updateRenderTarget() - * The main idea of this function is that it must be ASYNCHRONOUS since there might be additional processing - * happening due to underlying drawer requirements. - * @return {OpenSeadragon.Promise} + * Cache key for main cache that is 'cache-equal', but different from original cache key + * @return {string} * @private */ - updateRenderTargetWithDataTransform: function (drawerId, supportedFormats, usePrivateCache, processTimestamp) { - // Now, if working cache exists, we set main cache to the working cache --> prepare - let cache = this.getCache(this._wcKey); - if (cache) { - return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache).then(c => { - if (c && processTimestamp && this.processing === processTimestamp) { - this.updateRenderTarget(); - } - }); - } - - // If we requested restore, perform now - if (this.__restore) { - cache = this.getCache(this.originalCacheKey); - - this.tiledImage._tileCache.restoreTilesThatShareOriginalCache( - this, cache, this.__restoreRequestedFree - ); - this.__restore = false; - return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache).then((c) => { - if (c && processTimestamp && this.processing === processTimestamp) { - this.updateRenderTarget(); - } - }); - } - - cache = this.getCache(); - return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache); - }, - - /** - * Resolves render target: changes might've been made to the rendering pipeline: - * - working cache is set: make sure main cache will be replaced - * - working cache is unset: make sure main cache either gets updated to original data or stays (based on this.__restore) - * - * The main idea of this function is that it is SYNCHRONOUS, e.g. can perform in-place cache swap to update - * before any rendering occurs. - * @private - */ - updateRenderTarget: function (_allowTileNotLoaded = false) { - // Check if we asked for restore, and make sure we set it to false since we update the whole cache state - const requestedRestore = this.__restore; - this.__restore = false; - - // Now, if working cache exists, we set main cache to the working cache, since it has been updated - // if restore() was called last, then working cache was deleted (does not exist) - const cache = this.getCache(this._wcKey); - if (cache) { - let newCacheKey = this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey; - this.tiledImage._tileCache.consumeCache({ - tile: this, - victimKey: this._wcKey, - consumerKey: newCacheKey, - tileAllowNotLoaded: _allowTileNotLoaded - }); - this.cacheKey = newCacheKey; - } else if (requestedRestore) { - // If we requested restore, perform now - this.tiledImage._tileCache.restoreTilesThatShareOriginalCache( - this, this.getCache(this.originalCacheKey), this.__restoreRequestedFree - ); - } - // If transforming was set, we finished: drawer transform always finishes with updateRenderTarget() - this.transforming = false; + buildDistinctMainCacheKey: function () { + return this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey; }, /** @@ -648,10 +516,10 @@ $.Tile.prototype = { }, /** - * Set tile cache, possibly multiple with custom key - * @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. + * Create tile cache for given data object. NOTE: if the existing cache already exists, + * data parameter is ignored and inherited from the existing cache object. + * + * @param {string} key cache key, if unique, new cache object is created, else existing cache attached * @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. @@ -663,7 +531,8 @@ $.Tile.prototype = { * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. */ addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) { - if (!this.tiledImage) { + const tiledImage = this.tiledImage; + if (!tiledImage) { return null; //async can access outside its lifetime } @@ -679,35 +548,83 @@ $.Tile.prototype = { type = $.convertor.guessType(data); } - const writesToRenderingCache = key === this.cacheKey; - if (writesToRenderingCache && _safely) { + const overwritesMainCache = key === this.cacheKey; + if (_safely && (overwritesMainCache || setAsMain)) { // Need to get the supported type for rendering out of the active drawer. - const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats(); + const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats(); const conversion = $.convertor.getConversionPath(type, supportedTypes); $.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" + - "to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type); + `to render. Make sure OpenSeadragon.convertor was taught to convert ${type} to (one of): ${conversion.toString()}`); } - const cachedItem = this.tiledImage._tileCache.cacheTile({ + const cachedItem = tiledImage._tileCache.cacheTile({ data: data, dataType: type, tile: this, cacheKey: key, - //todo consider caching this on a tiled image level - cutoff: this.__cutoff || this.tiledImage.source.getClosestLevel(), + cutoff: tiledImage.source.getClosestLevel(), }); const havingRecord = this._caches[key]; if (havingRecord !== cachedItem) { this._caches[key] = cachedItem; + if (havingRecord) { + havingRecord.removeTile(this); + tiledImage._tileCache.safeUnloadCache(havingRecord); + } } // Update cache key if differs and main requested - if (!writesToRenderingCache && setAsMain) { + if (!overwritesMainCache && setAsMain) { this._updateMainCacheKey(key); } return cachedItem; }, + + /** + * Add cache object to the tile + * + * @param {string} key cache key, if unique, new cache object is created, else existing cache attached + * @param {OpenSeadragon.CacheRecord} cache the cache object to attach to this tile + * @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey, + * no effect if key === this.cacheKey + * @param [_safely=true] private + * @returns {OpenSeadragon.CacheRecord|null} - Returns cache parameter reference if attached. + */ + setCache(key, cache, setAsMain = false, _safely = true) { + const tiledImage = this.tiledImage; + if (!tiledImage) { + return null; //async can access outside its lifetime + } + + const overwritesMainCache = key === this.cacheKey; + if (_safely) { + $.console.assert(cache instanceof $.CacheRecord, "[Tile.setCache] cache must be a CacheRecord object!"); + if (overwritesMainCache || setAsMain) { + // Need to get the supported type for rendering out of the active drawer. + const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats(); + const conversion = $.convertor.getConversionPath(cache.type, supportedTypes); + $.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" + + `to render. Make sure OpenSeadragon.convertor was taught to convert ${cache.type} to (one of): ${conversion.toString()}`); + } + } + + const havingRecord = this._caches[key]; + if (havingRecord !== cache) { + this._caches[key] = cache; + if (havingRecord) { + havingRecord.removeTile(this); + tiledImage._tileCache.safeUnloadCache(havingRecord); + } + } + + // Update cache key if differs and main requested + if (!overwritesMainCache && setAsMain) { + this._updateMainCacheKey(key); + } + return cache; + }, + /** * Sets the main cache key for this tile and * performs necessary updates @@ -717,36 +634,11 @@ $.Tile.prototype = { _updateMainCacheKey: function(value) { let ref = this._caches[this._cKey]; if (ref) { - // make sure we free drawer internal cache + // make sure we free drawer internal cache if people change cache key externally + // todo make sure this is really needed even after refactoring ref.destroyInternalCache(); } this._cKey = value; - // we do not trigger redraw, this is handled within cache - // 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); }, /** @@ -761,12 +653,15 @@ $.Tile.prototype = { * Free tile cache. Removes by default the cache record if no other tile uses it. * @param {string} key cache key, required * @param {boolean} [freeIfUnused=true] set to false if zombie should be created + * @return {OpenSeadragon.CacheRecord|undefined} reference to the cache record if it was removed, + * undefined if removal was refused to perform (e.g. does not exist, it is an original data target etc.) */ removeCache: function(key, freeIfUnused = true) { - if (!this._caches[key]) { + const deleteTarget = this._caches[key]; + if (!deleteTarget) { // try to erase anyway in case the cache got stuck in memory this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, true); - return; + return undefined; } const currentMainKey = this.cacheKey, @@ -776,7 +671,7 @@ $.Tile.prototype = { if (!sameBuiltinKeys && originalDataKey === key) { $.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!", "If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData()."); - return; + return undefined; } if (currentMainKey === key) { @@ -786,13 +681,14 @@ $.Tile.prototype = { } else { $.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!", "If you want to remove the main cache, first set different cache as main with tile.addCache()"); - return; + return undefined; } } if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, false)) { //if we managed to free tile from record, we are sure we decreased cache count delete this._caches[key]; } + return deleteTarget; }, /** @@ -826,8 +722,8 @@ $.Tile.prototype = { // the sketch canvas to the top and left and we must use negative coordinates to repaint it // to the main canvas. In that case, some browsers throw: // INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value. - var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2)); - var y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2)); + const x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2)); + const y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2)); return new $.Point(x, y).minus( this.position .times($.pixelDensityRatio) diff --git a/src/tilecache.js b/src/tilecache.js index a668edb4..862b7254 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -163,7 +163,7 @@ /** * Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g. - * cache swap performed on working cache (consumeCache()) must be synchronous such that cache is always + * cache swap performed on working cache (replaceCache()) must be synchronous such that cache is always * ready to render, and swaps atomically between render calls. * * @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer @@ -190,8 +190,9 @@ let internalCache = this[DRAWER_INTERNAL_CACHE]; internalCache = internalCache && internalCache[drawer.getId()]; if (keepInternalCopy && !internalCache) { - $.console.warn("Attempt to render %s that is not prepared with drawer requesting " + - "internal cache! This might introduce artifacts.", this.toString()); + $.console.warn("Attempt to render cache that is not prepared for current drawer " + + "supported format: the preparation should've happened after tile processing has finished.", + this, tileToDraw); this.prepareForRendering(drawer.getId(), supportedTypes, keepInternalCopy) .then(() => this._triggerNeedsDraw()); @@ -211,8 +212,10 @@ } if (!supportedTypes.includes(internalCache.type)) { - $.console.warn("Attempt to render %s that is not prepared for current drawer " + - "supported format: the preparation should've happened after tile processing has finished.", this.toString()); + $.console.warn("Attempt to render cache that is not prepared for current drawer " + + "supported format: the preparation should've happened after tile processing has finished.", + Object.entries(this[DRAWER_INTERNAL_CACHE]), + this, tileToDraw); internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0]) .then(() => this._triggerNeedsDraw()); @@ -226,8 +229,8 @@ * @private * @param drawerId * @param supportedTypes - * @param keepInternalCopy - + * @param keepInternalCopy if a drawer requests internal copy, it means it can only use + * given cache for itself, cannot be shared -> initialize privately * @return {OpenSeadragon.Promise | null} * reference to the cache processed for drawer rendering requirements, or null on error */ @@ -330,10 +333,12 @@ * Conversion requires tile references: * keep the most 'up to date' ref here. It is called and managed automatically. * @param {OpenSeadragon.Tile} ref + * @return {OpenSeadragon.CacheRecord} self reference for builder pattern * @private */ withTileReference(ref) { this._tRef = ref; + return this; } /** @@ -642,9 +647,11 @@ * Must be called before transformTo or setDataAs. To keep * compatible api with CacheRecord where tile refs are known. * @param {OpenSeadragon.Tile} referenceTile reference tile for conversion + * @return {OpenSeadragon.SimpleCacheRecord} self reference for builder pattern */ withTileReference(referenceTile) { this._temporaryTileRef = referenceTile; + return this; } /** @@ -908,51 +915,92 @@ } /** - * Consume cache by another cache + * Inject new cache to the system + * @param {Object} options + * @param {OpenSeadragon.Tile} options.tile - Reference tile. All tiles sharing original data will be affected. + * @param {OpenSeadragon.CacheRecord} options.cache - Cache that will be injected. + * @param {String} options.targetKey - The target cache key to inhabit. Can replace existing cache. + * @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey. + * Otherwise, if consumerKey==tile.cacheKey the cache is set as main too. + * @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed, + * this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but + * it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready) + * @private + */ + injectCache(options) { + const targetKey = options.targetKey, + tile = options.tile; + if (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading) { + $.console.warn("Attempt to inject cache on tile in invalid state: this is probably a bug!"); + return; + } + const consumer = this._cachesLoaded[targetKey]; + if (consumer) { + // We need to avoid async execution here: replace consumer instead of overwriting the data. + const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy + for (let tile of iterateTiles) { + this.unloadCacheForTile(tile, targetKey, true, false); + } + } + if (this._cachesLoaded[targetKey]) { + $.console.error("The inject routine should've freed cache!"); + } + + const cache = options.cache; + this._cachesLoaded[targetKey] = cache; + + // Update cache: add the new cache, we must add since we removed above with unloadCacheForTile() + for (let t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles + t.setCache(targetKey, cache, options.setAsMainCache, false); + } + } + + /** + * Replace cache (and update tile references) by another cache * @param {Object} options * @param {OpenSeadragon.Tile} options.tile - The tile to own ot add record for the cache. * @param {String} options.victimKey - Cache that will be erased. In fact, the victim _replaces_ consumer, * inheriting its tiles and key. * @param {String} options.consumerKey - The cache that consumes the victim. In fact, it gets destroyed and * replaced by victim, which inherits all its metadata. + * @param {Boolean} options.setAsMainCache - If true, tiles main cache gets updated to consumerKey. + * Otherwise, if consumerKey==tile.cacheKey the cache is set as main too. * @param {Boolean} options.tileAllowNotLoaded - if true, tile that is not loaded is also processed, * this is internal parameter used in tile-loaded completion routine, as we need to prepare tile but * it is not yet loaded and cannot be marked as so (otherwise the system would think it is ready) * @private */ - consumeCache(options) { - const victim = this._cachesLoaded[options.victimKey], + replaceCache(options) { + const victimKey = options.victimKey, + consumerKey = options.consumerKey, + victim = this._cachesLoaded[victimKey], tile = options.tile; if (!victim || (!options.tileAllowNotLoaded && !tile.loaded && !tile.loading)) { - $.console.warn("Attempt to consume non-existent cache: this is probably a bug!"); + $.console.warn("Attempt to consume cache on tile in invalid state: this is probably a bug!"); return; } - const consumer = this._cachesLoaded[options.consumerKey]; - let tiles = [...tile.getCache()._tiles]; - + const consumer = this._cachesLoaded[consumerKey]; if (consumer) { // We need to avoid async execution here: replace consumer instead of overwriting the data. const iterateTiles = [...consumer._tiles]; // unloadCacheForTile() will modify the array, use a copy for (let tile of iterateTiles) { - this.unloadCacheForTile(tile, options.consumerKey, true, false); + this.unloadCacheForTile(tile, consumerKey, true, false); } } - if (this._cachesLoaded[options.consumerKey]) { - console.error("The routine should've freed cache!"); + if (this._cachesLoaded[consumerKey]) { + $.console.error("The consume routine should've freed cache!"); } // Just swap victim to become new consumer const resultCache = this.renameCache({ - oldCacheKey: options.victimKey, - newCacheKey: options.consumerKey + oldCacheKey: victimKey, + newCacheKey: consumerKey }); if (resultCache) { // Only one cache got working item, other caches were idle: update cache: add the new cache - // we can add since we removed above with unloadCacheForTile() - for (let tile of tiles) { - if (tile !== options.tile) { - tile.addCache(options.consumerKey, resultCache.data, resultCache.type, true, false); - } + // we must add since we removed above with unloadCacheForTile() + for (let t of tile.getCache(tile.originalCacheKey)._tiles) { // grab all cache-equal tiles + t.setCache(consumerKey, resultCache, options.setAsMainCache, false); } } } @@ -967,9 +1015,11 @@ */ restoreTilesThatShareOriginalCache(tile, originalCache, freeIfUnused) { for (let t of originalCache._tiles) { - this.unloadCacheForTile(t, t.cacheKey, freeIfUnused, false); - delete t._caches[t.cacheKey]; - t.cacheKey = t.originalCacheKey; + if (t.cacheKey !== t.originalCacheKey) { + this.unloadCacheForTile(t, t.cacheKey, freeIfUnused, true); + delete t._caches[t.cacheKey]; + t.cacheKey = t.originalCacheKey; + } } } @@ -1089,6 +1139,25 @@ return this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey]; } + /** + * Delete cache safely from the system if it is not needed + * @param {OpenSeadragon.CacheRecord} cache + */ + safeUnloadCache(cache) { + if (cache && !cache._destroyed && cache.getTileCount() < 1) { + for (let i in this._zombiesLoaded) { + const c = this._zombiesLoaded[i]; + if (c === cache) { + delete this._zombiesLoaded[i]; + c.destroy(); + return; + } + } + $.console.error("Attempt to delete an orphan cache that is not in zombie list: this could be a bug!", cache); + cache.destroy(); + } + } + /** * Delete cache record for a given til * @param {OpenSeadragon.Tile} tile diff --git a/src/tiledimage.js b/src/tiledimage.js index 73b9e8f5..44a788a4 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1179,7 +1179,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag ajaxHeaders = {}; } if (!$.isPlainObject(ajaxHeaders)) { - console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); + $.console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); return; } @@ -1881,32 +1881,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {OpenSeadragon.Tile} tile */ _tryFindTileCacheRecord: function(tile) { - let record = this._tileCache.getCacheRecord(tile.cacheKey); + let record = this._tileCache.getCacheRecord(tile.originalCacheKey); if (!record) { return false; } - - // if we find existing record, check the original data of existing tile of this record - let baseTile = record._tiles[0]; - if (!baseTile) { - // zombie cache -> revive, it's okay to use current tile as state inherit point since there is no state - baseTile = tile; - } - - // Setup tile manually, data can be null -> we already have existing cache to share, share also caches - tile.tiledImage = this; - tile.addCache(baseTile.originalCacheKey, null, record.type, false, false); - if (baseTile.cacheKey !== baseTile.originalCacheKey) { - tile.addCache(baseTile.cacheKey, null, record.type, true, false); - } - - tile.hasTransparency = tile.hasTransparency || this.source.hasTransparency( - undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData - ); - - tile.loading = false; - tile.loaded = true; + tile.loading = true; + this._setTileLoaded(tile, record.data, null, null, record.type); return true; }, @@ -2154,7 +2135,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag function markTileAsReady() { tile.lastProcess = false; tile.processing = false; - tile.transforming = false; const fallbackCompletion = getCompletionCallback(); @@ -2185,16 +2165,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag resolver = resolve; }), get image() { - $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile.getData()' instead."); + $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead."); return data; }, get data() { - $.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile.getData()' instead."); + $.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead."); return data; }, getCompletionCallback: function () { $.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " + - "use async event handlers instead, execution order is deducted by addHandler(...) priority"); + "use async event handlers instead, execution order is deducted by addHandler(...) priority argument."); return getCompletionCallback(); }, }).catch(() => { @@ -2207,8 +2187,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag const updatePromise = _this.viewer.world.requestTileInvalidateEvent([tile], now, false, true); updatePromise.then(markTileAsReady); } else { - // In case we did not succeed in tile restoration, request invalidation - // Tile-loaded not called on each tile, but only on tiles with new data! Verify we share the main cache + // Tile-invalidated not called on each tile, but only on tiles with new data! Verify we share the main cache const origCache = tile.getCache(tile.originalCacheKey); for (let t of origCache._tiles) { @@ -2218,11 +2197,18 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag // add reference also to the main cache, no matter what the other tile state has // completion of the invaldate event should take care of all such tiles const targetMainCache = t.getCache(); - tile.addCache(t.cacheKey, () => { - $.console.error("Attempt to share main cache with existing tile should not trigger data getter!"); - return targetMainCache.data; - }, targetMainCache.type, true, false); + tile.setCache(t.cacheKey, targetMainCache, true, false); break; + } else if (t.processing) { + console.log("ENCOUNTERED LOADING TILE!!!"); + let internval = setInterval(() => { + if (t.processing) { + clearInterval(internval); + console.log("FINISHED!!!!!"); + markTileAsReady(); + } + }, 500); + return; } } markTileAsReady(); diff --git a/src/viewer.js b/src/viewer.js index 558d2cc4..4450158c 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1135,7 +1135,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, ajaxHeaders = {}; } if (!$.isPlainObject(ajaxHeaders)) { - console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); + $.console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); return; } if (propagate === undefined) { diff --git a/src/world.js b/src/world.js index a31641f5..fc43a8b2 100644 --- a/src/world.js +++ b/src/world.js @@ -276,7 +276,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W // We allow re-execution on tiles that are in process but have too low processing timestamp, // which must be solved by ensuring subsequent data calls in the suddenly outdated processing // pipeline take no effect. - if (!tile || (!_allowTileUnloaded && !tile.loaded) || tile.transforming) { + if (!tile || (!_allowTileUnloaded && !tile.loaded)) { continue; } const tileCache = tile.getCache(tile.originalCacheKey); @@ -308,19 +308,95 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W const drawerId = this.viewer.drawer.getId(); const jobList = tileList.map(tile => { - if (restoreTiles) { - tile.restore(); - } + const tiledImage = tile.tiledImage; + const originalCache = tile.getCache(tile.originalCacheKey); + let workingCache = null; + const getWorkingCacheData = (type) => { + if (workingCache) { + return workingCache.getDataAs(type, false); + } + + const targetCopyKey = restoreTiles ? tile.originalCacheKey : tile.cacheKey; + const origCache = tile.getCache(targetCopyKey); + if (!origCache) { + $.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey); + return $.Promise.reject(); + } + // Here ensure type is defined, rquired by data callbacks + type = type || origCache.type; + workingCache = new $.CacheRecord().withTileReference(tile); + return origCache.getDataAs(type, true).then(data => { + workingCache.addTile(tile, data, type); + return workingCache.data; + }); + }; + const setWorkingCacheData = (value, type) => { + if (!workingCache) { + workingCache = new $.CacheRecord().withTileReference(tile); + workingCache.addTile(tile, value, type); + } else { + workingCache.setDataAs(value, type); + } + }; + const atomicCacheSwap = () => { + if (workingCache) { + let newCacheKey = tile.buildDistinctMainCacheKey(); + tiledImage._tileCache.injectCache({ + tile: tile, + cache: workingCache, + targetKey: newCacheKey, + setAsMainCache: true, + tileAllowNotLoaded: false //todo what if called from load event? + }); + } else if (restoreTiles) { + // If we requested restore, perform now + tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true); + } + }; + + //todo docs return eventTarget.raiseEventAwaiting('tile-invalidated', { tile: tile, - tiledImage: tile.tiledImage, - }, tile.getCache(tile.originalCacheKey)).then(cacheKey => { - if (cacheKey.__invStamp === tStamp) { - // asynchronous finisher - tile.transforming = tStamp; - return tile.updateRenderTargetWithDataTransform(drawerId, supportedFormats, keepInternalCacheCopy, tStamp).then(() => { - cacheKey.__invStamp = null; + tiledImage: tiledImage, + outdated: () => originalCache.__invStamp !== tStamp, + getData: getWorkingCacheData, + setData: setWorkingCacheData, + resetData: () => { + workingCache.destroy(); + workingCache = null; + } + }).then(_ => { + if (originalCache.__invStamp === tStamp) { + if (workingCache) { + return workingCache.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(c => { + if (c && originalCache.__invStamp === tStamp) { + atomicCacheSwap(); + originalCache.__invStamp = null; + } + }); + } + + // If we requested restore, perform now + if (restoreTiles) { + const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey); + + tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, freshOriginalCacheRef, true); + return freshOriginalCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then((c) => { + if (c && originalCache.__invStamp === tStamp) { + atomicCacheSwap(); + originalCache.__invStamp = null; + } + }); + } + + const freshMainCacheRef = tile.getCache(); + return freshMainCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(() => { + originalCache.__invStamp = null; }); + + } else if (workingCache) { + workingCache.destroy(); + workingCache = null; } return null; }).catch(e => { @@ -332,7 +408,6 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W for (let tile of markedTiles) { tile.lastProcess = false; tile.processing = false; - tile.transforming = false; } this.draw(); }); diff --git a/test/demo/filtering-plugin/demo.js b/test/demo/filtering-plugin/demo.js index f495f3d4..1da03828 100644 --- a/test/demo/filtering-plugin/demo.js +++ b/test/demo/filtering-plugin/demo.js @@ -807,7 +807,6 @@ async function processTile(tile) { console.log("Selected tile", tile); await Promise.all([ updateCanvas(document.getElementById("tile-original"), tile, tile.originalCacheKey), - updateCanvas(document.getElementById("tile-working"), tile, tile._wcKey), updateCanvas(document.getElementById("tile-main"), tile, tile.cacheKey), ]); } diff --git a/test/demo/filtering-plugin/index.html b/test/demo/filtering-plugin/index.html index 5c3f3b68..e8b9632e 100644 --- a/test/demo/filtering-plugin/index.html +++ b/test/demo/filtering-plugin/index.html @@ -73,7 +73,6 @@
-
diff --git a/test/demo/filtering-plugin/plugin.js b/test/demo/filtering-plugin/plugin.js index b126108a..566b9bbe 100644 --- a/test/demo/filtering-plugin/plugin.js +++ b/test/demo/filtering-plugin/plugin.js @@ -56,15 +56,15 @@ setOptions(this, options); async function applyFilters(e) { - const tile = e.tile, - tiledImage = e.tiledImage, + const tiledImage = e.tiledImage, processors = getFiltersProcessors(self, tiledImage); if (processors.length === 0) { return; } - const contextCopy = await tile.getData('context2d'); + const contextCopy = await e.getData('context2d'); + if (!contextCopy) return; if (contextCopy.canvas.width === 0) { debugger; @@ -79,7 +79,7 @@ await processors[i](contextCopy); } - await tile.setData(contextCopy, 'context2d'); + await e.setData(contextCopy, 'context2d'); } catch (e) { // pass, this is error caused by canvas being destroyed & replaced } diff --git a/test/demo/plugin-data-modification-interaction.html b/test/demo/plugin-data-modification-interaction.html index de3941e3..c1514769 100644 --- a/test/demo/plugin-data-modification-interaction.html +++ b/test/demo/plugin-data-modification-interaction.html @@ -89,25 +89,28 @@