Tests & Bugfixes: new cache tests, working cache preemptively deleted when restore() called, zombie cache had bug (restored cache had no attached tile reference and restoration failed since we relied on any existing tile on the cache to inherit state), deprecated old HTMLDrawer props on tile, rewritten HTMLDrawer to work also with cache API.

This commit is contained in:
Aiosa 2024-10-17 12:10:04 +02:00
parent f8e5cff117
commit 0b63a943b6
8 changed files with 557 additions and 310 deletions

View File

@ -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;

View File

@ -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 );

View File

@ -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;

View File

@ -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.

View File

@ -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

View File

@ -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>

View File

@ -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');
});
})();

View File

@ -7,7 +7,7 @@
window.QUnit = {
config: {
//one minute per test timeout
testTimeout: 60000
testTimeout: 5000
}
};
</script>