mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-30 00:56:08 +03:00
Add tests for zombie and data type conversion, ensure destructors are called.
Fix bugs (zombie was disabled on item replace, fix zombie cache system by separating to its own cache array). Fix CacheRecord destructor & dijkstra. Deduce cache only from originalCacheKey. Force explicit type declaration with types on users.
This commit is contained in:
parent
c3ab9a08e7
commit
219049976c
@ -43,6 +43,12 @@ class WeightedGraph {
|
||||
this.adjacencyList = {};
|
||||
this.vertices = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add vertex to graph
|
||||
* @param vertex unique vertex ID
|
||||
* @return {boolean} true if inserted, false if exists (no-op)
|
||||
*/
|
||||
addVertex(vertex) {
|
||||
if (!this.vertices[vertex]) {
|
||||
this.vertices[vertex] = new $.PriorityQueue.Node(0, vertex);
|
||||
@ -51,8 +57,28 @@ class WeightedGraph {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add edge to graph
|
||||
* @param vertex1 id, must exist by calling addVertex()
|
||||
* @param vertex2 id, must exist by calling addVertex()
|
||||
* @param weight
|
||||
* @param transform function that transforms on path vertex1 -> vertex2
|
||||
* @return {boolean} true if new edge, false if replaced existing
|
||||
*/
|
||||
addEdge(vertex1, vertex2, weight, transform) {
|
||||
this.adjacencyList[vertex1].push({ target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform });
|
||||
if (weight < 0) {
|
||||
$.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");
|
||||
}
|
||||
const outgoingPaths = this.adjacencyList[vertex1],
|
||||
replacedEdgeIndex = outgoingPaths.findIndex(edge => edge.target === this.vertices[vertex2]),
|
||||
newEdge = { target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform };
|
||||
if (replacedEdgeIndex < 0) {
|
||||
this.adjacencyList[vertex1].push(newEdge);
|
||||
return true;
|
||||
}
|
||||
this.adjacencyList[vertex1][replacedEdgeIndex] = newEdge;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +123,7 @@ class WeightedGraph {
|
||||
}
|
||||
}
|
||||
|
||||
if (!smallestNode || !smallestNode._previous) {
|
||||
if (!smallestNode || !smallestNode._previous || smallestNode.value !== finish) {
|
||||
return undefined; //no path
|
||||
}
|
||||
|
||||
@ -158,11 +184,11 @@ $.DataTypeConvertor = class {
|
||||
|
||||
// Teaching OpenSeadragon built-in conversions:
|
||||
|
||||
this.learn("canvas", "rasterUrl", (canvas) => canvas.toDataURL(), 1, 1);
|
||||
this.learn("image", "rasterUrl", (image) => image.url);
|
||||
this.learn("canvas", "context2d", (canvas) => canvas.getContext("2d"));
|
||||
this.learn("context2d", "canvas", (context2D) => context2D.canvas);
|
||||
this.learn("image", "canvas", (image) => {
|
||||
this.learn("canvas", "url", canvas => canvas.toDataURL(), 1, 1);
|
||||
this.learn("image", "url", image => image.url);
|
||||
this.learn("canvas", "context2d", canvas => canvas.getContext("2d"));
|
||||
this.learn("context2d", "canvas", context2D => context2D.canvas);
|
||||
this.learn("image", "canvas", image => {
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
@ -170,7 +196,7 @@ $.DataTypeConvertor = class {
|
||||
context.drawImage( image, 0, 0 );
|
||||
return canvas;
|
||||
}, 1, 1);
|
||||
this.learn("rasterUrl", "image", (url) => {
|
||||
this.learn("url", "image", url => {
|
||||
return new $.Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onerror = img.onabort = reject;
|
||||
@ -181,17 +207,15 @@ $.DataTypeConvertor = class {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: types are sensitive thing. Same data type might have different data semantics.
|
||||
* - 'string' can be anything, for images, dataUrl or some URI, or incompatible stuff: vector data (JSON)
|
||||
* - using $.type makes explicit requirements on its extensibility, and makes mess in naming
|
||||
* - most types are [object X]
|
||||
* - selected types are 'nice' -> string, canvas...
|
||||
* - hard to debug
|
||||
*
|
||||
* Unique identifier (unlike toString.call(x)) to be guessed
|
||||
* from the data value
|
||||
* from the data value. This type guess is more strict than
|
||||
* OpenSeadragon.type() implementation, but for most type recognition
|
||||
* this test relies on the output of OpenSeadragon.type().
|
||||
*
|
||||
* @function uniqueType
|
||||
* Note: although we try to implement the type guessing, do
|
||||
* not rely on this functionality! Prefer explicit type declaration.
|
||||
*
|
||||
* @function guessType
|
||||
* @param x object to get unique identifier for
|
||||
* - can be array, in that case, alphabetically-ordered list of inner unique types
|
||||
* is returned (null, undefined are ignored)
|
||||
@ -368,6 +392,23 @@ $.DataTypeConvertor = class {
|
||||
}
|
||||
return bestConvertorPath ? bestConvertorPath.path : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of known conversion types
|
||||
* @return {string[]}
|
||||
*/
|
||||
getKnownTypes() {
|
||||
return Object.keys(this.graph.vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given type is known to the convertor
|
||||
* @param {string} type type to test
|
||||
* @return {boolean}
|
||||
*/
|
||||
existsType(type) {
|
||||
return !!this.graph.vertices[type];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -376,7 +417,7 @@ $.DataTypeConvertor = class {
|
||||
* Built-in conversions include types:
|
||||
* - context2d canvas 2d context
|
||||
* - image HTMLImage element
|
||||
* - rasterUrl url string carrying or pointing to 2D raster data
|
||||
* - url url string carrying or pointing to 2D raster data
|
||||
* - canvas HTMLCanvas element
|
||||
*
|
||||
* @type OpenSeadragon.DataTypeConvertor
|
||||
|
@ -95,7 +95,7 @@ $.ImageJob.prototype = {
|
||||
var selfAbort = this.abort;
|
||||
|
||||
this.jobId = window.setTimeout(function () {
|
||||
self.finish(null, null, "Image load exceeded timeout (" + self.timeout + " ms)");
|
||||
self.fail("Image load exceeded timeout (" + self.timeout + " ms)", null);
|
||||
}, this.timeout);
|
||||
|
||||
this.abort = function() {
|
||||
|
10
src/tile.js
10
src/tile.js
@ -149,6 +149,8 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
||||
* that holds the cache original data (it was loaded with). In case you
|
||||
* change the tile data, the tile original data should be left with the cache
|
||||
* 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'.
|
||||
* This key is used in cache resolution: in case new tile data is requested, if
|
||||
* this cache key exists in the cache it is loaded.
|
||||
* @member {String} originalCacheKey
|
||||
* @memberof OpenSeadragon.Tile#
|
||||
*/
|
||||
@ -444,6 +446,7 @@ $.Tile.prototype = {
|
||||
/**
|
||||
* Get the default data for this tile
|
||||
* @param {?string} [type=undefined] data type to require
|
||||
* @return {*|undefined} data in the desired type, or undefined if a conversion is ongoing
|
||||
*/
|
||||
getData(type = undefined) {
|
||||
const cache = this.getCache(this.cacheKey);
|
||||
@ -494,7 +497,12 @@ $.Tile.prototype = {
|
||||
* @param [_cutoff=0] private
|
||||
*/
|
||||
setCache: function(key, data, type = undefined, _safely = true, _cutoff = 0) {
|
||||
type = type || $.convertor.guessType(data);
|
||||
if (!type && this.tiledImage && !this.tiledImage.__typeWarningReported) {
|
||||
$.console.warn(this, "[Tile.setCache] called without type specification. " +
|
||||
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
|
||||
this.tiledImage.__typeWarningReported = true;
|
||||
type = $.convertor.guessType(data);
|
||||
}
|
||||
|
||||
if (_safely && key === this.cacheKey) {
|
||||
//todo later, we could have drawers register their supported rendering type
|
||||
|
220
src/tilecache.js
220
src/tilecache.js
@ -62,17 +62,23 @@ $.CacheRecord = class {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._tiles = null;
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this.loaded = false;
|
||||
//make sure this gets destroyed even if loaded=false
|
||||
if (this.loaded) {
|
||||
$.convertor.destroy(this._type, this._data);
|
||||
} else {
|
||||
this._promise.then(x => $.convertor.destroy(this._type, x));
|
||||
}
|
||||
this._tiles = null;
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this._promise = $.Promise.resolve();
|
||||
} else {
|
||||
this._promise.then(x => {
|
||||
$.convertor.destroy(this._type, x);
|
||||
this._tiles = null;
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this._promise = $.Promise.resolve();
|
||||
});
|
||||
}
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
get data() {
|
||||
@ -119,7 +125,9 @@ $.CacheRecord = class {
|
||||
this._data = data;
|
||||
this.loaded = true;
|
||||
} else if (this._type !== type) {
|
||||
$.console.warn("[CacheRecord.addTile] Tile %s was added to an existing cache, but the tile is supposed to carry incompatible data type %s!", tile, type);
|
||||
//pass: the tile data type will silently change
|
||||
// as it inherits this cache
|
||||
// todo do not call events?
|
||||
}
|
||||
|
||||
this._tiles.push(tile);
|
||||
@ -185,7 +193,6 @@ $.CacheRecord = class {
|
||||
return convert(y, i + 1);
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
this.loaded = false;
|
||||
@ -195,124 +202,6 @@ $.CacheRecord = class {
|
||||
}
|
||||
};
|
||||
|
||||
//FIXME: really implement or throw away? new parameter would allow users to
|
||||
// use this implementation instead of the above to allow caching for old data
|
||||
// (for example in the default use, the data is downloaded as an image, and
|
||||
// converted to a canvas -> the image record gets thrown away)
|
||||
//
|
||||
//FIXME: Note that this can be also achieved somewhat by caching the midresults
|
||||
// as a single cache object instead. Also, there is the problem of lifecycle-oriented
|
||||
// data types such as WebGL textures we want to unload manually: this looks like
|
||||
// we really want to cache midresuls and have their custom destructors
|
||||
// $.MemoryCacheRecord = class extends $.CacheRecord {
|
||||
// constructor(memorySize) {
|
||||
// super();
|
||||
// this.length = memorySize;
|
||||
// this.index = 0;
|
||||
// this.content = [];
|
||||
// this.types = [];
|
||||
// this.defaultType = "image";
|
||||
// }
|
||||
//
|
||||
// // overrides:
|
||||
//
|
||||
// destroy() {
|
||||
// super.destroy();
|
||||
// this.types = null;
|
||||
// this.content = null;
|
||||
// this.types = null;
|
||||
// this.defaultType = null;
|
||||
// }
|
||||
//
|
||||
// getData(type = this.defaultType) {
|
||||
// let item = this.add(type, undefined);
|
||||
// if (item === undefined) {
|
||||
// //no such type available, get if possible
|
||||
// //todo: possible unomptimal use, we could cache costs and re-use known paths, though it adds overhead...
|
||||
// item = $.convertor.convert(this.current(), this.currentType(), type);
|
||||
// this.add(type, item);
|
||||
// }
|
||||
// return item;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @deprecated
|
||||
// */
|
||||
// get data() {
|
||||
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use getData(...) instead!");
|
||||
// return this.current();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @deprecated
|
||||
// * @param value
|
||||
// */
|
||||
// set data(value) {
|
||||
// //FIXME: addTile bit bad name, related to the issue mentioned elsewhere
|
||||
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use addTile(...) instead!");
|
||||
// this.defaultType = $.convertor.guessType(value);
|
||||
// this.add(this.defaultType, value);
|
||||
// }
|
||||
//
|
||||
// addTile(tile, data, type) {
|
||||
// $.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||
//
|
||||
// //allow overriding the cache - existing tile or different type
|
||||
// if (this._tiles.includes(tile)) {
|
||||
// this.removeTile(tile);
|
||||
// } else if (!this.defaultType !== type) {
|
||||
// this.defaultType = type;
|
||||
// this.add(type, data);
|
||||
// }
|
||||
//
|
||||
// this._tiles.push(tile);
|
||||
// }
|
||||
//
|
||||
// // extends:
|
||||
//
|
||||
// add(type, item) {
|
||||
// const index = this.hasIndex(type);
|
||||
// if (index > -1) {
|
||||
// //no index change, swap (optimally, move all by one - too expensive...)
|
||||
// item = this.content[index];
|
||||
// this.content[index] = this.content[this.index];
|
||||
// } else {
|
||||
// this.index = (this.index + 1) % this.length;
|
||||
// }
|
||||
// this.content[this.index] = item;
|
||||
// this.types[this.index] = type;
|
||||
// return item;
|
||||
// }
|
||||
//
|
||||
// has(type) {
|
||||
// for (let i = 0; i < this.types.length; i++) {
|
||||
// const t = this.types[i];
|
||||
// if (t === type) {
|
||||
// return this.content[i];
|
||||
// }
|
||||
// }
|
||||
// return undefined;
|
||||
// }
|
||||
//
|
||||
// hasIndex(type) {
|
||||
// for (let i = 0; i < this.types.length; i++) {
|
||||
// const t = this.types[i];
|
||||
// if (t === type) {
|
||||
// return i;
|
||||
// }
|
||||
// }
|
||||
// return -1;
|
||||
// }
|
||||
//
|
||||
// current() {
|
||||
// return this.content[this.index];
|
||||
// }
|
||||
//
|
||||
// currentType() {
|
||||
// return this.types[this.index];
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* @class TileCache
|
||||
* @memberof OpenSeadragon
|
||||
@ -328,6 +217,8 @@ $.TileCache = class {
|
||||
|
||||
this._maxCacheItemCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount;
|
||||
this._tilesLoaded = [];
|
||||
this._zombiesLoaded = [];
|
||||
this._zombiesLoadedCount = 0;
|
||||
this._cachesLoaded = [];
|
||||
this._cachesLoadedCount = 0;
|
||||
}
|
||||
@ -368,7 +259,7 @@ $.TileCache = class {
|
||||
insertionIndex = this._tilesLoaded.length,
|
||||
cacheKey = options.cacheKey || options.tile.cacheKey;
|
||||
|
||||
let cacheRecord = this._cachesLoaded[options.tile.cacheKey];
|
||||
let cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
|
||||
if (!cacheRecord) {
|
||||
|
||||
if (!options.data) {
|
||||
@ -378,14 +269,12 @@ $.TileCache = class {
|
||||
}
|
||||
|
||||
$.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an CacheRecord" );
|
||||
cacheRecord = this._cachesLoaded[options.tile.cacheKey] = new $.CacheRecord();
|
||||
cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord();
|
||||
this._cachesLoadedCount++;
|
||||
} else if (cacheRecord.__zombie__) {
|
||||
delete cacheRecord.__zombie__;
|
||||
//revive cache, replace from _tilesLoaded so it won't get unloaded
|
||||
this._tilesLoaded.splice( cacheRecord.__index__, 1 );
|
||||
delete cacheRecord.__index__;
|
||||
insertionIndex--;
|
||||
} else if (!cacheRecord.getTileCount()) {
|
||||
//revive zombie
|
||||
delete this._zombiesLoaded[cacheKey];
|
||||
this._zombiesLoadedCount--;
|
||||
}
|
||||
|
||||
if (!options.dataType) {
|
||||
@ -398,7 +287,16 @@ $.TileCache = class {
|
||||
|
||||
// Note that just because we're unloading a tile doesn't necessarily mean
|
||||
// we're unloading its cache records. With repeated calls it should sort itself out, though.
|
||||
if ( this._cachesLoadedCount > this._maxCacheItemCount ) {
|
||||
if ( this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount ) {
|
||||
//prefer zombie deletion, faster, better
|
||||
if (this._zombiesLoadedCount > 0) {
|
||||
for (let zombie in this._zombiesLoaded) {
|
||||
this._zombiesLoaded[zombie].destroy();
|
||||
delete this._zombiesLoaded[zombie];
|
||||
this._zombiesLoadedCount--;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
let worstTile = null;
|
||||
let worstTileIndex = -1;
|
||||
let prevTile, worstTime, worstLevel, prevTime, prevLevel;
|
||||
@ -406,20 +304,6 @@ $.TileCache = class {
|
||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||
prevTile = this._tilesLoaded[ i ];
|
||||
|
||||
//todo try different approach? the only ugly part, keep tilesLoaded array empty of unloaded tiles
|
||||
if (!prevTile.loaded) {
|
||||
//iterates from the array end, safe to remove
|
||||
this._tilesLoaded.splice( i, 1 );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( prevTile.__zombie__ !== undefined ) {
|
||||
//remove without hesitation, CacheRecord instance
|
||||
worstTile = prevTile.__zombie__;
|
||||
worstTileIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( prevTile.level <= cutoff || prevTile.beingDrawn ) {
|
||||
continue;
|
||||
} else if ( !worstTile ) {
|
||||
@ -445,6 +329,7 @@ $.TileCache = class {
|
||||
insertionIndex = worstTileIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._tilesLoaded[ insertionIndex ] = options.tile;
|
||||
}
|
||||
@ -456,17 +341,28 @@ $.TileCache = class {
|
||||
clearTilesFor( tiledImage ) {
|
||||
$.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required');
|
||||
let tile;
|
||||
|
||||
let cacheOverflows = this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount;
|
||||
if (tiledImage._zombieCache && cacheOverflows && this._zombiesLoadedCount > 0) {
|
||||
//prefer newer zombies
|
||||
for (let zombie in this._zombiesLoaded) {
|
||||
this._zombiesLoaded[zombie].destroy();
|
||||
delete this._zombiesLoaded[zombie];
|
||||
}
|
||||
this._zombiesLoadedCount = 0;
|
||||
cacheOverflows = this._cachesLoadedCount > this._maxCacheItemCount;
|
||||
}
|
||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||
tile = this._tilesLoaded[ i ];
|
||||
|
||||
//todo try different approach? the only ugly part, keep tilesLoaded array empty of unloaded tiles
|
||||
//todo might be errorprone: tile.loading true--> problem! maybe set some other flag by
|
||||
if (!tile.loaded) {
|
||||
//iterates from the array end, safe to remove
|
||||
this._tilesLoaded.splice( i, 1 );
|
||||
i--;
|
||||
} else if ( tile.tiledImage === tiledImage ) {
|
||||
this._unloadTile(tile, !tiledImage._zombieCache ||
|
||||
this._cachesLoadedCount > this._maxCacheItemCount, i);
|
||||
//todo tile loading, if abort... we cloud notify the cache, maybe it works (cache destroy will wait for conversion...)
|
||||
this._unloadTile(tile, !tiledImage._zombieCache || cacheOverflows, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,19 +392,15 @@ $.TileCache = class {
|
||||
cacheRecord.destroy();
|
||||
delete this._cachesLoaded[tile.cacheKey];
|
||||
this._cachesLoadedCount--;
|
||||
|
||||
} else if (deleteAtIndex !== undefined) {
|
||||
// #2 Tile is a zombie. Do not delete record, reuse.
|
||||
this._zombiesLoaded[ tile.cacheKey ] = cacheRecord;
|
||||
this._zombiesLoadedCount++;
|
||||
}
|
||||
//delete also the tile record
|
||||
if (deleteAtIndex !== undefined) {
|
||||
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||
}
|
||||
} else if (deleteAtIndex !== undefined) {
|
||||
// #2 Tile is a zombie. Do not delete record, reuse.
|
||||
// a bit dirty but performant... -> we can remove later, or revive
|
||||
// we can do this, in array the tile is once for each its cache object
|
||||
this._tilesLoaded[ deleteAtIndex ] = cacheRecord;
|
||||
cacheRecord.__zombie__ = tile;
|
||||
cacheRecord.__index__ = deleteAtIndex;
|
||||
}
|
||||
} else if (deleteAtIndex !== undefined) {
|
||||
// #3 Cache stays. Tile record needs to be removed anyway, since the tile is removed.
|
||||
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||
@ -528,10 +420,12 @@ $.TileCache = class {
|
||||
* @type {object}
|
||||
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the unloaded tile.
|
||||
* @property {OpenSeadragon.Tile} tile - The tile which has been unloaded.
|
||||
* @property {boolean} destroyed - False if the tile data was kept in the system.
|
||||
*/
|
||||
tiledImage.viewer.raiseEvent("tile-unloaded", {
|
||||
tile: tile,
|
||||
tiledImage: tiledImage
|
||||
tiledImage: tiledImage,
|
||||
destroyed: destroy
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1536,12 +1536,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
tile.cacheKey = "";
|
||||
tile.originalCacheKey = "";
|
||||
}
|
||||
const similarCacheRecord =
|
||||
this._tileCache.getCacheRecord(tile.originalCacheKey) ||
|
||||
this._tileCache.getCacheRecord(tile.cacheKey);
|
||||
|
||||
//do not use tile.cacheKey: that cache might be different from what we really want
|
||||
// since this request could come from different tiled image and might not want
|
||||
// to use the modified data
|
||||
const similarCacheRecord = this._tileCache.getCacheRecord(tile.originalCacheKey);
|
||||
|
||||
if (similarCacheRecord) {
|
||||
const cutoff = this.source.getClosestLevel();
|
||||
tile.loading = true;
|
||||
tile.loaded = false;
|
||||
if (similarCacheRecord.loaded) {
|
||||
this._setTileLoaded(tile, similarCacheRecord.data, cutoff, null, similarCacheRecord.type);
|
||||
} else {
|
||||
|
@ -786,7 +786,7 @@ $.TileSource.prototype = {
|
||||
return;
|
||||
}
|
||||
image.onload = image.onerror = image.onabort = null;
|
||||
context.finish(image, dataStore.request); //dataType="image" recognized automatically
|
||||
context.finish(image, dataStore.request, "image");
|
||||
};
|
||||
image.onload = function () {
|
||||
finalize();
|
||||
@ -900,7 +900,7 @@ $.TileSource.prototype = {
|
||||
* Raw data getter, should return anything that is compatible with the system, or undefined
|
||||
* if the system can handle it.
|
||||
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||
* @returns {Promise<?>} cache data
|
||||
* @returns {OpenSeadragon.Promise<?>} cache data
|
||||
* @deprecated
|
||||
*/
|
||||
getTileCacheData: function(cacheObject) {
|
||||
|
@ -1579,7 +1579,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
||||
if (newIndex !== -1) {
|
||||
queueItem.options.index = newIndex;
|
||||
}
|
||||
queueItem.options.replaceItem.allowZombieCache(queueItem.options.zombieCache || false);
|
||||
_this.world.removeItem(queueItem.options.replaceItem);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,41 @@
|
||||
/* global QUnit, testLog */
|
||||
|
||||
(function() {
|
||||
let viewer;
|
||||
|
||||
//we override jobs: remember original function
|
||||
const originalJob = OpenSeadragon.ImageLoader.prototype.addJob;
|
||||
|
||||
//event awaiting
|
||||
function waitFor(predicate) {
|
||||
const time = setInterval(() => {
|
||||
if (predicate()) {
|
||||
clearInterval(time);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
// ----------
|
||||
QUnit.module('TileCache', {
|
||||
beforeEach: function () {
|
||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||
|
||||
testLog.reset();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100 // Faster animation = faster tests
|
||||
});
|
||||
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
||||
},
|
||||
afterEach: function () {
|
||||
if (viewer && viewer.close) {
|
||||
viewer.close();
|
||||
}
|
||||
|
||||
viewer = null;
|
||||
}
|
||||
});
|
||||
|
||||
@ -146,4 +174,159 @@
|
||||
done();
|
||||
});
|
||||
|
||||
QUnit.test('Zombie Cache', function(test) {
|
||||
const done = test.async();
|
||||
|
||||
//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');
|
||||
});
|
||||
|
||||
QUnit.test('Zombie Cache Replace Item', function(test) {
|
||||
const done = test.async();
|
||||
|
||||
//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);
|
||||
|
||||
const oldCacheSize = event.item._tileCache._cachesLoadedCount +
|
||||
event.item._tileCache._zombiesLoadedCount;
|
||||
|
||||
waitFor(() => {
|
||||
if (tilesFinished === jobCounter && event.item._fullyLoaded) {
|
||||
coverage = $.extend(true, {}, event.item.coverage);
|
||||
viewer.addTiledImage({
|
||||
tileSource: '/test/data/testpattern.dzi',
|
||||
index: 0,
|
||||
replace: true,
|
||||
success: e => {
|
||||
test.equal(oldCacheSize, e.item._tileCache._cachesLoadedCount +
|
||||
e.item._tileCache._zombiesLoadedCount,
|
||||
"Image replace should erase no cache with zombies.");
|
||||
}
|
||||
});
|
||||
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');
|
||||
});
|
||||
|
||||
})();
|
||||
|
277
test/modules/typeConversion.js
Normal file
277
test/modules/typeConversion.js
Normal file
@ -0,0 +1,277 @@
|
||||
/* global QUnit, $, Util, testLog */
|
||||
|
||||
(function() {
|
||||
const Convertor = OpenSeadragon.convertor;
|
||||
|
||||
let viewer;
|
||||
|
||||
//we override jobs: remember original function
|
||||
const originalJob = OpenSeadragon.ImageLoader.prototype.addJob;
|
||||
|
||||
//event awaiting
|
||||
function waitFor(predicate) {
|
||||
const time = setInterval(() => {
|
||||
if (predicate()) {
|
||||
clearInterval(time);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
//hijack conversion paths
|
||||
//count jobs: how many items we process?
|
||||
let jobCounter = 0;
|
||||
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
||||
jobCounter++;
|
||||
return originalJob.call(this, options);
|
||||
};
|
||||
|
||||
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
||||
// other tests will interfere
|
||||
let imageToCanvas = 0, srcToImage = 0, context2DtoImage = 0, canvasToContext2D = 0, imageToUrl = 0,
|
||||
canvasToUrl = 0;
|
||||
//set all same costs to get easy testing, know which path will be taken
|
||||
Convertor.learn("__TEST__canvas", "__TEST__url", canvas => {
|
||||
canvasToUrl++;
|
||||
return canvas.toDataURL();
|
||||
}, 1, 1);
|
||||
Convertor.learn("__TEST__image", "__TEST__url", image => {
|
||||
imageToUrl++;
|
||||
return image.url;
|
||||
}, 1, 1);
|
||||
Convertor.learn("__TEST__canvas", "__TEST__context2d", canvas => {
|
||||
canvasToContext2D++;
|
||||
return canvas.getContext("2d");
|
||||
}, 1, 1);
|
||||
Convertor.learn("__TEST__context2d", "__TEST__canvas", context2D => {
|
||||
context2DtoImage++;
|
||||
return context2D.canvas;
|
||||
}, 1, 1);
|
||||
Convertor.learn("__TEST__image", "__TEST__canvas", image => {
|
||||
imageToCanvas++;
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage( image, 0, 0 );
|
||||
return canvas;
|
||||
}, 1, 1);
|
||||
Convertor.learn("__TEST__url", "__TEST__image", url => {
|
||||
return new Promise((resolve, reject) => {
|
||||
srcToImage++;
|
||||
const img = new Image();
|
||||
img.onerror = img.onabort = reject;
|
||||
img.onload = () => resolve(img);
|
||||
img.src = url;
|
||||
});
|
||||
}, 1, 1);
|
||||
|
||||
let canvasDestroy = 0, imageDestroy = 0, contex2DDestroy = 0, urlDestroy = 0;
|
||||
//also learn destructors
|
||||
Convertor.learnDestroy("__TEST__canvas", () => {
|
||||
canvasDestroy++;
|
||||
});
|
||||
Convertor.learnDestroy("__TEST__image", () => {
|
||||
imageDestroy++;
|
||||
});
|
||||
Convertor.learnDestroy("__TEST__context2d", () => {
|
||||
contex2DDestroy++;
|
||||
});
|
||||
Convertor.learnDestroy("__TEST__url", () => {
|
||||
urlDestroy++;
|
||||
});
|
||||
|
||||
|
||||
QUnit.module('TypeConversion', {
|
||||
beforeEach: function () {
|
||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||
|
||||
testLog.reset();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100 // Faster animation = faster tests
|
||||
});
|
||||
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
||||
},
|
||||
afterEach: function () {
|
||||
if (viewer && viewer.close) {
|
||||
viewer.close();
|
||||
}
|
||||
|
||||
viewer = null;
|
||||
imageToCanvas = 0; srcToImage = 0; context2DtoImage = 0;
|
||||
canvasToContext2D = 0; imageToUrl = 0; canvasToUrl = 0;
|
||||
canvasDestroy = 0; imageDestroy = 0; contex2DDestroy = 0; urlDestroy = 0;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Conversion path deduction', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
test.ok(Convertor.getConversionPath("__TEST__url", "__TEST__image"),
|
||||
"Type conversion ok between TEST types.");
|
||||
test.ok(Convertor.getConversionPath("canvas", "context2d"),
|
||||
"Type conversion ok between real types.");
|
||||
|
||||
test.equal(Convertor.getConversionPath("url", "__TEST__image"), undefined,
|
||||
"Type conversion not possible between TEST and real types.");
|
||||
test.equal(Convertor.getConversionPath("__TEST__canvas", "context2d"), undefined,
|
||||
"Type conversion not possible between TEST and real types.");
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
// ----------
|
||||
QUnit.test('Manual Data Convertors: testing conversion & destruction', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
//load image object: url -> image
|
||||
Convertor.convert("/test/data/A.png", "__TEST__url", "__TEST__image").then(i => {
|
||||
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
|
||||
test.equal(srcToImage, 1, "Conversion happened.");
|
||||
test.equal(urlDestroy, 1, "Url destructor called.");
|
||||
test.equal(imageDestroy, 0, "Image destructor not called.");
|
||||
return Convertor.convert(i, "__TEST__image", "__TEST__canvas");
|
||||
}).then(c => { //path image -> canvas
|
||||
test.equal(OpenSeadragon.type(c), "canvas", "Got canvas object after conversion.");
|
||||
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
||||
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
||||
test.equal(urlDestroy, 1, "Url destructor not called.");
|
||||
test.equal(imageDestroy, 1, "Image destructor called.");
|
||||
return Convertor.convert(c, "__TEST__canvas", "__TEST__image");
|
||||
}).then(i => { //path canvas, image: canvas -> url -> image
|
||||
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
|
||||
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
|
||||
test.equal(imageToCanvas, 1, "Conversion image->canvas did not happened.");
|
||||
test.equal(context2DtoImage, 0, "Conversion c2d->image did not happened.");
|
||||
test.equal(canvasToContext2D, 0, "Conversion canvas->c2d did not happened.");
|
||||
test.equal(canvasToUrl, 1, "Conversion canvas->url happened.");
|
||||
test.equal(imageToUrl, 0, "Conversion image->url did not happened.");
|
||||
|
||||
test.equal(urlDestroy, 2, "Url destructor called.");
|
||||
test.equal(imageDestroy, 1, "Image destructor not called.");
|
||||
test.equal(canvasDestroy, 1, "Canvas destructor called.");
|
||||
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('Data Convertors via Cache object: testing conversion & destruction', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
const dummyRect = new OpenSeadragon.Rect(0, 0, 0, 0, 0);
|
||||
const dummyTile = new OpenSeadragon.Tile(0, 0, 0, dummyRect, true, "",
|
||||
undefined, true, null, dummyRect, "", "key");
|
||||
|
||||
const cache = new OpenSeadragon.CacheRecord();
|
||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||
|
||||
//load image object: url -> image
|
||||
cache.getData("__TEST__image").then(_ => {
|
||||
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
|
||||
test.equal(srcToImage, 1, "Conversion happened.");
|
||||
test.equal(urlDestroy, 1, "Url destructor called.");
|
||||
test.equal(imageDestroy, 0, "Image destructor not called.");
|
||||
return cache.getData("__TEST__canvas");
|
||||
}).then(_ => { //path image -> canvas
|
||||
test.equal(OpenSeadragon.type(cache.data), "canvas", "Got canvas object after conversion.");
|
||||
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
||||
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
||||
test.equal(urlDestroy, 1, "Url destructor not called.");
|
||||
test.equal(imageDestroy, 1, "Image destructor called.");
|
||||
return cache.getData("__TEST__image");
|
||||
}).then(_ => { //path canvas, image: canvas -> url -> image
|
||||
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
|
||||
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
|
||||
test.equal(imageToCanvas, 1, "Conversion image->canvas did not happened.");
|
||||
test.equal(context2DtoImage, 0, "Conversion c2d->image did not happened.");
|
||||
test.equal(canvasToContext2D, 0, "Conversion canvas->c2d did not happened.");
|
||||
test.equal(canvasToUrl, 1, "Conversion canvas->url happened.");
|
||||
test.equal(imageToUrl, 0, "Conversion image->url did not happened.");
|
||||
|
||||
test.equal(urlDestroy, 2, "Url destructor called.");
|
||||
test.equal(imageDestroy, 1, "Image destructor not called.");
|
||||
test.equal(canvasDestroy, 1, "Canvas destructor called.");
|
||||
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
||||
}).then(_ => {
|
||||
cache.destroy();
|
||||
|
||||
test.equal(urlDestroy, 2, "Url destructor not called.");
|
||||
test.equal(imageDestroy, 2, "Image destructor called.");
|
||||
test.equal(canvasDestroy, 1, "Canvas destructor not called.");
|
||||
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('Deletion cache while being in the conversion process', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
let conversionHappened = false;
|
||||
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", _ => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
conversionHappened = true;
|
||||
resolve("some interesting data");
|
||||
}, 20);
|
||||
});
|
||||
}, 1, 1);
|
||||
let destructionHappened = false;
|
||||
Convertor.learnDestroy("__TEST__longConversionProcessForTesting", _ => {
|
||||
destructionHappened = true;
|
||||
});
|
||||
|
||||
const dummyRect = new OpenSeadragon.Rect(0, 0, 0, 0, 0);
|
||||
const dummyTile = new OpenSeadragon.Tile(0, 0, 0, dummyRect, true, "",
|
||||
undefined, true, null, dummyRect, "", "key");
|
||||
|
||||
const cache = new OpenSeadragon.CacheRecord();
|
||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||
cache.getData("__TEST__longConversionProcessForTesting").then(_ => {
|
||||
test.ok(conversionHappened, "Interrupted conversion finished.");
|
||||
test.ok(cache.loaded, "Cache is loaded.");
|
||||
test.equal(cache.data, "some interesting data", "We got the correct data.");
|
||||
test.equal(cache.type, "__TEST__longConversionProcessForTesting", "Cache declares new type.");
|
||||
test.equal(urlDestroy, 1, "Url was destroyed.");
|
||||
|
||||
//destruction will likely happen after we finish current async callback
|
||||
setTimeout(() => {
|
||||
test.ok(destructionHappened, "Interrupted conversion finished.");
|
||||
done();
|
||||
}, 25);
|
||||
});
|
||||
test.ok(!cache.loaded, "Cache is still not loaded.");
|
||||
test.equal(cache.data, undefined, "Cache is still not loaded.");
|
||||
test.equal(cache.type, "__TEST__longConversionProcessForTesting", "Cache already declares new type.");
|
||||
cache.destroy();
|
||||
test.equal(cache.type, "__TEST__longConversionProcessForTesting",
|
||||
"Type not erased immediatelly as we still process the data.");
|
||||
test.ok(!conversionHappened, "We destroyed cache before conversion finished.");
|
||||
});
|
||||
|
||||
// TODO: The ultimate integration test:
|
||||
// three items: one plain image data
|
||||
// one modified image data by two different plugins
|
||||
// one modified data by custom code that creates its own cache
|
||||
|
||||
|
||||
// QUnit.test('Manual Data Convertors: testing conversion & destruction', function (test) {
|
||||
// const done = test.async();
|
||||
//
|
||||
//
|
||||
//
|
||||
// viewer.world.addHandler('add-item', event => {
|
||||
// waitFor(() => {
|
||||
// if (event.item._fullyLoaded) {
|
||||
//
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// viewer.open('/test/data/testpattern.dzi');
|
||||
// });
|
||||
|
||||
})();
|
@ -33,6 +33,7 @@
|
||||
<script src="/test/modules/event-source.js"></script>
|
||||
<script src="/test/modules/viewerretrieval.js"></script>
|
||||
<script src="/test/modules/basic.js"></script>
|
||||
<script src="/test/modules/typeConversion.js"></script>
|
||||
<script src="/test/modules/strings.js"></script>
|
||||
<script src="/test/modules/formats.js"></script>
|
||||
<script src="/test/modules/iiif.js"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user