mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-22 21:26:10 +03:00
Implement cache manipulation strategy: default copy on access if tile in the rendering process, remove 'canvas' type support, many bugfixes and new tests.
This commit is contained in:
parent
2a1090ffa8
commit
2c67860c61
@ -181,29 +181,33 @@ $.DataTypeConvertor = class {
|
||||
constructor() {
|
||||
this.graph = new WeightedGraph();
|
||||
this.destructors = {};
|
||||
this.copyings = {};
|
||||
|
||||
// Teaching OpenSeadragon built-in conversions:
|
||||
|
||||
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;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage( image, 0, 0 );
|
||||
return canvas;
|
||||
}, 1, 1);
|
||||
this.learn("url", "image", url => {
|
||||
return new $.Promise((resolve, reject) => {
|
||||
const imageCreator = (url) => new $.Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onerror = img.onabort = reject;
|
||||
img.onload = () => resolve(img);
|
||||
img.src = url;
|
||||
});
|
||||
}, 1, 1);
|
||||
const canvasContextCreator = (imageData) => {
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage( imageData, 0, 0 );
|
||||
return context;
|
||||
};
|
||||
|
||||
this.learn("context2d", "url", ctx => ctx.canvas.toDataURL(), 1, 2);
|
||||
this.learn("image", "url", image => image.url);
|
||||
this.learn("image", "context2d", canvasContextCreator, 1, 1);
|
||||
this.learn("url", "image", imageCreator, 1, 1);
|
||||
|
||||
//Copies
|
||||
this.learn("image", "image", image => imageCreator(image.src), 1, 1);
|
||||
this.learn("url", "url", url => url, 0, 1); //strings are immutable, no need to copy
|
||||
this.learn("context2d", "context2d", ctx => canvasContextCreator(ctx.canvas));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,6 +280,9 @@ $.DataTypeConvertor = class {
|
||||
$.console.assert(costPower >= 0 && costPower <= 7, "[DataTypeConvertor] Conversion costPower must be between <0, 7>.");
|
||||
$.console.assert($.isFunction(callback), "[DataTypeConvertor:learn] Callback must be a valid function!");
|
||||
|
||||
if (from === to) {
|
||||
this.copyings[to] = callback;
|
||||
} else {
|
||||
//we won't know if somebody added multiple edges, though it will choose some edge anyway
|
||||
costPower++;
|
||||
costMultiplier = Math.min(Math.max(costMultiplier, 1), 10 ^ 5);
|
||||
@ -284,6 +291,7 @@ $.DataTypeConvertor = class {
|
||||
this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
|
||||
this._known = {}; //invalidate precomputed paths :/
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teach the system to destroy data type 'type'
|
||||
@ -301,6 +309,9 @@ $.DataTypeConvertor = class {
|
||||
* Convert data item x of type 'from' to any of the 'to' types, chosen is the cheapest known conversion.
|
||||
* Data is destroyed upon conversion. For different behavior, implement your conversion using the
|
||||
* path rules obtained from getConversionPath().
|
||||
* Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion).
|
||||
* It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these
|
||||
* manually if these should be destroyed.
|
||||
* @param {*} x data item to convert
|
||||
* @param {string} from data item type
|
||||
* @param {string} to desired type(s)
|
||||
@ -315,7 +326,7 @@ $.DataTypeConvertor = class {
|
||||
|
||||
const stepCount = conversionPath.length,
|
||||
_this = this;
|
||||
const step = (x, i) => {
|
||||
const step = (x, i, destroy = true) => {
|
||||
if (i >= stepCount) {
|
||||
return $.Promise.resolve(x);
|
||||
}
|
||||
@ -326,23 +337,46 @@ $.DataTypeConvertor = class {
|
||||
return $.Promise.resolve();
|
||||
}
|
||||
//node.value holds the type string
|
||||
_this.destroy(edge.origin.value, x);
|
||||
if (destroy) {
|
||||
_this.destroy(x, edge.origin.value);
|
||||
}
|
||||
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
|
||||
return result.then(res => step(res, i + 1));
|
||||
};
|
||||
return step(x, 0);
|
||||
//destroy only mid-results, but not the original value
|
||||
return step(x, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the data item given.
|
||||
* @param {string} type data type
|
||||
* @param {?} data
|
||||
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor
|
||||
*/
|
||||
destroy(type, data) {
|
||||
copy(data, type) {
|
||||
const copyTransform = this.copyings[type];
|
||||
if (copyTransform) {
|
||||
const y = copyTransform(data);
|
||||
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
|
||||
}
|
||||
$.console.warn(`[OpenSeadragon.convertor.copy] is not supported with type %s`, type);
|
||||
return $.Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the data item given.
|
||||
* @param {string} type data type
|
||||
* @param {?} data
|
||||
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor, or undefined
|
||||
* if not such conversion exists
|
||||
*/
|
||||
destroy(data, type) {
|
||||
const destructor = this.destructors[type];
|
||||
if (destructor) {
|
||||
destructor(data);
|
||||
const y = destructor(data);
|
||||
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,7 +263,7 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea
|
||||
|
||||
if (data.preferredFormats) {
|
||||
for (var f = 0; f < data.preferredFormats.length; f++ ) {
|
||||
if ( OpenSeadragon.imageFormatSupported(data.preferredFormats[f]) ) {
|
||||
if ( $.imageFormatSupported(data.preferredFormats[f]) ) {
|
||||
data.tileFormat = data.preferredFormats[f];
|
||||
break;
|
||||
}
|
||||
|
@ -707,6 +707,12 @@
|
||||
* NOTE: passing POST data from URL by this feature only supports string values, however,
|
||||
* TileSource can send any data using POST as long as the header is correct
|
||||
* (@see OpenSeadragon.TileSource.prototype.getTilePostData)
|
||||
*
|
||||
* @property {Boolean} [callTileLoadedWithCachedData=false]
|
||||
* tile-loaded event is called only for tiles that downloaded new data or
|
||||
* their data is stored in the original form in a suplementary cache object.
|
||||
* Caches that render directly from re-used cache does not trigger this event again,
|
||||
* as possible modifications would be applied twice.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -1207,6 +1213,7 @@ function OpenSeadragon( options ){
|
||||
loadTilesWithAjax: false,
|
||||
ajaxHeaders: {},
|
||||
splitHashDataForPost: false,
|
||||
callTileLoadedWithCachedData: false,
|
||||
|
||||
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
|
||||
panHorizontal: true,
|
||||
|
135
src/tile.js
135
src/tile.js
@ -284,6 +284,10 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
||||
* @private
|
||||
*/
|
||||
this._caches = {};
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._cacheSize = 0;
|
||||
};
|
||||
|
||||
/** @lends OpenSeadragon.Tile.prototype */
|
||||
@ -360,7 +364,7 @@ $.Tile.prototype = {
|
||||
* @returns {Image}
|
||||
*/
|
||||
get image() {
|
||||
$.console.error("[Tile.image] property has been deprecated. Use [Tile.prototype.getImage] instead.");
|
||||
$.console.error("[Tile.image] property has been deprecated. Use [Tile.getData] instead.");
|
||||
return this.getImage();
|
||||
},
|
||||
|
||||
@ -372,7 +376,7 @@ $.Tile.prototype = {
|
||||
* @returns {String}
|
||||
*/
|
||||
get url() {
|
||||
$.console.error("[Tile.url] property has been deprecated. Use [Tile.prototype.getUrl] instead.");
|
||||
$.console.error("[Tile.url] property has been deprecated. Use [Tile.getUrl] instead.");
|
||||
return this.getUrl();
|
||||
},
|
||||
|
||||
@ -381,7 +385,14 @@ $.Tile.prototype = {
|
||||
* @returns {?Image}
|
||||
*/
|
||||
getImage: function() {
|
||||
return this.getData("image");
|
||||
//TODO: after merge $.console.error("[Tile.getImage] property has been deprecated. Use [Tile.getData] instead.");
|
||||
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
|
||||
const cache = this.getCache(this.cacheKey);
|
||||
if (!cache) {
|
||||
return undefined;
|
||||
}
|
||||
cache.transformTo("image");
|
||||
return cache.data;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -402,7 +413,14 @@ $.Tile.prototype = {
|
||||
* @returns {?CanvasRenderingContext2D}
|
||||
*/
|
||||
getCanvasContext: function() {
|
||||
return this.getData("context2d");
|
||||
//TODO: after merge $.console.error("[Tile.getCanvasContext] property has been deprecated. Use [Tile.getData] instead.");
|
||||
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
|
||||
const cache = this.getCache(this.cacheKey);
|
||||
if (!cache) {
|
||||
return undefined;
|
||||
}
|
||||
cache.transformTo("context2d");
|
||||
return cache.data;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -411,8 +429,8 @@ $.Tile.prototype = {
|
||||
* @type {CanvasRenderingContext2D} context2D
|
||||
*/
|
||||
get context2D() {
|
||||
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::getCache().");
|
||||
return this.getData("context2d");
|
||||
$.console.error("[Tile.context2D] property has been deprecated. Use [Tile.getData] instead.");
|
||||
return this.getCanvasContext();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -420,7 +438,7 @@ $.Tile.prototype = {
|
||||
* @deprecated
|
||||
*/
|
||||
set context2D(value) {
|
||||
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::setCache().");
|
||||
$.console.error("[Tile.context2D] property has been deprecated. Use [Tile.setData] instead.");
|
||||
this.setData(value, "context2d");
|
||||
},
|
||||
|
||||
@ -440,49 +458,63 @@ $.Tile.prototype = {
|
||||
*/
|
||||
set cacheImageRecord(value) {
|
||||
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::setCache.");
|
||||
this._caches[this.cacheKey] = value;
|
||||
const cache = this._caches[this.cacheKey];
|
||||
|
||||
if (!value) {
|
||||
this.unsetCache(this.cacheKey);
|
||||
} else {
|
||||
const _this = this;
|
||||
cache.await().then(x => _this.setCache(this.cacheKey, x, cache.type, false));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the default data for this tile
|
||||
* @param {?string} [type=undefined] data type to require
|
||||
* @param {string} type data type to require
|
||||
* @param {boolean?} [copy=this.loaded] whether to force copy retrieval
|
||||
* @return {*|undefined} data in the desired type, or undefined if a conversion is ongoing
|
||||
*/
|
||||
getData(type = undefined) {
|
||||
getData: function(type, copy = this.loaded) {
|
||||
//we return the data synchronously immediatelly (undefined if conversion happens)
|
||||
const cache = this.getCache(this.cacheKey);
|
||||
if (!cache) {
|
||||
$.console.error("[Tile::getData] There is no cache available for tile with key " + this.cacheKey);
|
||||
return undefined;
|
||||
}
|
||||
cache.getData(type); //returns a promise
|
||||
//we return the data synchronously immediatelly (undefined if conversion happens)
|
||||
return cache.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate the tile so that viewport gets updated.
|
||||
*/
|
||||
save() {
|
||||
const parent = this.tiledImage;
|
||||
if (parent) {
|
||||
parent._needsDraw = true;
|
||||
}
|
||||
return cache.getDataAs(type, copy);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set cache data
|
||||
* @param {*} value
|
||||
* @param {?string} [type=undefined] data type to require
|
||||
* @param {?string} type data type to require
|
||||
* @param {boolean} [preserveOriginalData=true] if true and cacheKey === originalCacheKey,
|
||||
* then stores the underlying data as 'original' and changes the cacheKey to point
|
||||
* to a new data. This makes the Tile assigned to two cache objects.
|
||||
*/
|
||||
setData(value, type = undefined) {
|
||||
this.setCache(this.cacheKey, value, type);
|
||||
setData: function(value, type, preserveOriginalData = true) {
|
||||
if (preserveOriginalData && this.cacheKey === this.originalCacheKey) {
|
||||
//caches equality means we have only one cache:
|
||||
// change current pointer to a new cache and create it: new tiles will
|
||||
// not arrive at this data, but at originalCacheKey state
|
||||
this.cacheKey = "mod://" + this.originalCacheKey;
|
||||
return this.setCache(this.cacheKey, value, type)._promise;
|
||||
}
|
||||
//else overwrite cache
|
||||
const cache = this.getCache(this.cacheKey);
|
||||
if (!cache) {
|
||||
$.console.error("[Tile::setData] There is no cache available for tile with key " + this.cacheKey);
|
||||
return $.Promise.resolve();
|
||||
}
|
||||
return cache.setDataAs(value, type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Read tile cache data object (CacheRecord)
|
||||
* @param {string} key cache key to read that belongs to this tile
|
||||
* @param {string?} [key=this.cacheKey] cache key to read that belongs to this tile
|
||||
* @return {OpenSeadragon.CacheRecord}
|
||||
*/
|
||||
getCache: function(key) {
|
||||
getCache: function(key = this.cacheKey) {
|
||||
return this._caches[key];
|
||||
},
|
||||
|
||||
@ -495,12 +527,15 @@ $.Tile.prototype = {
|
||||
* @param {?string} type data type, will be guessed if not provided
|
||||
* @param [_safely=true] private
|
||||
* @param [_cutoff=0] private
|
||||
* @returns {OpenSeadragon.CacheRecord} - The cache record the tile was attached to.
|
||||
*/
|
||||
setCache: function(key, data, type = undefined, _safely = true, _cutoff = 0) {
|
||||
if (!type && this.tiledImage && !this.tiledImage.__typeWarningReported) {
|
||||
if (!type) {
|
||||
if (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);
|
||||
}
|
||||
|
||||
@ -508,18 +543,54 @@ $.Tile.prototype = {
|
||||
//todo later, we could have drawers register their supported rendering type
|
||||
// and OpenSeadragon would check compatibility automatically, now we render
|
||||
// using two main types so we check their ability
|
||||
const conversion = $.convertor.getConversionPath(type, "canvas", "image");
|
||||
const conversion = $.convertor.getConversionPath(type, "context2d");
|
||||
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" +
|
||||
"to render. Make sure OpenSeadragon.convertor was taught to convert type: " + type);
|
||||
}
|
||||
|
||||
this.tiledImage._tileCache.cacheTile({
|
||||
const cachedItem = this.tiledImage._tileCache.cacheTile({
|
||||
data: data,
|
||||
dataType: type,
|
||||
tile: this,
|
||||
cacheKey: key,
|
||||
cutoff: _cutoff
|
||||
});
|
||||
const havingRecord = this._caches[key];
|
||||
if (havingRecord !== cachedItem) {
|
||||
if (!havingRecord) {
|
||||
this._cacheSize++;
|
||||
}
|
||||
this._caches[key] = cachedItem;
|
||||
}
|
||||
return cachedItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of caches available to this tile
|
||||
* @returns {number} number of caches
|
||||
*/
|
||||
getCacheSize: function() {
|
||||
return this._cacheSize;
|
||||
},
|
||||
|
||||
/**
|
||||
* Free tile cache. Removes by default the cache record if no other tile uses it.
|
||||
* @param {string} key cache key, required
|
||||
* @param {boolean} [freeIfUnused=true] set to false if zombie should be created
|
||||
*/
|
||||
unsetCache: function(key, freeIfUnused = true) {
|
||||
if (this.cacheKey === key) {
|
||||
if (this.cacheKey !== this.originalCacheKey) {
|
||||
this.cacheKey = this.originalCacheKey;
|
||||
} else {
|
||||
$.console.warn("[Tile.unsetCache] trying to remove the only cache that is used to draw the tile!");
|
||||
}
|
||||
}
|
||||
if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused)) {
|
||||
//if we managed to free tile from record, we are sure we decreased cache count
|
||||
this._cacheSize--;
|
||||
delete this._caches[key];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -680,10 +751,12 @@ $.Tile.prototype = {
|
||||
|
||||
this.tiledImage = null;
|
||||
this._caches = [];
|
||||
this._cacheSize = 0;
|
||||
this.element = null;
|
||||
this.imgElement = null;
|
||||
this.loaded = false;
|
||||
this.loading = false;
|
||||
this.cacheKey = this.originalCacheKey;
|
||||
}
|
||||
};
|
||||
|
||||
|
393
src/tilecache.js
393
src/tilecache.js
@ -47,65 +47,197 @@
|
||||
*
|
||||
* @typedef {{
|
||||
* destroy: function,
|
||||
* revive: function,
|
||||
* save: function,
|
||||
* getData: function,
|
||||
* getDataAs: function,
|
||||
* transformTo: function,
|
||||
* data: ?,
|
||||
* loaded: boolean
|
||||
* }} OpenSeadragon.CacheRecord
|
||||
*/
|
||||
$.CacheRecord = class {
|
||||
constructor() {
|
||||
this._tiles = [];
|
||||
this._data = null;
|
||||
this.loaded = false;
|
||||
this._promise = $.Promise.resolve();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
//make sure this gets destroyed even if loaded=false
|
||||
if (this.loaded) {
|
||||
$.convertor.destroy(this._type, this._data);
|
||||
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;
|
||||
this.revive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the cache record data directly. Preferred way of data access.
|
||||
* Might be undefined if this.loaded = false.
|
||||
* You can access the data in synchronous way, but the data might not be available.
|
||||
* If you want to access the data indirectly (await), use this.transformTo or this.getDataAs
|
||||
* @return {any}
|
||||
*/
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the cache type. The type can dynamically change, but should be consistent at
|
||||
* one point in the time. For available types see the OpenSeadragon.Convertor, or the tutorials.
|
||||
* @return {string}
|
||||
*/
|
||||
get type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
save() {
|
||||
for (let tile of this._tiles) {
|
||||
tile._needsDraw = true;
|
||||
/**
|
||||
* Await ongoing process so that we get cache ready on callback.
|
||||
* @returns {null|*}
|
||||
*/
|
||||
await() {
|
||||
if (!this._promise) { //if not cache loaded, do not fail
|
||||
return $.Promise.resolve();
|
||||
}
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
getData(type = this._type) {
|
||||
getImage() {
|
||||
$.console.error("[CacheRecord.getImage] options.image is deprecated. Moreover, it might not work" +
|
||||
" correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");
|
||||
this.transformTo("image");
|
||||
return this.data;
|
||||
}
|
||||
|
||||
getRenderedContext() {
|
||||
$.console.error("[CacheRecord.getRenderedContext] options.getRenderedContext is deprecated. Moreover, it might not work" +
|
||||
" correctly as the cache system performs conversion asynchronously in case the type needs to be converted.");
|
||||
this.transformTo("context2d");
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache data. Asynchronous.
|
||||
* @param {any} data
|
||||
* @param {string} type
|
||||
* @returns {OpenSeadragon.Promise<?>} the old cache data that has been overwritten
|
||||
*/
|
||||
setDataAs(data, type) {
|
||||
//allow set data with destroyed state, destroys the data if necessary
|
||||
$.console.assert(data !== undefined, "[CacheRecord.setDataAs] needs valid data to set!");
|
||||
if (this._conversionJobQueue) {
|
||||
//delay saving if ongiong conversion, these were registered first
|
||||
let resolver = null;
|
||||
const promise = new $.Promise((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
this._conversionJobQueue.push(() => resolver(this._overwriteData(data, type)));
|
||||
return promise;
|
||||
}
|
||||
return this._overwriteData(data, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the cache record data indirectly. Preferred way of data access. Asynchronous.
|
||||
* @param {string?} [type=this.type]
|
||||
* @param {boolean?} [copy=true] if false and same type is retrieved as the cache type,
|
||||
* copy is not performed: note that this is potentially dangerous as it might
|
||||
* introduce race conditions (you get a cache data direct reference you modify,
|
||||
* but others might also access it, for example drawers to draw the viewport).
|
||||
* @returns {OpenSeadragon.Promise<?>} desired data type in promise, undefined if the cache was destroyed
|
||||
*/
|
||||
getDataAs(type = this._type, copy = true) {
|
||||
if (this.loaded && type === this._type) {
|
||||
return copy ? $.convertor.copy(this._data, type) : this._promise;
|
||||
}
|
||||
|
||||
return this._promise.then(data => {
|
||||
//might get destroyed in meanwhile
|
||||
if (this._destroyed) {
|
||||
return undefined;
|
||||
}
|
||||
if (type !== this._type) {
|
||||
return $.convertor.convert(data, this._type, type);
|
||||
}
|
||||
if (copy) { //convert does not copy data if same type, do explicitly
|
||||
return $.convertor.copy(data, type);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform cache to desired type and get the data after conversion.
|
||||
* Does nothing if the type equals to the current type. Asynchronous.
|
||||
* @param {string} type
|
||||
* @return {OpenSeadragon.Promise<?>|*}
|
||||
*/
|
||||
transformTo(type = this._type) {
|
||||
if (!this.loaded || type !== this._type) {
|
||||
if (!this.loaded) {
|
||||
$.console.warn("Attempt to call getData with desired type %s, the tile data type is %s and the tile is not loaded!", type, this._type);
|
||||
return this._promise;
|
||||
this._conversionJobQueue = this._conversionJobQueue || [];
|
||||
let resolver = null;
|
||||
const promise = new $.Promise((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
this._conversionJobQueue.push(() => {
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
if (type !== this._type) {
|
||||
//ensures queue gets executed after finish
|
||||
this._convert(this._type, type);
|
||||
this._promise.then(data => resolver(data));
|
||||
} else {
|
||||
//must ensure manually, but after current promise finished, we won't wait for the following job
|
||||
this._promise.then(data => {
|
||||
this._checkAwaitsConvert();
|
||||
return resolver(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
this._convert(this._type, type);
|
||||
}
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial state, prepare for usage.
|
||||
* Must not be called on active cache, e.g. first call destroy().
|
||||
*/
|
||||
revive() {
|
||||
$.console.assert(!this.loaded && !this._type, "[CacheRecord::revive] must not be called when loaded!");
|
||||
this._tiles = [];
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this.loaded = false;
|
||||
this._promise = null;
|
||||
this._destroyed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free all the data and call data destructors if defined.
|
||||
*/
|
||||
destroy() {
|
||||
delete this._conversionJobQueue;
|
||||
this._destroyed = true;
|
||||
|
||||
//make sure this gets destroyed even if loaded=false
|
||||
if (this.loaded) {
|
||||
$.convertor.destroy(this._data, this._type);
|
||||
this._tiles = null;
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this._promise = null;
|
||||
} else {
|
||||
const oldType = this._type;
|
||||
this._promise.then(x => {
|
||||
//ensure old data destroyed
|
||||
$.convertor.destroy(x, oldType);
|
||||
//might get revived...
|
||||
if (!this._destroyed) {
|
||||
return;
|
||||
}
|
||||
this._tiles = null;
|
||||
this._data = null;
|
||||
this._type = null;
|
||||
this._promise = null;
|
||||
});
|
||||
}
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tile dependency on this record
|
||||
* @param tile
|
||||
@ -113,6 +245,9 @@ $.CacheRecord = class {
|
||||
* @param type
|
||||
*/
|
||||
addTile(tile, data, type) {
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||
|
||||
//allow overriding the cache - existing tile or different type
|
||||
@ -124,28 +259,28 @@ $.CacheRecord = class {
|
||||
this._promise = $.Promise.resolve(data);
|
||||
this._data = data;
|
||||
this.loaded = true;
|
||||
} else if (this._type !== type) {
|
||||
//pass: the tile data type will silently change
|
||||
// as it inherits this cache
|
||||
// todo do not call events?
|
||||
}
|
||||
|
||||
//else pass: the tile data type will silently change as it inherits this cache
|
||||
this._tiles.push(tile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tile dependency on this record.
|
||||
* @param tile
|
||||
* @returns {Boolean} true if record removed
|
||||
*/
|
||||
removeTile(tile) {
|
||||
if (this._destroyed) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this._tiles.length; i++) {
|
||||
if (this._tiles[i] === tile) {
|
||||
this._tiles.splice(i, 1);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,7 +288,57 @@ $.CacheRecord = class {
|
||||
* @return {number}
|
||||
*/
|
||||
getTileCount() {
|
||||
return this._tiles.length;
|
||||
return this._tiles ? this._tiles.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private conversion that makes sure collided requests are
|
||||
* processed eventually
|
||||
* @private
|
||||
*/
|
||||
_checkAwaitsConvert() {
|
||||
if (!this._conversionJobQueue || this._destroyed) {
|
||||
return;
|
||||
}
|
||||
//let other code finish first
|
||||
setTimeout(() => {
|
||||
//check again, meanwhile things might've changed
|
||||
if (!this._conversionJobQueue || this._destroyed) {
|
||||
return;
|
||||
}
|
||||
const job = this._conversionJobQueue[0];
|
||||
this._conversionJobQueue.splice(0, 1);
|
||||
if (this._conversionJobQueue.length === 0) {
|
||||
delete this._conversionJobQueue;
|
||||
}
|
||||
job();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely overwrite the cache data and return the old data
|
||||
* @private
|
||||
*/
|
||||
_overwriteData(data, type) {
|
||||
if (this._destroyed) {
|
||||
//we take ownership of the data, destroy
|
||||
$.convertor.destroy(data, type);
|
||||
return $.Promise.resolve();
|
||||
}
|
||||
if (this.loaded) {
|
||||
$.convertor.destroy(this._data, this._type);
|
||||
this._type = type;
|
||||
this._data = data;
|
||||
this._promise = $.Promise.resolve(data);
|
||||
return this._promise;
|
||||
}
|
||||
return this._promise.then(x => {
|
||||
$.convertor.destroy(x, this._type);
|
||||
this._type = type;
|
||||
this._data = data;
|
||||
this._promise = $.Promise.resolve(data);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,6 +360,7 @@ $.CacheRecord = class {
|
||||
if (i >= stepCount) {
|
||||
_this._data = x;
|
||||
_this.loaded = true;
|
||||
_this._checkAwaitsConvert();
|
||||
return $.Promise.resolve(x);
|
||||
}
|
||||
let edge = conversionPath[i];
|
||||
@ -189,7 +375,7 @@ $.CacheRecord = class {
|
||||
return originalData;
|
||||
}
|
||||
//node.value holds the type string
|
||||
convertor.destroy(edge.origin.value, x);
|
||||
convertor.destroy(x, edge.origin.value);
|
||||
return convert(y, i + 1);
|
||||
}
|
||||
);
|
||||
@ -232,15 +418,23 @@ $.TileCache = class {
|
||||
return this._tilesLoaded.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number} The total number of cached objects (+ zombies)
|
||||
*/
|
||||
numCachesLoaded() {
|
||||
return this._zombiesLoadedCount + this._cachesLoadedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the specified tile, removing an old tile if necessary to stay under the
|
||||
* maxImageCacheCount specified on construction. Note that if multiple tiles reference
|
||||
* the same image, there may be more tiles than maxImageCacheCount; the goal is to keep
|
||||
* the number of images below that number. Note, as well, that even the number of images
|
||||
* may temporarily surpass that number, but should eventually come back down to the max specified.
|
||||
* @private
|
||||
* @param {Object} options - Tile info.
|
||||
* @param {OpenSeadragon.Tile} options.tile - The tile to cache.
|
||||
* @param {String} [options.cacheKey=undefined] - Cache Key to use. Defaults to options.tile.cacheKey
|
||||
* @param {?String} [options.cacheKey=undefined] - Cache Key to use. Defaults to options.tile.cacheKey
|
||||
* @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache.
|
||||
* Used if cacheKey not set.
|
||||
* @param {Image} options.image - The image of the tile to cache. Deprecated.
|
||||
@ -249,30 +443,33 @@ $.TileCache = class {
|
||||
* @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this
|
||||
* function will release an old tile. The cutoff option specifies a tile level at or below which
|
||||
* tiles will not be released.
|
||||
* @returns {OpenSeadragon.CacheRecord} - The cache record the tile was attached to.
|
||||
*/
|
||||
cacheTile( options ) {
|
||||
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
||||
$.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
|
||||
$.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
|
||||
const theTile = options.tile;
|
||||
$.console.assert( theTile, "[TileCache.cacheTile] options.tile is required" );
|
||||
$.console.assert( theTile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
|
||||
|
||||
let cutoff = options.cutoff || 0,
|
||||
insertionIndex = this._tilesLoaded.length,
|
||||
cacheKey = options.cacheKey || options.tile.cacheKey;
|
||||
cacheKey = options.cacheKey || theTile.cacheKey;
|
||||
|
||||
let cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
|
||||
if (!cacheRecord) {
|
||||
|
||||
if (!options.data) {
|
||||
if (options.data === undefined) {
|
||||
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
|
||||
"has been deprecated and will be removed in the future.");
|
||||
options.data = options.image;
|
||||
}
|
||||
|
||||
$.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an CacheRecord" );
|
||||
//allow anything but undefined, null, false (other values mean the data was set, for example '0')
|
||||
$.console.assert( options.data !== undefined && options.data !== null && options.data !== false,
|
||||
"[TileCache.cacheTile] options.data is required to create an CacheRecord" );
|
||||
cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord();
|
||||
this._cachesLoadedCount++;
|
||||
} else if (!cacheRecord.getTileCount()) {
|
||||
//revive zombie
|
||||
} else if (cacheRecord._destroyed) {
|
||||
cacheRecord.revive();
|
||||
delete this._zombiesLoaded[cacheKey];
|
||||
this._zombiesLoadedCount--;
|
||||
}
|
||||
@ -282,11 +479,12 @@ $.TileCache = class {
|
||||
"For easier use of the cache system, use the tile instance API.");
|
||||
options.dataType = $.convertor.guessType(options.data);
|
||||
}
|
||||
cacheRecord.addTile(options.tile, options.data, options.dataType);
|
||||
options.tile._caches[ cacheKey ] = cacheRecord;
|
||||
|
||||
cacheRecord.addTile(theTile, options.data, options.dataType);
|
||||
|
||||
// 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.
|
||||
let worstTileIndex = -1;
|
||||
if ( this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount ) {
|
||||
//prefer zombie deletion, faster, better
|
||||
if (this._zombiesLoadedCount > 0) {
|
||||
@ -298,7 +496,6 @@ $.TileCache = class {
|
||||
}
|
||||
} else {
|
||||
let worstTile = null;
|
||||
let worstTileIndex = -1;
|
||||
let prevTile, worstTime, worstLevel, prevTime, prevLevel;
|
||||
|
||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||
@ -325,13 +522,20 @@ $.TileCache = class {
|
||||
}
|
||||
|
||||
if ( worstTile && worstTileIndex >= 0 ) {
|
||||
this._unloadTile(worstTile, true);
|
||||
this.unloadTile(worstTile, true);
|
||||
insertionIndex = worstTileIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._tilesLoaded[ insertionIndex ] = options.tile;
|
||||
if (theTile.getCacheSize() === 0) {
|
||||
this._tilesLoaded[ insertionIndex ] = theTile;
|
||||
} else if (worstTileIndex >= 0) {
|
||||
//tile is already recorded, do not add tile, but remove the tile at insertion index
|
||||
this._tilesLoaded.splice(insertionIndex, 1);
|
||||
}
|
||||
|
||||
return cacheRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -344,7 +548,7 @@ $.TileCache = class {
|
||||
|
||||
let cacheOverflows = this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount;
|
||||
if (tiledImage._zombieCache && cacheOverflows && this._zombiesLoadedCount > 0) {
|
||||
//prefer newer zombies
|
||||
//prefer newer (fresh ;) zombies
|
||||
for (let zombie in this._zombiesLoaded) {
|
||||
this._zombiesLoaded[zombie].destroy();
|
||||
delete this._zombiesLoaded[zombie];
|
||||
@ -355,22 +559,60 @@ $.TileCache = class {
|
||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||
tile = this._tilesLoaded[ i ];
|
||||
|
||||
//todo might be errorprone: tile.loading true--> problem! maybe set some other flag by
|
||||
if (tile.tiledImage === tiledImage) {
|
||||
if (!tile.loaded) {
|
||||
//iterates from the array end, safe to remove
|
||||
this._tilesLoaded.splice( i, 1 );
|
||||
i--;
|
||||
} else if ( tile.tiledImage === tiledImage ) {
|
||||
//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);
|
||||
this.unloadTile(tile, !tiledImage._zombieCache || cacheOverflows, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private
|
||||
/**
|
||||
* Get cache record (might be a unattached record, i.e. a zombie)
|
||||
* @param cacheKey
|
||||
* @returns {OpenSeadragon.CacheRecord|undefined}
|
||||
*/
|
||||
getCacheRecord(cacheKey) {
|
||||
$.console.assert(cacheKey, '[TileCache.getCacheRecord] cacheKey is required');
|
||||
return this._cachesLoaded[cacheKey];
|
||||
return this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache record for a given til
|
||||
* @param {OpenSeadragon.Tile} tile
|
||||
* @param {string} key cache key
|
||||
* @param {boolean} destroy if true, empty cache is destroyed, else left as a zombie
|
||||
* @private
|
||||
*/
|
||||
unloadCacheForTile(tile, key, destroy) {
|
||||
const cacheRecord = this._cachesLoaded[key];
|
||||
//unload record only if relevant - the tile exists in the record
|
||||
if (cacheRecord) {
|
||||
if (cacheRecord.removeTile(tile)) {
|
||||
if (!cacheRecord.getTileCount()) {
|
||||
if (destroy) {
|
||||
// #1 tile marked as destroyed (e.g. too much cached tiles or not a zombie)
|
||||
cacheRecord.destroy();
|
||||
} else {
|
||||
// #2 Tile is a zombie. Do not delete record, reuse.
|
||||
this._zombiesLoaded[key] = cacheRecord;
|
||||
this._zombiesLoadedCount++;
|
||||
}
|
||||
// Either way clear cache
|
||||
delete this._cachesLoaded[key];
|
||||
this._cachesLoadedCount--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
$.console.error("[TileCache.unloadCacheForTile] System tried to delete tile from cache it " +
|
||||
"does not belong to! This could mean a bug in the cache system.");
|
||||
return false;
|
||||
}
|
||||
$.console.warn("[TileCache.unloadCacheForTile] Attempting to delete missing cache!");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -379,36 +621,19 @@ $.TileCache = class {
|
||||
* @param deleteAtIndex index to remove the tile record at, will not remove from _tiledLoaded 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) {
|
||||
const cacheRecord = this._cachesLoaded[key];
|
||||
if (cacheRecord) {
|
||||
cacheRecord.removeTile(tile);
|
||||
if (!cacheRecord.getTileCount()) {
|
||||
if (destroy) {
|
||||
// #1 tile marked as destroyed (e.g. too much cached tiles or not a zombie)
|
||||
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++;
|
||||
//we are 'ok' to remove tile caches here since we later call destroy on tile, otherwise
|
||||
//tile has count of its cache size --> would be inconsistent
|
||||
this.unloadCacheForTile(tile, key, destroy);
|
||||
}
|
||||
//delete also the tile record
|
||||
if (deleteAtIndex !== undefined) {
|
||||
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||
}
|
||||
} else if (deleteAtIndex !== undefined) {
|
||||
// #3 Cache stays. Tile record needs to be removed anyway, since the tile is removed.
|
||||
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||
}
|
||||
} else {
|
||||
$.console.warn("[TileCache._unloadTile] Attempting to delete missing cache!");
|
||||
}
|
||||
}
|
||||
|
||||
const tiledImage = tile.tiledImage;
|
||||
tile.unload();
|
||||
|
||||
|
@ -83,6 +83,8 @@
|
||||
* Defaults to the setting in {@link OpenSeadragon.Options}.
|
||||
* @param {Object} [options.ajaxHeaders={}]
|
||||
* A set of headers to include when making tile AJAX requests.
|
||||
* @param {Boolean} [options.callTileLoadedWithCachedData]
|
||||
* Invoke tile-loded event for also for tiles loaded from cache if true.
|
||||
*/
|
||||
$.TiledImage = function( options ) {
|
||||
var _this = this;
|
||||
@ -184,7 +186,8 @@ $.TiledImage = function( options ) {
|
||||
preload: $.DEFAULT_SETTINGS.preload,
|
||||
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
|
||||
subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency,
|
||||
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame
|
||||
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame,
|
||||
callTileLoadedWithCachedData: $.DEFAULT_SETTINGS.callTileLoadedWithCachedData
|
||||
}, options );
|
||||
|
||||
this._preload = this.preload;
|
||||
@ -1531,28 +1534,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
);
|
||||
|
||||
if (!tile.loaded && !tile.loading) {
|
||||
// Tile was created or its data removed: check whether cache has the data before downloading.
|
||||
if (!tile.cacheKey) {
|
||||
tile.cacheKey = "";
|
||||
tile.originalCacheKey = "";
|
||||
}
|
||||
|
||||
//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 {
|
||||
similarCacheRecord.getData().then(data =>
|
||||
this._setTileLoaded(tile, data, cutoff, null, similarCacheRecord.type));
|
||||
}
|
||||
}
|
||||
// Tile was created or its data removed: check whether cache has the data.
|
||||
// this method sets tile.loading=true if data available, which prevents
|
||||
// job creation later on
|
||||
this._tryFindTileCacheRecord(tile);
|
||||
}
|
||||
|
||||
if ( tile.loaded ) {
|
||||
@ -1577,6 +1562,45 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
return best;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inner
|
||||
* Try to find existing cache of the tile
|
||||
* @param {OpenSeadragon.Tile} tile
|
||||
*/
|
||||
_tryFindTileCacheRecord: function(tile) {
|
||||
if (!tile.cacheKey) {
|
||||
tile.cacheKey = "";
|
||||
tile.originalCacheKey = "";
|
||||
}
|
||||
|
||||
let record = this._tileCache.getCacheRecord(tile.cacheKey);
|
||||
const cutoff = this.source.getClosestLevel();
|
||||
|
||||
if (record) {
|
||||
//setup without calling tile loaded event! tile cache is ready for usage,
|
||||
tile.loading = true;
|
||||
tile.loaded = false;
|
||||
//set data as null, cache already has data, it does not overwrite
|
||||
this._setTileLoaded(tile, null, cutoff, null, record.type,
|
||||
this.callTileLoadedWithCachedData);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tile.cacheKey !== tile.originalCacheKey) {
|
||||
//we found original data: this data will be used to re-execute the pipeline
|
||||
record = this._tileCache.getCacheRecord(tile.originalCacheKey);
|
||||
if (record) {
|
||||
tile.loading = true;
|
||||
tile.loaded = false;
|
||||
//set data as null, cache already has data, it does not overwrite
|
||||
this._setTileLoaded(tile, null, cutoff, null, record.type);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inner
|
||||
@ -1779,8 +1803,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
* @param {?Number} cutoff
|
||||
* @param {?XMLHttpRequest} tileRequest
|
||||
* @param {?String} [dataType=undefined] data type, derived automatically if not set
|
||||
* @param {?Boolean} [withEvent=true] do not trigger event if true
|
||||
*/
|
||||
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) {
|
||||
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType, withEvent = true) {
|
||||
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
|
||||
// -> reason why it is not in the constructor
|
||||
tile.setCache(tile.cacheKey, data, dataType, false, cutoff);
|
||||
@ -1811,7 +1836,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
resolver(tile);
|
||||
} else if (cache.type !== requiredType) {
|
||||
//initiate conversion as soon as possible if incompatible with the drawer
|
||||
cache.getData(requiredType).then(_ => {
|
||||
cache.transformTo(requiredType).then(_ => {
|
||||
tile.loading = false;
|
||||
tile.loaded = true;
|
||||
resolver(tile);
|
||||
@ -1821,12 +1846,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
tile.loaded = true;
|
||||
resolver(tile);
|
||||
}
|
||||
|
||||
//FIXME: design choice: cache tile now set automatically so users can do
|
||||
// tile.getCache(...) inside this event, but maybe we would like to have users
|
||||
// freedom to decide on the cache creation (note, tiles now MUST have cache, e.g.
|
||||
// it is no longer possible to store all tiles in the memory as it was with context2D prop)
|
||||
tile.save();
|
||||
}
|
||||
|
||||
function getCompletionCallback() {
|
||||
@ -1839,6 +1858,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
||||
}
|
||||
|
||||
const fallbackCompletion = getCompletionCallback();
|
||||
if (!withEvent) {
|
||||
fallbackCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a tile has just been loaded in memory. That means that the
|
||||
|
@ -915,7 +915,8 @@ $.TileSource.prototype = {
|
||||
* @deprecated
|
||||
*/
|
||||
getTileCacheData: function(cacheObject) {
|
||||
return cacheObject.getData();
|
||||
$.console.error("[TileSource.getTileCacheData] has been deprecated. Use cache API of a tile instead.");
|
||||
return cacheObject.getDataAs(undefined, false);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -930,7 +931,7 @@ $.TileSource.prototype = {
|
||||
*/
|
||||
getTileCacheDataAsImage: function(cacheObject) {
|
||||
$.console.error("[TileSource.getTileCacheDataAsImage] has been deprecated. Use cache API of a tile instead.");
|
||||
return cacheObject.getData("image");
|
||||
return cacheObject.getImage();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -944,7 +945,7 @@ $.TileSource.prototype = {
|
||||
*/
|
||||
getTileCacheDataAsContext2D: function(cacheObject) {
|
||||
$.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead.");
|
||||
return cacheObject.getData("context2d");
|
||||
return cacheObject.getRenderedContext();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1623,7 +1623,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
||||
loadTilesWithAjax: queueItem.options.loadTilesWithAjax,
|
||||
ajaxHeaders: queueItem.options.ajaxHeaders,
|
||||
debugMode: _this.debugMode,
|
||||
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency
|
||||
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency,
|
||||
callTileLoadedWithCachedData: _this.callTileLoadedWithCachedData,
|
||||
});
|
||||
|
||||
if (_this.collectionMode) {
|
||||
|
@ -77,7 +77,7 @@
|
||||
options.minLevel = 0;
|
||||
options.maxLevel = options.gridSize.length - 1;
|
||||
|
||||
OpenSeadragon.TileSource.apply(this, [options]);
|
||||
$.TileSource.apply(this, [options]);
|
||||
};
|
||||
|
||||
$.extend($.ZoomifyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ZoomifyTileSource.prototype */ {
|
||||
|
@ -1,6 +1,9 @@
|
||||
/* global QUnit, testLog */
|
||||
|
||||
(function() {
|
||||
const Convertor = OpenSeadragon.convertor,
|
||||
T_A = "__TEST__typeA", T_B = "__TEST__typeB", T_C = "__TEST__typeC", T_D = "__TEST__typeD", T_E = "__TEST__typeE";
|
||||
|
||||
let viewer;
|
||||
|
||||
//we override jobs: remember original function
|
||||
@ -15,6 +18,82 @@
|
||||
}, 20);
|
||||
}
|
||||
|
||||
function createFakeTile(url, tiledImage, loading=false, loaded=true) {
|
||||
const dummyRect = new OpenSeadragon.Rect(0, 0, 0, 0, 0);
|
||||
//default cutoof = 0 --> use level 1 to not to keep caches from unloading (cutoff = navigator data, kept in cache)
|
||||
const dummyTile = new OpenSeadragon.Tile(1, 0, 0, dummyRect, true, url,
|
||||
undefined, true, null, dummyRect, null, url);
|
||||
dummyTile.tiledImage = tiledImage;
|
||||
dummyTile.loading = loading;
|
||||
dummyTile.loaded = loaded;
|
||||
return dummyTile;
|
||||
}
|
||||
|
||||
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
||||
// other tests will interfere
|
||||
let typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
||||
//set all same costs to get easy testing, know which path will be taken
|
||||
Convertor.learn(T_A, T_B, x => {
|
||||
typeAtoB++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_B, T_C, x => {
|
||||
typeBtoC++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_C, T_A, x => {
|
||||
typeCtoA++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_D, T_A, x => {
|
||||
typeDtoA++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_C, T_E, x => {
|
||||
typeCtoE++;
|
||||
return x+1;
|
||||
});
|
||||
//'Copy constructors'
|
||||
let copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
||||
//also learn destructors
|
||||
Convertor.learn(T_A, T_A,x => {
|
||||
copyA++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_B, T_B,x => {
|
||||
copyB++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_C, T_C,x => {
|
||||
copyC++;
|
||||
return x-1;
|
||||
});
|
||||
Convertor.learn(T_D, T_D,x => {
|
||||
copyD++;
|
||||
return x+1;
|
||||
});
|
||||
Convertor.learn(T_E, T_E,x => {
|
||||
copyE++;
|
||||
return x+1;
|
||||
});
|
||||
let destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
||||
//also learn destructors
|
||||
Convertor.learnDestroy(T_A, () => {
|
||||
destroyA++;
|
||||
});
|
||||
Convertor.learnDestroy(T_B, () => {
|
||||
destroyB++;
|
||||
});
|
||||
Convertor.learnDestroy(T_C, () => {
|
||||
destroyC++;
|
||||
});
|
||||
Convertor.learnDestroy(T_D, () => {
|
||||
destroyD++;
|
||||
});
|
||||
Convertor.learnDestroy(T_E, () => {
|
||||
destroyE++;
|
||||
});
|
||||
|
||||
// ----------
|
||||
QUnit.module('TileCache', {
|
||||
beforeEach: function () {
|
||||
@ -42,54 +121,38 @@
|
||||
// ----------
|
||||
// TODO: this used to be async
|
||||
QUnit.test('basics', function(assert) {
|
||||
var done = assert.async();
|
||||
var fakeViewer = {
|
||||
const done = assert.async();
|
||||
const fakeViewer = {
|
||||
raiseEvent: function() {}
|
||||
};
|
||||
var fakeTiledImage0 = {
|
||||
const fakeTiledImage0 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype
|
||||
};
|
||||
var fakeTiledImage1 = {
|
||||
const fakeTiledImage1 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype
|
||||
};
|
||||
|
||||
var fakeTile0 = {
|
||||
url: 'foo.jpg',
|
||||
cacheKey: 'foo.jpg',
|
||||
image: {},
|
||||
loaded: true,
|
||||
tiledImage: fakeTiledImage0,
|
||||
_caches: [],
|
||||
unload: function() {}
|
||||
};
|
||||
const tile0 = createFakeTile('foo.jpg', fakeTiledImage0);
|
||||
const tile1 = createFakeTile('foo.jpg', fakeTiledImage1);
|
||||
|
||||
var fakeTile1 = {
|
||||
url: 'foo.jpg',
|
||||
cacheKey: 'foo.jpg',
|
||||
image: {},
|
||||
loaded: true,
|
||||
tiledImage: fakeTiledImage1,
|
||||
_caches: [],
|
||||
unload: function() {}
|
||||
};
|
||||
|
||||
var cache = new OpenSeadragon.TileCache();
|
||||
const cache = new OpenSeadragon.TileCache();
|
||||
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
||||
|
||||
cache.cacheTile({
|
||||
tile: fakeTile0,
|
||||
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
||||
tile: tile0,
|
||||
tiledImage: fakeTiledImage0
|
||||
});
|
||||
tile0._cacheSize++;
|
||||
|
||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after cache');
|
||||
|
||||
cache.cacheTile({
|
||||
tile: fakeTile1,
|
||||
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
||||
tile: tile1,
|
||||
tiledImage: fakeTiledImage1
|
||||
});
|
||||
|
||||
tile1._cacheSize++;
|
||||
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
|
||||
|
||||
cache.clearTilesFor(fakeTiledImage0);
|
||||
@ -105,75 +168,369 @@
|
||||
|
||||
// ----------
|
||||
QUnit.test('maxImageCacheCount', function(assert) {
|
||||
var done = assert.async();
|
||||
var fakeViewer = {
|
||||
const done = assert.async();
|
||||
const fakeViewer = {
|
||||
raiseEvent: function() {}
|
||||
};
|
||||
var fakeTiledImage0 = {
|
||||
const fakeTiledImage0 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype
|
||||
};
|
||||
|
||||
var fakeTile0 = {
|
||||
url: 'different.jpg',
|
||||
cacheKey: 'different.jpg',
|
||||
image: {},
|
||||
loaded: true,
|
||||
tiledImage: fakeTiledImage0,
|
||||
_caches: [],
|
||||
unload: function() {}
|
||||
};
|
||||
const tile0 = createFakeTile('different.jpg', fakeTiledImage0);
|
||||
const tile1 = createFakeTile('same.jpg', fakeTiledImage0);
|
||||
const tile2 = createFakeTile('same.jpg', fakeTiledImage0);
|
||||
|
||||
var fakeTile1 = {
|
||||
url: 'same.jpg',
|
||||
cacheKey: 'same.jpg',
|
||||
image: {},
|
||||
loaded: true,
|
||||
tiledImage: fakeTiledImage0,
|
||||
_caches: [],
|
||||
unload: function() {}
|
||||
};
|
||||
|
||||
var fakeTile2 = {
|
||||
url: 'same.jpg',
|
||||
cacheKey: 'same.jpg',
|
||||
image: {},
|
||||
loaded: true,
|
||||
tiledImage: fakeTiledImage0,
|
||||
_caches: [],
|
||||
unload: function() {}
|
||||
};
|
||||
|
||||
var cache = new OpenSeadragon.TileCache({
|
||||
const cache = new OpenSeadragon.TileCache({
|
||||
maxImageCacheCount: 1
|
||||
});
|
||||
|
||||
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
||||
|
||||
cache.cacheTile({
|
||||
tile: fakeTile0,
|
||||
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
||||
tile: tile0,
|
||||
tiledImage: fakeTiledImage0
|
||||
});
|
||||
tile0._cacheSize++;
|
||||
|
||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add');
|
||||
|
||||
cache.cacheTile({
|
||||
tile: fakeTile1,
|
||||
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
||||
tile: tile1,
|
||||
tiledImage: fakeTiledImage0
|
||||
});
|
||||
tile1._cacheSize++;
|
||||
|
||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add of second image');
|
||||
|
||||
cache.cacheTile({
|
||||
tile: fakeTile2,
|
||||
tile2._caches[tile2.cacheKey] = cache.cacheTile({
|
||||
tile: tile2,
|
||||
tiledImage: fakeTiledImage0
|
||||
});
|
||||
tile2._cacheSize++;
|
||||
|
||||
assert.equal(cache.numTilesLoaded(), 2, 'tile count after additional same image');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
//Tile API and cache interaction
|
||||
QUnit.test('Tile API: basic conversion', function(test) {
|
||||
const done = test.async();
|
||||
const fakeViewer = {
|
||||
raiseEvent: function() {}
|
||||
};
|
||||
const tileCache = new OpenSeadragon.TileCache();
|
||||
const fakeTiledImage0 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype,
|
||||
_tileCache: tileCache
|
||||
};
|
||||
const fakeTiledImage1 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype,
|
||||
_tileCache: tileCache
|
||||
};
|
||||
|
||||
//load data
|
||||
const tile00 = createFakeTile('foo.jpg', fakeTiledImage0);
|
||||
tile00.setCache(tile00.cacheKey, 0, T_A, false);
|
||||
const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0);
|
||||
tile01.setCache(tile01.cacheKey, 0, T_B, false);
|
||||
const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1);
|
||||
tile10.setCache(tile10.cacheKey, 0, T_C, false);
|
||||
const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1);
|
||||
tile11.setCache(tile11.cacheKey, 0, T_C, false);
|
||||
const tile12 = createFakeTile('foo.jpg', fakeTiledImage1);
|
||||
tile12.setCache(tile12.cacheKey, 0, T_A, false);
|
||||
|
||||
const collideGetSet = async (tile, type) => {
|
||||
const value = await tile.getData(type, false);
|
||||
await tile.setData(value, type, false);
|
||||
return value;
|
||||
};
|
||||
|
||||
//test set/get data in async env
|
||||
(async function() {
|
||||
test.equal(tileCache.numTilesLoaded(), 5, "We loaded 5 tiles");
|
||||
test.equal(tileCache.numCachesLoaded(), 3, "We loaded 3 cache objects");
|
||||
|
||||
//test structure
|
||||
const c00 = tile00.getCache(tile00.cacheKey);
|
||||
test.equal(c00.getTileCount(), 2, "Two tiles share key = url = foo.jpg.");
|
||||
const c01 = tile01.getCache(tile01.cacheKey);
|
||||
test.equal(c01.getTileCount(), 1, "No tiles share key = url = foo2.jpg.");
|
||||
const c10 = tile10.getCache(tile10.cacheKey);
|
||||
test.equal(c10.getTileCount(), 2, "Two tiles share key = url = foo3.jpg.");
|
||||
const c12 = tile12.getCache(tile12.cacheKey);
|
||||
|
||||
//test get/set data A
|
||||
let value = await tile00.getData(undefined, false);
|
||||
test.equal(typeAtoB, 0, "No conversion happened when requesting default type data.");
|
||||
test.equal(value, 0, "No conversion, no increase in value A.");
|
||||
//explicit type
|
||||
value = await tile00.getData(T_A, false);
|
||||
test.equal(typeAtoB, 0, "No conversion also for tile sharing the cache.");
|
||||
test.equal(value, 0, "Again, no increase in value A.");
|
||||
|
||||
//copy & set type A
|
||||
value = await tile00.getData(T_A, true);
|
||||
test.equal(typeAtoB, 0, "No conversion also for tile sharing the cache.");
|
||||
test.equal(copyA, 1, "A copy happened.");
|
||||
test.equal(value, 1, "+1 conversion step happened.");
|
||||
await tile00.setData(value, T_A, false); //overwrite
|
||||
test.equal(tile00.cacheKey, tile00.originalCacheKey, "Overwriting cache: no change in value.");
|
||||
test.equal(c00.type, T_A, "The tile cache data type was unchanged.");
|
||||
//convert to B, async + sync behavior
|
||||
value = await tile00.getData(T_B, false);
|
||||
await tile00.setData(value, T_B, false); //overwrite
|
||||
test.equal(typeAtoB, 1, "Conversion A->B happened.");
|
||||
test.equal(value, 2, "+1 conversion step happened.");
|
||||
//shares cache with tile12 (overwrite=false)
|
||||
value = await tile12.getData(T_B, false);
|
||||
test.equal(typeAtoB, 1, "Conversion A->B happened only once.");
|
||||
test.equal(value, 2, "Value did not change.");
|
||||
|
||||
//test ASYNC get data
|
||||
value = await tile12.getData(T_B);
|
||||
await tile12.setData(value, T_B, false); //overwrite
|
||||
test.equal(typeAtoB, 1, "No conversion happened when requesting default type data.");
|
||||
test.equal(typeBtoC, 0, "No conversion happened when requesting default type data.");
|
||||
test.equal(copyB, 1, "B type copied.");
|
||||
test.equal(value, 3, "Copy, increase in value type B.");
|
||||
|
||||
// Async collisions testing
|
||||
|
||||
//convert to A, before that request conversion to A and B several times, since we copy
|
||||
// there should be just exactly the right amount of conversions
|
||||
tile12.getData(T_A); // B -> C -> A
|
||||
tile12.getData(T_B); // no conversion, all run at the same time
|
||||
tile12.getData(T_B); // no conversion, all run at the same time
|
||||
tile12.getData(T_A); // B -> C -> A
|
||||
tile12.getData(T_B); // no conversion, all run at the same time
|
||||
value = await tile12.getData(T_A); // B -> C -> A
|
||||
test.equal(typeAtoB, 1, "No conversion A->B.");
|
||||
test.equal(typeBtoC, 3, "Conversion B->C happened three times.");
|
||||
test.equal(typeCtoA, 3, "Conversion C->A happened three times.");
|
||||
test.equal(typeDtoA, 0, "Conversion D->A did not happen.");
|
||||
test.equal(typeCtoE, 0, "Conversion C->E did not happen.");
|
||||
test.equal(value, 5, "+2 conversion step happened, other conversion steps are copies discarded " +
|
||||
"(get data does not modify cache).");
|
||||
|
||||
//but direct requests on cache change await
|
||||
//convert to A, before that request conversion to A and B several times, should finish accordingly
|
||||
c12.transformTo(T_A); // B -> C -> A
|
||||
c12.transformTo(T_B); // A -> B second time
|
||||
c12.transformTo(T_B); // no-op
|
||||
c12.transformTo(T_A); // B -> C -> A
|
||||
c12.transformTo(T_B); // A -> B third time
|
||||
//should finish with next await with 6 steps at this point, add two more and await end
|
||||
value = await c12.transformTo(T_A); // B -> C -> A
|
||||
test.equal(typeAtoB, 3, "Conversion A->B happened three times.");
|
||||
test.equal(typeBtoC, 6, "Conversion B->C happened six times.");
|
||||
test.equal(typeCtoA, 6, "Conversion C->A happened six times.");
|
||||
test.equal(typeDtoA, 0, "Conversion D->A did not happen.");
|
||||
test.equal(typeCtoE, 0, "Conversion C->E did not happen.");
|
||||
test.equal(value, 11, "5-2+8 conversion step happened (the test above did not save the cache so 3 is value).");
|
||||
await tile12.setData(value, T_B, false); // B -> C -> A
|
||||
|
||||
// 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();
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
//Tile API and cache interaction
|
||||
QUnit.test('Tile API Cache Interaction', function(test) {
|
||||
const done = test.async();
|
||||
const fakeViewer = {
|
||||
raiseEvent: function() {}
|
||||
};
|
||||
const tileCache = new OpenSeadragon.TileCache();
|
||||
const fakeTiledImage0 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype,
|
||||
_tileCache: tileCache
|
||||
};
|
||||
const fakeTiledImage1 = {
|
||||
viewer: fakeViewer,
|
||||
source: OpenSeadragon.TileSource.prototype,
|
||||
_tileCache: tileCache
|
||||
};
|
||||
|
||||
//load data
|
||||
const tile00 = createFakeTile('foo.jpg', fakeTiledImage0);
|
||||
tile00.setCache(tile00.cacheKey, 0, T_A, false);
|
||||
const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0);
|
||||
tile01.setCache(tile01.cacheKey, 0, T_B, false);
|
||||
const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1);
|
||||
tile10.setCache(tile10.cacheKey, 0, T_C, false);
|
||||
const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1);
|
||||
tile11.setCache(tile11.cacheKey, 0, T_C, false);
|
||||
const tile12 = createFakeTile('foo.jpg', fakeTiledImage1);
|
||||
tile12.setCache(tile12.cacheKey, 0, T_A, false);
|
||||
|
||||
//test set/get data in async env
|
||||
(async function() {
|
||||
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.setCache("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.setCache("my_custom_cache2", 128, T_C);
|
||||
tile00.unsetCache("my_custom_cache2");
|
||||
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
|
||||
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items.");
|
||||
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
|
||||
|
||||
//delete cache as a zombie
|
||||
tile00.setCache("my_custom_cache2", 17, T_C);
|
||||
//direct access shoes correct value although we set key!
|
||||
const myCustomCache2Data = tile00.getCache("my_custom_cache2").data;
|
||||
test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene.");
|
||||
test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6.");
|
||||
//keep zombie
|
||||
tile00.unsetCache("my_custom_cache2", false);
|
||||
test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
|
||||
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
|
||||
|
||||
//revive zombie
|
||||
tile01.setCache("my_custom_cache2", 18, T_C);
|
||||
const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data;
|
||||
test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived.");
|
||||
//again, keep zombie
|
||||
tile01.unsetCache("my_custom_cache2", false);
|
||||
|
||||
//first create additional cache so zombie is not the youngest
|
||||
tile01.setCache("some weird cache", 11, T_A);
|
||||
test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys.");
|
||||
|
||||
//insertion aadditional cache clears the zombie first although it is not the youngest one
|
||||
test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
|
||||
|
||||
//Test CAP
|
||||
tileCache._maxCacheItemCount = 7;
|
||||
|
||||
//does not trigger insertion - deletion, since we setData to cache that already exists, 43 value ignored
|
||||
tile12.setData(43, T_B, true);
|
||||
test.notEqual(tile12.cacheKey, tile12.originalCacheKey, "Original cache key differs.");
|
||||
test.equal(theTileKey, tile12.originalCacheKey, "Original cache key preserved.");
|
||||
test.equal(tileCache.numCachesLoaded(), 7, "The cache has still 7 items.");
|
||||
//we called SET DATA with preserve=true on tile12 which was sharing cache with tile00, new cache is also shared
|
||||
test.equal(tile00.originalCacheKey, tile12.originalCacheKey, "Original cache key matches between tiles.");
|
||||
test.equal(tile00.cacheKey, tile12.cacheKey, "Modified cache key matches between tiles.");
|
||||
test.equal(tile12.getCache().data, 42, "The value is not 43 as setData triggers cache share!");
|
||||
|
||||
//triggers insertion - deletion of zombie cache 'my_custom_cache2'
|
||||
tile00.setCache("trigger-max-cache-handler", 5, T_C);
|
||||
//reset CAP
|
||||
tileCache._maxCacheItemCount = OpenSeadragon.DEFAULT_SETTINGS.maxImageCacheCount;
|
||||
|
||||
//try to revive zombie will fail: the zombie was deleted, we will find 18
|
||||
tile01.setCache("my_custom_cache2", 18, T_C);
|
||||
const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
|
||||
test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because created.");
|
||||
test.equal(myCustomCache2RecreatedData, 18, "Cache data is actually as set to 18.");
|
||||
test.equal(tileCache.numCachesLoaded(), 8, "The cache has now 8 items.");
|
||||
|
||||
|
||||
//delete cache bound to other tiles, this tile has 4 caches:
|
||||
// cacheKey: shared, originalCacheKey: shared, <custom cache key>, <custom cache key>
|
||||
// note that cacheKey is shared because we called setData on two items that both create MOD cache
|
||||
tileCache.unloadTile(tile00, true, tileCache._tilesLoaded.indexOf(tile00));
|
||||
test.equal(tileCache.numCachesLoaded(), 6, "The cache has now 8-2 items.");
|
||||
test.equal(tileCache.numTilesLoaded(), 4, "One tile removed.");
|
||||
test.equal(c00.getTileCount(), 1, "The cache has still tile12 left.");
|
||||
|
||||
//now test tile destruction as zombie
|
||||
|
||||
//now test tile cache sharing
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
QUnit.test('Zombie Cache', function(test) {
|
||||
const done = test.async();
|
||||
|
||||
|
@ -27,6 +27,7 @@
|
||||
|
||||
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
||||
// other tests will interfere
|
||||
// Note: this is not the same as in the production conversion, where CANVAS on its own does not exist
|
||||
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
|
||||
@ -81,6 +82,7 @@
|
||||
});
|
||||
|
||||
|
||||
|
||||
QUnit.module('TypeConversion', {
|
||||
beforeEach: function () {
|
||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||
@ -113,7 +115,7 @@
|
||||
|
||||
test.ok(Convertor.getConversionPath("__TEST__url", "__TEST__image"),
|
||||
"Type conversion ok between TEST types.");
|
||||
test.ok(Convertor.getConversionPath("canvas", "context2d"),
|
||||
test.ok(Convertor.getConversionPath("url", "context2d"),
|
||||
"Type conversion ok between real types.");
|
||||
|
||||
test.equal(Convertor.getConversionPath("url", "__TEST__image"), undefined,
|
||||
@ -124,15 +126,61 @@
|
||||
done();
|
||||
});
|
||||
|
||||
QUnit.test('Copy of build-in types', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
//prepare data
|
||||
const URL = "/test/data/A.png";
|
||||
const image = new Image();
|
||||
image.onerror = image.onabort = () => {
|
||||
test.ok(false, "Image data preparation failed to load!");
|
||||
done();
|
||||
};
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
//test when ready
|
||||
image.onload = async () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage( image, 0, 0 );
|
||||
|
||||
//copy URL
|
||||
const URL2 = await Convertor.copy(URL, "url");
|
||||
//we cannot check if they are not the same object, strings are immutable (and we don't copy anyway :D )
|
||||
test.equal(URL, URL2, "String copy is equal in data.");
|
||||
test.equal(typeof URL, typeof URL2, "Type of copies equals.");
|
||||
test.equal(URL.length, URL2.length, "Data length is also equal.");
|
||||
|
||||
//copy context
|
||||
const context2 = await Convertor.copy(context, "context2d");
|
||||
test.notEqual(context, context2, "Copy is not the same as original canvas.");
|
||||
test.equal(typeof context, typeof context2, "Type of copies equals.");
|
||||
test.equal(context.canvas.toDataURL(), context2.canvas.toDataURL(), "Data is equal.");
|
||||
|
||||
//copy image
|
||||
const image2 = await Convertor.copy(image, "image");
|
||||
test.notEqual(image, image2, "Copy is not the same as original image.");
|
||||
test.equal(typeof image, typeof image2, "Type of copies equals.");
|
||||
test.equal(image.src, image2.src, "Data is equal.");
|
||||
|
||||
done();
|
||||
};
|
||||
image.src = URL;
|
||||
});
|
||||
|
||||
// ----------
|
||||
QUnit.test('Manual Data Convertors: testing conversion & destruction', function (test) {
|
||||
QUnit.test('Manual Data Convertors: testing conversion, copies & 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, 0, "Url destructor not called automatically.");
|
||||
Convertor.destroy("/test/data/A.png", "__TEST__url");
|
||||
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
|
||||
@ -140,7 +188,7 @@
|
||||
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.");
|
||||
test.equal(imageDestroy, 0, "Image destructor not called unless we ask it.");
|
||||
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.");
|
||||
@ -152,8 +200,8 @@
|
||||
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(imageDestroy, 0, "Image destructor not called.");
|
||||
test.equal(canvasDestroy, 0, "Canvas destructor called.");
|
||||
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
||||
done();
|
||||
});
|
||||
@ -170,19 +218,19 @@
|
||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||
|
||||
//load image object: url -> image
|
||||
cache.getData("__TEST__image").then(_ => {
|
||||
cache.transformTo("__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");
|
||||
return cache.transformTo("__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");
|
||||
return cache.transformTo("__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.");
|
||||
@ -208,15 +256,113 @@
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('Data Convertors via Cache object: testing set/get', 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.testGetSet = async function(type) {
|
||||
const value = await cache.getDataAs(type, false);
|
||||
await cache.setDataAs(value, type);
|
||||
return value;
|
||||
}
|
||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||
|
||||
//load image object: url -> image
|
||||
cache.testGetSet("__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.testGetSet("__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.testGetSet("__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 after a copy was requested but not yet processed.', function (test) {
|
||||
const done = test.async();
|
||||
|
||||
let conversionHappened = false;
|
||||
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", value => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
conversionHappened = true;
|
||||
resolve("modified " + value);
|
||||
}, 20);
|
||||
});
|
||||
}, 1, 1);
|
||||
let longConversionDestroy = 0;
|
||||
Convertor.learnDestroy("__TEST__longConversionProcessForTesting", _ => {
|
||||
longConversionDestroy++;
|
||||
});
|
||||
|
||||
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.getDataAs("__TEST__longConversionProcessForTesting").then(convertedData => {
|
||||
test.equal(longConversionDestroy, 0, "Copy not destroyed.");
|
||||
test.notOk(cache.loaded, "Cache was destroyed.");
|
||||
test.equal(cache.data, undefined, "Already destroyed cache does not return data.");
|
||||
test.equal(urlDestroy, 1, "Url was destroyed.");
|
||||
test.notOk(conversionHappened, "Nothing happened since before us the cache was deleted.");
|
||||
|
||||
//destruction will likely happen after we finish current async callback
|
||||
setTimeout(async () => {
|
||||
test.notOk(conversionHappened, "Still no conversion.");
|
||||
done();
|
||||
}, 25);
|
||||
});
|
||||
test.ok(cache.loaded, "Cache is still not loaded.");
|
||||
test.equal(cache.data, "/test/data/A.png", "Get data does not override cache.");
|
||||
test.equal(cache.type, "__TEST__url", "Cache did not change its type.");
|
||||
cache.destroy();
|
||||
test.notOk(cache.type, "Type erased immediatelly as the data copy is out.");
|
||||
test.equal(urlDestroy, 1, "We destroyed cache before copy conversion finished.");
|
||||
});
|
||||
|
||||
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", _ => {
|
||||
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", value => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
conversionHappened = true;
|
||||
resolve("some interesting data");
|
||||
resolve("modified " + value);
|
||||
}, 20);
|
||||
});
|
||||
}, 1, 1);
|
||||
@ -231,10 +377,10 @@
|
||||
|
||||
const cache = new OpenSeadragon.CacheRecord();
|
||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||
cache.getData("__TEST__longConversionProcessForTesting").then(_ => {
|
||||
cache.transformTo("__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.data, "modified /test/data/A.png", "We got the correct data.");
|
||||
test.equal(cache.type, "__TEST__longConversionProcessForTesting", "Cache declares new type.");
|
||||
test.equal(urlDestroy, 1, "Url was destroyed.");
|
||||
|
||||
@ -253,6 +399,7 @@
|
||||
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
|
@ -33,7 +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/type-conversion.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