diff --git a/src/htmldrawer.js b/src/htmldrawer.js index fa613dbf..34f9fce4 100644 --- a/src/htmldrawer.js +++ b/src/htmldrawer.js @@ -68,12 +68,55 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{ this.viewer.rejectEventHandler("tile-drawing", "The HTMLDrawer does not raise the tile-drawing event"); // Since the tile-drawn event is fired by this drawer, make sure handlers can be added for it this.viewer.allowEventHandler("tile-drawn"); + + // works with canvas & image objects + function _prepareTile(tile, data) { + const element = $.makeNeutralElement( "div" ); + const imgElement = data.cloneNode(); + imgElement.style.msInterpolationMode = "nearest-neighbor"; + imgElement.style.width = "100%"; + imgElement.style.height = "100%"; + + const style = element.style; + style.position = "absolute"; + + return { + element, imgElement, style, data + }; + } + + // The actual placing logics will not happen at draw event, but when the cache is created: + $.convertor.learn("context2d", HTMLDrawer.canvasCacheType, (t, d) => _prepareTile(t, d.canvas), 1, 1); + $.convertor.learn("image", HTMLDrawer.imageCacheType, _prepareTile, 1, 1); + // Also learn how to move back, since these elements can be just used as-is + $.convertor.learn(HTMLDrawer.canvasCacheType, "context2d", (t, d) => d.data.getContext('2d'), 1, 3); + $.convertor.learn(HTMLDrawer.imageCacheType, "image", (t, d) => d.data, 1, 3); + + function _freeTile(data) { + if ( data.imgElement && data.imgElement.parentNode ) { + data.imgElement.parentNode.removeChild( data.imgElement ); + } + if ( data.element && data.element.parentNode ) { + data.element.parentNode.removeChild( data.element ); + } + } + + $.convertor.learnDestroy(HTMLDrawer.canvasCacheType, _freeTile); + $.convertor.learnDestroy(HTMLDrawer.imageCacheType, _freeTile); + } + + static get imageCacheType() { + return 'htmlDrawer[image]'; + } + + static get canvasCacheType() { + return 'htmlDrawer[canvas]'; } /** * @returns {Boolean} always true */ - static isSupported(){ + static isSupported() { return true; } @@ -86,7 +129,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{ } getSupportedDataFormats() { - return ["image"]; + return [HTMLDrawer.imageCacheType]; } /** @@ -215,41 +258,25 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{ //EXPERIMENTAL - trying to figure out how to scale the container // content during animation of the container size. - if ( !tile.element ) { - const image = this.getDataToDraw(tile); - if (!image) { - return; - } - - tile.element = $.makeNeutralElement( "div" ); - tile.imgElement = image.cloneNode(); - tile.imgElement.style.msInterpolationMode = "nearest-neighbor"; - tile.imgElement.style.width = "100%"; - tile.imgElement.style.height = "100%"; - - tile.style = tile.element.style; - tile.style.position = "absolute"; + const dataObject = this.getDataToDraw(tile); + if ( dataObject.element.parentNode !== container ) { + container.appendChild( dataObject.element ); + } + if ( dataObject.imgElement.parentNode !== dataObject.element ) { + dataObject.element.appendChild( dataObject.imgElement ); } - if ( tile.element.parentNode !== container ) { - container.appendChild( tile.element ); - } - if ( tile.imgElement.parentNode !== tile.element ) { - tile.element.appendChild( tile.imgElement ); - } - - tile.style.top = tile.position.y + "px"; - tile.style.left = tile.position.x + "px"; - tile.style.height = tile.size.y + "px"; - tile.style.width = tile.size.x + "px"; + dataObject.style.top = tile.position.y + "px"; + dataObject.style.left = tile.position.x + "px"; + dataObject.style.height = tile.size.y + "px"; + dataObject.style.width = tile.size.x + "px"; if (tile.flipped) { - tile.style.transform = "scaleX(-1)"; + dataObject.style.transform = "scaleX(-1)"; } - $.setElementOpacity( tile.element, tile.opacity ); + $.setElementOpacity( dataObject.element, tile.opacity ); } - } $.HTMLDrawer = HTMLDrawer; diff --git a/src/openseadragon.js b/src/openseadragon.js index d52c9ae0..c5f2ce5c 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -2379,6 +2379,14 @@ function OpenSeadragon( options ){ * @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials * @throws {Error} * @returns {XMLHttpRequest} + *//** + * Makes an AJAX request. + * @param {String} url - the url to request + * @param {Function} onSuccess + * @param {Function} onError + * @throws {Error} + * @returns {XMLHttpRequest} + * @deprecated deprecated way of calling this function */ makeAjaxRequest: function( url, onSuccess, onError ) { var withCredentials; @@ -2388,7 +2396,6 @@ function OpenSeadragon( options ){ // Note that our preferred API is that you pass in a single object; the named // arguments are for legacy support. - // FIXME ^ are we ready to drop legacy support? since we abandoned old ES... if( $.isPlainObject( url ) ){ onSuccess = url.success; onError = url.error; @@ -2397,6 +2404,8 @@ function OpenSeadragon( options ){ responseType = url.responseType || null; postData = url.postData || null; url = url.url; + } else { + $.console.warn("OpenSeadragon.makeAjaxRequest() deprecated usage!"); } var protocol = $.getUrlProtocol( url ); diff --git a/src/tile.js b/src/tile.js index 263791ea..95e41c23 100644 --- a/src/tile.js +++ b/src/tile.js @@ -160,26 +160,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @memberof OpenSeadragon.Tile# */ this.loading = false; - - /** - * The HTML div element for this tile - * @member {Element} element - * @memberof OpenSeadragon.Tile# - */ - this.element = null; - /** - * The HTML img element for this tile. - * @member {Element} imgElement - * @memberof OpenSeadragon.Tile# - */ - this.imgElement = null; - - /** - * The alias of this.element.style. - * @member {String} style - * @memberof OpenSeadragon.Tile# - */ - this.style = null; /** * This tile's position on screen, in pixels. * @member {OpenSeadragon.Point} position @@ -365,6 +345,63 @@ $.Tile.prototype = { return this.getUrl(); }, + /** + * The HTML div element for this tile + * @member {Element} element + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get element() { + $.console.error("Tile::element property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.element; + }, + + /** + * The HTML img element for this tile. + * @member {Element} imgElement + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get imgElement() { + $.console.error("Tile::imgElement property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.imgElement; + }, + + /** + * The alias of this.element.style. + * @member {String} style + * @memberof OpenSeadragon.Tile# + * @deprecated + */ + get style() { + $.console.error("Tile::style property is deprecated. Use cache API instead. Moreover, this property might be unstable."); + const cache = this.getCache(); + if (!cache || !cache.loaded) { + return null; + } + if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) { + $.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!"); + return null; + } + return cache.data.style; + }, + /** * Get the Image object for this tile. * @returns {?Image} @@ -486,10 +523,12 @@ $.Tile.prototype = { return; //async context can access the tile outside its lifetime } + this.__restoreRequestedFree = freeIfUnused; if (this.originalCacheKey !== this.cacheKey) { - this.__restoreRequestedFree = freeIfUnused; 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); }, /** @@ -528,7 +567,7 @@ $.Tile.prototype = { const cache = this.getCache(this.originalCacheKey); this.tiledImage._tileCache.restoreTilesThatShareOriginalCache( - this, cache + this, cache, this.__restoreRequestedFree ); this.__restore = false; return cache.prepareForRendering(drawerId, supportedFormats, usePrivateCache, this.processing); @@ -555,7 +594,7 @@ $.Tile.prototype = { //TODO IMPLEMENT LOCKING AND IGNORE PIPELINE OUT OF THESE CALLS // Now, if working cache exists, we set main cache to the working cache, since it has been updated - const cache = this.getCache(this._wcKey); + const cache = !requestedRestore && this.getCache(this._wcKey); if (cache) { let newCacheKey = this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey; this.tiledImage._tileCache.consumeCache({ @@ -569,7 +608,7 @@ $.Tile.prototype = { // If we requested restore, perform now if (requestedRestore) { this.tiledImage._tileCache.restoreTilesThatShareOriginalCache( - this, this.getCache(this.originalCacheKey) + this, this.getCache(this.originalCacheKey), this.__restoreRequestedFree ); } // Else no work to be done @@ -805,17 +844,22 @@ $.Tile.prototype = { }, /** - * Removes tile from its container. - * @function + * Removes tile from the system: it will still be present in the + * OSD memory, but marked as loaded=false, and its data will be erased. + * @param {boolean} [erase=false] */ - unload: function() { - //TODO AIOSA remove this.element and move it to a data constructor - if ( this.imgElement && this.imgElement.parentNode ) { - this.imgElement.parentNode.removeChild( this.imgElement ); - } - if ( this.element && this.element.parentNode ) { - this.element.parentNode.removeChild( this.element ); + unload: function(erase = false) { + if (!this.loaded) { + return; } + this.tiledImage._tileCache.unloadTile(this, erase); + }, + + /** + * this method shall be called only by cache system when the tile is already empty of data + * @private + */ + _unload: function () { this.tiledImage = null; this._caches = {}; this._cacheSize = 0; diff --git a/src/tilecache.js b/src/tilecache.js index 3a941170..47d343e3 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -412,16 +412,17 @@ if (data instanceof $.Promise) { this._promise = data.then(d => { this._data = d; + this.loaded = true; return d; }); this._data = null; } else { this._promise = $.Promise.resolve(data); this._data = data; + this.loaded = true; } this._type = type; - this.loaded = true; this._tiles.push(tile); } else if (!this._tiles.includes(tile)) { this._tiles.push(tile); @@ -936,11 +937,11 @@ * was requested restore(). * @param tile * @param originalCache + * @param freeIfUnused if true, zombie is not created */ - restoreTilesThatShareOriginalCache(tile, originalCache) { + restoreTilesThatShareOriginalCache(tile, originalCache, freeIfUnused) { for (let t of originalCache._tiles) { - // todo a bit dirty, touching tile privates - this.unloadCacheForTile(t, t.cacheKey, t.__restoreRequestedFree); + this.unloadCacheForTile(t, t.cacheKey, freeIfUnused); delete t._caches[t.cacheKey]; t.cacheKey = t.originalCacheKey; } @@ -993,7 +994,7 @@ } if ( worstTile && worstTileIndex >= 0 ) { - this.unloadTile(worstTile, true); + this._unloadTile(worstTile, true); insertionIndex = worstTileIndex; } } @@ -1033,7 +1034,7 @@ //iterates from the array end, safe to remove this._tilesLoaded.splice( i, 1 ); } else if ( tile.tiledImage === tiledImage ) { - this.unloadTile(tile, !tiledImage._zombieCache || cacheOverflows, i); + this._unloadTile(tile, !tiledImage._zombieCache || cacheOverflows, i); } } } @@ -1096,14 +1097,28 @@ return false; } + /** + * Unload tile: this will free the tile data and mark the tile as unloaded. + * @param {OpenSeadragon.Tile} tile + * @param {boolean} destroy if set to true, tile data is not preserved as zombies but deleted immediatelly + */ + unloadTile(tile, destroy = false) { + if (!tile.loaded) { + $.console.warn("Attempt to unload already unloaded tile."); + return; + } + const index = this._tilesLoaded.findIndex(x => x === tile); + this._unloadTile(tile, destroy, index); + } + /** * @param tile tile to unload * @param destroy destroy tile cache if the cache tile counts falls to zero - * @param deleteAtIndex index to remove the tile record at, will not remove from _tiledLoaded if not set + * @param deleteAtIndex index to remove the tile record at, will not remove from _tilesLoaded if not set * @private */ - unloadTile(tile, destroy, deleteAtIndex) { - $.console.assert(tile, '[TileCache.unloadTile] tile is required'); + _unloadTile(tile, destroy, deleteAtIndex) { + $.console.assert(tile, '[TileCache._unloadTile] tile is required'); for (let key in tile._caches) { //we are 'ok' to remove tile caches here since we later call destroy on tile, otherwise @@ -1121,7 +1136,7 @@ } const tiledImage = tile.tiledImage; - tile.unload(); + tile._unload(); /** * Triggered when a tile has just been unloaded from memory. diff --git a/src/tiledimage.js b/src/tiledimage.js index 98c976b1..0c989e5d 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1885,8 +1885,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag // if we find existing record, check the original data of existing tile of this record let baseTile = record._tiles[0]; if (!baseTile) { - // we are unable to setup the tile, this might be a bug somewhere else - return false; + // 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 diff --git a/test/demo/basic-html-drawer.html b/test/demo/basic-html-drawer.html new file mode 100644 index 00000000..b5757f8d --- /dev/null +++ b/test/demo/basic-html-drawer.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<head> + <title>OpenSeadragon Basic Demo</title> + <script type="text/javascript" src='../../build/openseadragon/openseadragon.js'></script> + <script type="text/javascript" src='../lib/jquery-1.9.1.min.js'></script> + <style type="text/css"> + + .openseadragon1 { + width: 800px; + height: 600px; + } + + </style> +</head> +<body> + <div> + Simple demo page to show a default OpenSeadragon viewer. + </div> + <div id="contentDiv" class="openseadragon1"></div> + <script type="text/javascript"> + + var viewer = OpenSeadragon({ + // debugMode: true, + id: "contentDiv", + prefixUrl: "../../build/openseadragon/images/", + tileSources: "../data/testpattern.dzi", + drawer: 'html', + showNavigator: true, + }); + </script> +</body> +</html> diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index d12ee130..d6c0a3da 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -218,7 +218,8 @@ const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer); const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer); - //load data + //load data: note that tests SETUP MORE CACHES than they might use: it tests that some other caches / tiles + // are not touched during the manipulation of unrelated caches / tiles const tile00 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0); tile00.addCache(tile00.cacheKey, 0, T_A, false, false); const tile01 = MockSeadragon.getTile('foo2.jpg', fakeTiledImage0); @@ -324,72 +325,192 @@ test.equal(c12.data, 6, "In total 6 conversions on the cache object, above set changes working cache."); test.equal(c12.data, 6, "Changing type of working cache fires no conversion, we overwrite cache state."); + // Get set collide tries to modify the cache: all first request the data, and set the data in random order, + // but writing is done after reading --> we start from TA + collideGetSet(tile12, T_A); // no conversion, already in TA + collideGetSet(tile12, T_B); // conversion to TB + collideGetSet(tile12, T_B); // no conversion, already in TA + collideGetSet(tile12, T_A); // conversion to TB + collideGetSet(tile12, T_B); // conversion to TB + //should finish with next await with 6 steps at this point, add two more and await end + value = await collideGetSet(tile12, T_C); // A -> B -> C (forced await) + test.equal(typeAtoB, 8, "Conversion A->B increased by three + one for the last await."); + test.equal(typeBtoC, 6, "Conversion B->C + one for the last await."); + test.equal(typeCtoA, 5, "Conversion C->A did not happen."); + test.equal(typeDtoA, 0, "Conversion D->A did not happen."); + test.equal(typeCtoE, 0, "Conversion C->E did not happen."); + test.equal(value, 8, "6+2 steps (writes are colliding, just single write will happen)."); + const workingc12 = tile12.getCache(tile12._wcKey); + test.equal(workingc12.type, T_C, "Working cache is really type C."); + + //working cache not shared, even if these two caches share key they have different data now + value = await tile00.getData(T_C); // B -> C + test.equal(typeAtoB, 8, "Conversion A->B nor triggered."); + test.equal(typeBtoC, 7, "Conversion B->C triggered."); + const workingc00 = tile00.getCache(tile00._wcKey); + test.notEqual(workingc00, workingc12, "Underlying working cache is not shared despite tiles share hash key."); + //TODO fix test from here test.ok("TODO: FIX TEST SUITE FOR NEW CACHE SYSTEM"); - // // Get set collide tries to modify the cache - // collideGetSet(tile12, T_A); // B -> C -> A - // collideGetSet(tile12, T_B); // no conversion, all run at the same time - // collideGetSet(tile12, T_B); // no conversion, all run at the same time - // collideGetSet(tile12, T_A); // B -> C -> A - // collideGetSet(tile12, T_B); // no conversion, all run at the same time - // //should finish with next await with 6 steps at this point, add two more and await end - // value = await collideGetSet(tile12, T_A); // B -> C -> A - // test.equal(typeAtoB, 3, "Conversion A->B not increased, not needed as all T_B requests resolve immediatelly."); - // test.equal(typeBtoC, 9, "Conversion B->C happened three times more."); - // test.equal(typeCtoA, 9, "Conversion C->A happened three times more."); - // test.equal(typeDtoA, 0, "Conversion D->A did not happen."); - // test.equal(typeCtoE, 0, "Conversion C->E did not happen."); - // test.equal(value, 13, "11+2 steps (writes are colliding, just single write will happen)."); - // - // //shares cache with tile12 - // value = await tile00.getData(T_A, false); - // test.equal(typeAtoB, 3, "Conversion A->B nor triggered."); - // test.equal(value, 13, "Value did not change."); - // - // //now set value with keeping origin - // await tile00.setData(42, T_D, true); - // test.equal(tile12.originalCacheKey, tile12.cacheKey, "Related tile not affected."); - // test.equal(tile00.originalCacheKey, tile12.originalCacheKey, "Cache data was modified, original kept."); - // test.notEqual(tile00.cacheKey, tile12.cacheKey, "Main cache keys changed."); - // const newCache = tile00.getCache(); - // await newCache.transformTo(T_C); - // test.equal(typeDtoA, 1, "Conversion D->A happens first time."); - // test.equal(c12.data, 13, "Original cache value kept"); - // test.equal(c12.type, T_A, "Original cache type kept"); - // test.equal(c12, c00, "The same cache."); - // - // test.equal(typeAtoB, 4, "Conversion A->B triggered."); - // test.equal(newCache.type, T_C, "Original cache type kept"); - // test.equal(newCache.data, 45, "42+3 steps happened."); - // - // //try again change in set data, now the cache gets overwritten - // await tile00.setData(42, T_B, true); - // test.equal(newCache.type, T_B, "Reset happened in place."); - // test.equal(newCache.data, 42, "Reset happened in place."); - // - // // Overwriting stress test with diff cache (see the same test as above, the same reasoning) - // collideGetSet(tile00, T_A); // B -> C -> A - // collideGetSet(tile00, T_B); // no conversion, all run at the same time - // collideGetSet(tile00, T_B); // no conversion, all run at the same time - // collideGetSet(tile00, T_A); // B -> C -> A - // collideGetSet(tile00, T_B); // no conversion, all run at the same time - // //should finish with next await with 6 steps at this point, add two more and await end - // value = await collideGetSet(tile00, T_A); // B -> C -> A - // test.equal(typeAtoB, 4, "Conversion A->B not increased."); - // test.equal(typeBtoC, 13, "Conversion B->C happened three times more."); - // //we converted D->C before, that's why C->A is one less - // test.equal(typeCtoA, 12, "Conversion C->A happened three times more."); - // test.equal(typeDtoA, 1, "Conversion D->A did not happen."); - // test.equal(typeCtoE, 0, "Conversion C->E did not happen."); - // test.equal(value, 44, "+2 writes value (writes collide, just one finishes last)."); - // - // test.equal(c12.data, 13, "Original cache value kept"); - // test.equal(c12.type, T_A, "Original cache type kept"); - // test.equal(c12, c00, "The same cache."); - // - // //todo test destruction throughout the test above - // //tile00.unload(); + // now set value with keeping origin + await tile00.setData(42, T_D); + const newCache = tile00.getCache(tile00._wcKey); + await newCache.transformTo(T_C); // D -> A -> B -> C + test.equal(typeDtoA, 1, "Conversion D->A happens first time."); + test.equal(tile12.originalCacheKey, tile12.cacheKey, "Related tile not affected."); + test.equal(tile00.originalCacheKey, tile12.originalCacheKey, "Cache data was modified, original kept."); + test.equal(tile00.cacheKey, tile12.cacheKey, "Main cache keys not changed."); + + // tile restore has no effect on the result since tile00 gets overwritten by tile12.updateRenderTarget() + tile00.restore(true); + // tile 12 changes data both tile 00 and tile 12 + tile12.updateRenderTarget(); + let newMainCache12 = tile12.getCache(); + let newMainCache00 = tile00.getCache(); + + test.equal(newMainCache12.data, 8, "Tile 12 main cache value is now 8 as inherited from working cache."); + test.equal(newMainCache00.data, 8, "Tile 00 also shares the same data as tile 12."); + + tile00.updateRenderTarget(); + test.equal(newMainCache12.data, 8, "No effect in update target."); + test.equal(newMainCache00.data, 8, "No effect in update target."); + test.notEqual(tile00.cacheKey, tile00.originalCacheKey, "Tiles have original type."); + + // Overwriting stress test with diff cache (see the same test as above, the same reasoning) + // but now stress test from clean state (WC initialized with first call) + tile00.restore(true); //first we call restore so that set/get reads from original cache + await collideGetSet(tile00, T_C); // tile has no working cache, conversion from original A -> B -> C + test.equal(await tile00.getData(T_C), 8, "Data is now 8 (6 at original + 2 conversion steps)."); + + // initialization of working cache directly as different type + collideGetSet(tile00, T_B); // C -> A -> B + collideGetSet(tile00, T_A); // C -> A + collideGetSet(tile00, T_A); // C -> A + collideGetSet(tile00, T_C); // no change + //should finish with next await with 6 steps at this point, add two more and await end + value = await collideGetSet(tile00, T_B); // C -> A -> B + test.equal(typeAtoB, 12, "Conversion A->B +3"); + test.equal(typeBtoC, 9, "Conversion B->C +2 (one here, one a bit above)"); + test.equal(typeCtoA, 9, "Conversion C->A +4"); + test.equal(typeDtoA, 1, "Conversion D->A did not happen."); + test.equal(typeCtoE, 0, "Conversion C->E did not happen."); + + test.equal(value, 10, "6 original + 4 conversions value (last collide get set taken in action, rest values discarded)."); + + // tile restore has now effect since we swap order of updates + tile00.restore(true); + // tile 12 changes data both tile 00 and tile 12 + tile00.updateRenderTarget(); + newMainCache12 = tile12.getCache(); + newMainCache00 = tile00.getCache(); + + test.equal(newMainCache12.data, 6, "Tile data is now 6 since we restored old data from tile00."); + test.equal(newMainCache12, newMainCache00, "Caches are equal."); + + // we delete tile: original and main cache not freed, working yes + let cacheSize = tileCache.numCachesLoaded(); + tile00.unload(true); + test.equal(tile00.getCacheSize(), 0, "No caches left."); + test.equal(await tile00.getData(T_A), undefined, "No data available."); + test.equal(tile00.loaded, false, "Tile in off state."); + test.equal(tile00.loading, false, "Tile no loading state."); + test.equal(tileCache.numCachesLoaded(), cacheSize, "Tile cache no change since original data shared."); + + // we delete another tile, now original and main caches should be freed too + tile12.unload(true); + test.equal(tile12.getCacheSize(), 0, "No caches left."); + test.equal(await tile12.getData(T_A), undefined, "No data available."); + test.equal(tile12.loaded, false, "Tile in off state."); + test.equal(tile12.loading, false, "Tile no loading state."); + test.equal(tileCache.numCachesLoaded(), cacheSize - 1, "Tile cache shrunken by 1 since tile12 had only original data."); + + done(); + })(); + }); + + QUnit.test('Tile API: basic conversion', function(test) { + const done = test.async(); + const fakeViewer = MockSeadragon.getViewer( + MockSeadragon.getDrawer({ + // tile in safe mode inspects the supported formats upon cache set + getSupportedDataFormats() { + return [T_A, T_B, T_C, T_D, T_E]; + } + }) + ); + const tileCache = fakeViewer.tileCache; + const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer); + const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer); + + //load data: note that tests SETUP MORE CACHES than they might use: it tests that some other caches / tiles + // are not touched during the manipulation of unrelated caches / tiles + const tile00 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0); + tile00.addCache(tile00.cacheKey, 0, T_A, false, false); + const tile01 = MockSeadragon.getTile('foo2.jpg', fakeTiledImage0); + tile01.addCache(tile01.cacheKey, 0, T_B, false, false); + const tile10 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1); + tile10.addCache(tile10.cacheKey, 0, T_C, false, false); + const tile11 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1); + tile11.addCache(tile11.cacheKey, 0, T_C, false, false); + const tile12 = MockSeadragon.getTile('foo.jpg', fakeTiledImage1); + tile12.addCache(tile12.cacheKey, 0, T_A, false, false); + + //test set/get data in async env + (async function() { + + // Tile10 VS Tile11 --> share cache (same key), collision update keeps last call sane + await tile10.setData(3, T_A); + await tile11.setData(5, T_C); + + await tile10.updateRenderTarget(); + + test.equal(tile10.getCache().data, 3, "Tile 10 data used as main."); + test.equal(tile11.getCache().data, 3, "Tile 10 data used as main."); + test.equal(tile11.getCache().type, T_A, "Tile 10 data used as main."); + await tile11.updateRenderTarget(); + + test.equal(tile10.getCache().data, 5, "Tile 11 data used as main."); + test.equal(tile11.getCache().data, 5, "Tile 11 data used as main."); + test.equal(tile11.getCache().type, T_C, "Tile 11 data used as main."); + + // Tile10 updated, reset -> OK + await tile10.setData(42, T_A); + tile10.restore(); + await tile10.updateRenderTarget(); + test.equal(tile10.getCache().data, 0, "Original data used as main: restore was called."); + test.equal(tile11.getCache().data, 0); + test.equal(tile11.getCache().type, T_C); + + + // UpdateTarget called on restore data + await tile11.setData(189, T_D); + await tile10.setData(-87, T_B); + tile10.restore(); + await tile11.updateRenderTarget(); + + test.equal(tile11.getCache().data, 189, "New data reflected."); + test.equal(tile10.getCache().data, 189, "New data reflected also on connected tile."); + await tile10.updateRenderTarget(); + test.equal(tile11.getCache().data, 189, "No effect: restore was called."); + test.equal(tile10.getCache().data, 189, "No effect: restore was called."); + + let cacheSize = tileCache.numCachesLoaded(); + tile11.unload(true); + test.equal(tile11.getCacheSize(), 0, "No caches left in tile11."); + test.equal(tileCache.numCachesLoaded(), cacheSize, "No caches freed: shared with tile12"); + + tile10.unload(true); + test.equal(tile10.getCacheSize(), 0, "No caches left in tile11."); + test.equal(tileCache.numCachesLoaded(), cacheSize - 2, "Two caches freed."); + + tile01.unload(false); + test.equal(tileCache.numCachesLoaded(), cacheSize - 2, "No cache freed: zombie cache left."); + + tile00.unload(true); + test.equal(tileCache.numCachesLoaded(), cacheSize - 2, "No cache freed: shared with tile12."); + tile12.unload(true); + test.equal(tileCache.numCachesLoaded(), 1, "One zombie cache left."); done(); })(); @@ -424,32 +545,36 @@ //test set/get data in async env (async function() { - // TODO FIX + test.equal(tileCache.numTilesLoaded(), 5, "We loaded 5 tiles"); + test.equal(tileCache.numCachesLoaded(), 3, "We loaded 3 cache objects - three different urls"); + + const c00 = tile00.getCache(tile00.cacheKey); + const c12 = tile12.getCache(tile12.cacheKey); + + //now test multi-cache within tile + const theTileKey = tile00.cacheKey; + tile00.setData(42, T_E); + test.equal(tile00.cacheKey, tile00.originalCacheKey, "Original cache still rendered."); + test.equal(theTileKey, tile00.originalCacheKey, "Original cache key preserved."); + + tile00.updateRenderTarget(); + test.notEqual(tile00.cacheKey, tile00.originalCacheKey, "New cache rendered."); + + //now add artifically another record + tile00.addCache("my_custom_cache", 128, T_C); + test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles."); + test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items (two new added already)."); + + test.equal(c00.getTileCount(), 2, "The cache still has only two tiles attached."); + test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects (original data, main cache & custom."); + //related tile not affected + test.notEqual(tile12.cacheKey, tile12.originalCacheKey, "Original cache change reflected on shared caches."); + test.equal(tile12.originalCacheKey, theTileKey, "Original cache key also preserved."); + test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached."); + test.equal(tile12.getCacheSize(), 2, "Related tile cache has also two caches."); + + //TODO fix test from here test.ok("TODO: FIX TEST SUITE FOR NEW CACHE SYSTEM"); - done(); - // test.equal(tileCache.numTilesLoaded(), 5, "We loaded 5 tiles"); - // test.equal(tileCache.numCachesLoaded(), 3, "We loaded 3 cache objects"); - // - // const c00 = tile00.getCache(tile00.cacheKey); - // const c12 = tile12.getCache(tile12.cacheKey); - // - // //now test multi-cache within tile - // const theTileKey = tile00.cacheKey; - // tile00.setData(42, T_E, true); - // test.ok(tile00.cacheKey !== tile00.originalCacheKey, "Original cache key differs."); - // test.equal(theTileKey, tile00.originalCacheKey, "Original cache key preserved."); - // - // //now add artifically another record - // tile00.addCache("my_custom_cache", 128, T_C); - // test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles."); - // test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items."); - // test.equal(c00.getTileCount(), 2, "The cache still has only two tiles attached."); - // test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects."); - // //related tile not really affected - // test.equal(tile12.cacheKey, tile12.originalCacheKey, "Original cache key not affected elsewhere."); - // test.equal(tile12.originalCacheKey, theTileKey, "Original cache key also preserved."); - // test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached."); - // test.equal(tile12.getCacheSize(), 1, "Related tile cache did not increase."); // // //add and delete cache nothing changes // tile00.addCache("my_custom_cache2", 128, T_C); @@ -520,161 +645,155 @@ // //now test tile destruction as zombie // // //now test tile cache sharing - // done(); + done(); })(); }); QUnit.test('Zombie Cache', function(test) { const done = test.async(); - // TODO FIX - test.ok("TODO: FIX TEST SUITE FOR NEW CACHE SYSTEM"); - done(); - // //test jobs by coverage: fail if - // let jobCounter = 0, coverage = undefined; - // OpenSeadragon.ImageLoader.prototype.addJob = function (options) { - // jobCounter++; - // if (coverage) { - // //old coverage of previous tiled image: if loaded, fail --> should be in cache - // const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y]; - // test.ok(!coverageItem, "Attempt to add job for tile that is not in cache OK if previously not loaded."); - // } - // return originalJob.call(this, options); - // }; - // - // let tilesFinished = 0; - // const tileCounter = function (event) {tilesFinished++;} - // - // const openHandler = function(event) { - // event.item.allowZombieCache(true); - // - // viewer.world.removeHandler('add-item', openHandler); - // test.ok(jobCounter === 0, 'Initial state, no images loaded'); - // - // waitFor(() => { - // if (tilesFinished === jobCounter && event.item._fullyLoaded) { - // coverage = $.extend(true, {}, event.item.coverage); - // viewer.world.removeAll(); - // return true; - // } - // return false; - // }); - // }; - // - // let jobsAfterRemoval = 0; - // const removalHandler = function (event) { - // viewer.world.removeHandler('remove-item', removalHandler); - // test.ok(jobCounter > 0, 'Tiled image removed after 100 ms, should load some images.'); - // jobsAfterRemoval = jobCounter; - // - // viewer.world.addHandler('add-item', reopenHandler); - // viewer.addTiledImage({ - // tileSource: '/test/data/testpattern.dzi' - // }); - // } - // - // const reopenHandler = function (event) { - // event.item.allowZombieCache(true); - // - // viewer.removeHandler('add-item', reopenHandler); - // test.equal(jobCounter, jobsAfterRemoval, 'Reopening image does not fetch any tiles imemdiatelly.'); - // - // waitFor(() => { - // if (event.item._fullyLoaded) { - // viewer.removeHandler('tile-unloaded', unloadTileHandler); - // viewer.removeHandler('tile-loaded', tileCounter); - // - // //console test needs here explicit removal to finish correctly - // OpenSeadragon.ImageLoader.prototype.addJob = originalJob; - // done(); - // return true; - // } - // return false; - // }); - // }; - // - // const unloadTileHandler = function (event) { - // test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!"); - // } - // - // viewer.world.addHandler('add-item', openHandler); - // viewer.world.addHandler('remove-item', removalHandler); - // viewer.addHandler('tile-unloaded', unloadTileHandler); - // viewer.addHandler('tile-loaded', tileCounter); - // - // viewer.open('/test/data/testpattern.dzi'); + //test jobs by coverage: fail if cached coverage not fully re-stored without jobs + let jobCounter = 0, coverage = undefined; + OpenSeadragon.ImageLoader.prototype.addJob = function (options) { + jobCounter++; + if (coverage) { + //old coverage of previous tiled image: if loaded, fail --> should be in cache + const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y]; + test.ok(!coverageItem, "Attempt to add job for tile that should be already in memory."); + } + return originalJob.call(this, options); + }; + + let tilesFinished = 0; + const tileCounter = function (event) {tilesFinished++;} + + const openHandler = function(event) { + event.item.allowZombieCache(true); + + viewer.world.removeHandler('add-item', openHandler); + test.ok(jobCounter === 0, 'Initial state, no images loaded'); + + waitFor(() => { + if (tilesFinished === jobCounter && event.item._fullyLoaded) { + coverage = $.extend(true, {}, event.item.coverage); + viewer.world.removeAll(); + return true; + } + return false; + }); + }; + + let jobsAfterRemoval = 0; + const removalHandler = function (event) { + viewer.world.removeHandler('remove-item', removalHandler); + test.ok(jobCounter > 0, 'Tiled image removed after 100 ms, should load some images.'); + jobsAfterRemoval = jobCounter; + + viewer.world.addHandler('add-item', reopenHandler); + viewer.addTiledImage({ + tileSource: '/test/data/testpattern.dzi' + }); + } + + const reopenHandler = function (event) { + event.item.allowZombieCache(true); + + viewer.removeHandler('add-item', reopenHandler); + test.equal(jobCounter, jobsAfterRemoval, 'Reopening image does not fetch any tiles imemdiatelly.'); + + waitFor(() => { + if (event.item._fullyLoaded) { + viewer.removeHandler('tile-unloaded', unloadTileHandler); + viewer.removeHandler('tile-loaded', tileCounter); + coverage = undefined; + + //console test needs here explicit removal to finish correctly + OpenSeadragon.ImageLoader.prototype.addJob = originalJob; + done(); + return true; + } + return false; + }); + }; + + const unloadTileHandler = function (event) { + test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!"); + } + + viewer.world.addHandler('add-item', openHandler); + viewer.world.addHandler('remove-item', removalHandler); + viewer.addHandler('tile-unloaded', unloadTileHandler); + viewer.addHandler('tile-loaded', tileCounter); + + viewer.open('/test/data/testpattern.dzi'); }); QUnit.test('Zombie Cache Replace Item', function(test) { const done = test.async(); - //TODO FIX - test.ok("TODO: FIX TEST SUITE FOR NEW CACHE SYSTEM"); - done(); - // //test jobs by coverage: fail if - // let jobCounter = 0, coverage = undefined; - // OpenSeadragon.ImageLoader.prototype.addJob = function (options) { - // jobCounter++; - // if (coverage) { - // //old coverage of previous tiled image: if loaded, fail --> should be in cache - // const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y]; - // if (!coverageItem) { - // console.warn(coverage, coverage[options.tile.level][options.tile.x], options.tile); - // } - // test.ok(!coverageItem, "Attempt to add job for tile data that was previously loaded."); - // } - // return originalJob.call(this, options); - // }; - // - // let tilesFinished = 0; - // const tileCounter = function (event) {tilesFinished++;} - // - // const openHandler = function(event) { - // event.item.allowZombieCache(true); - // viewer.world.removeHandler('add-item', openHandler); - // viewer.world.addHandler('add-item', reopenHandler); - // - // waitFor(() => { - // if (tilesFinished === jobCounter && event.item._fullyLoaded) { - // coverage = $.extend(true, {}, event.item.coverage); - // viewer.addTiledImage({ - // tileSource: '/test/data/testpattern.dzi', - // index: 0, - // replace: true - // }); - // return true; - // } - // return false; - // }); - // }; - // - // const reopenHandler = function (event) { - // event.item.allowZombieCache(true); - // - // viewer.removeHandler('add-item', reopenHandler); - // waitFor(() => { - // if (event.item._fullyLoaded) { - // viewer.removeHandler('tile-unloaded', unloadTileHandler); - // viewer.removeHandler('tile-loaded', tileCounter); - // - // //console test needs here explicit removal to finish correctly - // OpenSeadragon.ImageLoader.prototype.addJob = originalJob; - // done(); - // return true; - // } - // return false; - // }); - // }; - // - // const unloadTileHandler = function (event) { - // test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!"); - // } - // - // viewer.world.addHandler('add-item', openHandler); - // viewer.addHandler('tile-unloaded', unloadTileHandler); - // viewer.addHandler('tile-loaded', tileCounter); - // - // viewer.open('/test/data/testpattern.dzi'); + let jobCounter = 0, coverage = undefined; + OpenSeadragon.ImageLoader.prototype.addJob = function (options) { + jobCounter++; + if (coverage) { + //old coverage of previous tiled image: if loaded, fail --> should be in cache + const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y]; + if (!coverageItem) { + console.warn(coverage, coverage[options.tile.level][options.tile.x], options.tile); + } + test.ok(!coverageItem, "Attempt to add job for tile data that was previously loaded."); + } + return originalJob.call(this, options); + }; + + let tilesFinished = 0; + const tileCounter = function (event) {tilesFinished++;} + + const openHandler = function(event) { + event.item.allowZombieCache(true); + viewer.world.removeHandler('add-item', openHandler); + viewer.world.addHandler('add-item', reopenHandler); + + waitFor(() => { + if (tilesFinished === jobCounter && event.item._fullyLoaded) { + coverage = $.extend(true, {}, event.item.coverage); + viewer.addTiledImage({ + tileSource: '/test/data/testpattern.dzi', + index: 0, + replace: true + }); + return true; + } + return false; + }); + }; + + const reopenHandler = function (event) { + event.item.allowZombieCache(true); + + viewer.removeHandler('add-item', reopenHandler); + waitFor(() => { + if (event.item._fullyLoaded) { + viewer.removeHandler('tile-unloaded', unloadTileHandler); + viewer.removeHandler('tile-loaded', tileCounter); + + //console test needs here explicit removal to finish correctly + OpenSeadragon.ImageLoader.prototype.addJob = originalJob; + done(); + return true; + } + return false; + }); + }; + + const unloadTileHandler = function (event) { + test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!"); + } + + viewer.world.addHandler('add-item', openHandler); + viewer.addHandler('tile-unloaded', unloadTileHandler); + viewer.addHandler('tile-loaded', tileCounter); + + viewer.open('/test/data/testpattern.dzi'); }); })(); diff --git a/test/test.html b/test/test.html index 5da55417..d7f19389 100644 --- a/test/test.html +++ b/test/test.html @@ -7,7 +7,7 @@ window.QUnit = { config: { //one minute per test timeout - testTimeout: 60000 + testTimeout: 5000 } }; </script>