mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-23 05:36:09 +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() {
|
constructor() {
|
||||||
this.graph = new WeightedGraph();
|
this.graph = new WeightedGraph();
|
||||||
this.destructors = {};
|
this.destructors = {};
|
||||||
|
this.copyings = {};
|
||||||
|
|
||||||
// Teaching OpenSeadragon built-in conversions:
|
// Teaching OpenSeadragon built-in conversions:
|
||||||
|
const imageCreator = (url) => new $.Promise((resolve, reject) => {
|
||||||
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 img = new Image();
|
const img = new Image();
|
||||||
img.onerror = img.onabort = reject;
|
img.onerror = img.onabort = reject;
|
||||||
img.onload = () => resolve(img);
|
img.onload = () => resolve(img);
|
||||||
img.src = url;
|
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(costPower >= 0 && costPower <= 7, "[DataTypeConvertor] Conversion costPower must be between <0, 7>.");
|
||||||
$.console.assert($.isFunction(callback), "[DataTypeConvertor:learn] Callback must be a valid function!");
|
$.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
|
//we won't know if somebody added multiple edges, though it will choose some edge anyway
|
||||||
costPower++;
|
costPower++;
|
||||||
costMultiplier = Math.min(Math.max(costMultiplier, 1), 10 ^ 5);
|
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.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
|
||||||
this._known = {}; //invalidate precomputed paths :/
|
this._known = {}; //invalidate precomputed paths :/
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Teach the system to destroy data type 'type'
|
* 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.
|
* 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
|
* Data is destroyed upon conversion. For different behavior, implement your conversion using the
|
||||||
* path rules obtained from getConversionPath().
|
* 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 {*} x data item to convert
|
||||||
* @param {string} from data item type
|
* @param {string} from data item type
|
||||||
* @param {string} to desired type(s)
|
* @param {string} to desired type(s)
|
||||||
@ -315,7 +326,7 @@ $.DataTypeConvertor = class {
|
|||||||
|
|
||||||
const stepCount = conversionPath.length,
|
const stepCount = conversionPath.length,
|
||||||
_this = this;
|
_this = this;
|
||||||
const step = (x, i) => {
|
const step = (x, i, destroy = true) => {
|
||||||
if (i >= stepCount) {
|
if (i >= stepCount) {
|
||||||
return $.Promise.resolve(x);
|
return $.Promise.resolve(x);
|
||||||
}
|
}
|
||||||
@ -326,23 +337,46 @@ $.DataTypeConvertor = class {
|
|||||||
return $.Promise.resolve();
|
return $.Promise.resolve();
|
||||||
}
|
}
|
||||||
//node.value holds the type string
|
//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);
|
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
|
||||||
return result.then(res => step(res, i + 1));
|
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.
|
* Destroy the data item given.
|
||||||
* @param {string} type data type
|
* @param {string} type data type
|
||||||
* @param {?} data
|
* @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];
|
const destructor = this.destructors[type];
|
||||||
if (destructor) {
|
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) {
|
if (data.preferredFormats) {
|
||||||
for (var f = 0; f < data.preferredFormats.length; f++ ) {
|
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];
|
data.tileFormat = data.preferredFormats[f];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -707,6 +707,12 @@
|
|||||||
* NOTE: passing POST data from URL by this feature only supports string values, however,
|
* 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
|
* TileSource can send any data using POST as long as the header is correct
|
||||||
* (@see OpenSeadragon.TileSource.prototype.getTilePostData)
|
* (@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,
|
loadTilesWithAjax: false,
|
||||||
ajaxHeaders: {},
|
ajaxHeaders: {},
|
||||||
splitHashDataForPost: false,
|
splitHashDataForPost: false,
|
||||||
|
callTileLoadedWithCachedData: false,
|
||||||
|
|
||||||
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
|
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
|
||||||
panHorizontal: true,
|
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
|
* @private
|
||||||
*/
|
*/
|
||||||
this._caches = {};
|
this._caches = {};
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._cacheSize = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @lends OpenSeadragon.Tile.prototype */
|
/** @lends OpenSeadragon.Tile.prototype */
|
||||||
@ -360,7 +364,7 @@ $.Tile.prototype = {
|
|||||||
* @returns {Image}
|
* @returns {Image}
|
||||||
*/
|
*/
|
||||||
get 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();
|
return this.getImage();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -372,7 +376,7 @@ $.Tile.prototype = {
|
|||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
get url() {
|
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();
|
return this.getUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -381,7 +385,14 @@ $.Tile.prototype = {
|
|||||||
* @returns {?Image}
|
* @returns {?Image}
|
||||||
*/
|
*/
|
||||||
getImage: function() {
|
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}
|
* @returns {?CanvasRenderingContext2D}
|
||||||
*/
|
*/
|
||||||
getCanvasContext: function() {
|
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
|
* @type {CanvasRenderingContext2D} context2D
|
||||||
*/
|
*/
|
||||||
get context2D() {
|
get context2D() {
|
||||||
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::getCache().");
|
$.console.error("[Tile.context2D] property has been deprecated. Use [Tile.getData] instead.");
|
||||||
return this.getData("context2d");
|
return this.getCanvasContext();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -420,7 +438,7 @@ $.Tile.prototype = {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
set context2D(value) {
|
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");
|
this.setData(value, "context2d");
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -440,49 +458,63 @@ $.Tile.prototype = {
|
|||||||
*/
|
*/
|
||||||
set cacheImageRecord(value) {
|
set cacheImageRecord(value) {
|
||||||
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::setCache.");
|
$.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
|
* 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
|
* @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);
|
const cache = this.getCache(this.cacheKey);
|
||||||
if (!cache) {
|
if (!cache) {
|
||||||
|
$.console.error("[Tile::getData] There is no cache available for tile with key " + this.cacheKey);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
cache.getData(type); //returns a promise
|
return cache.getDataAs(type, copy);
|
||||||
//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;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set cache data
|
* Set cache data
|
||||||
* @param {*} value
|
* @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) {
|
setData: function(value, type, preserveOriginalData = true) {
|
||||||
this.setCache(this.cacheKey, value, type);
|
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)
|
* 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}
|
* @return {OpenSeadragon.CacheRecord}
|
||||||
*/
|
*/
|
||||||
getCache: function(key) {
|
getCache: function(key = this.cacheKey) {
|
||||||
return this._caches[key];
|
return this._caches[key];
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -495,12 +527,15 @@ $.Tile.prototype = {
|
|||||||
* @param {?string} type data type, will be guessed if not provided
|
* @param {?string} type data type, will be guessed if not provided
|
||||||
* @param [_safely=true] private
|
* @param [_safely=true] private
|
||||||
* @param [_cutoff=0] 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) {
|
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. " +
|
$.console.warn(this, "[Tile.setCache] called without type specification. " +
|
||||||
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
|
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
|
||||||
this.tiledImage.__typeWarningReported = true;
|
this.tiledImage.__typeWarningReported = true;
|
||||||
|
}
|
||||||
type = $.convertor.guessType(data);
|
type = $.convertor.guessType(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,18 +543,54 @@ $.Tile.prototype = {
|
|||||||
//todo later, we could have drawers register their supported rendering type
|
//todo later, we could have drawers register their supported rendering type
|
||||||
// and OpenSeadragon would check compatibility automatically, now we render
|
// and OpenSeadragon would check compatibility automatically, now we render
|
||||||
// using two main types so we check their ability
|
// 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" +
|
$.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);
|
"to render. Make sure OpenSeadragon.convertor was taught to convert type: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tiledImage._tileCache.cacheTile({
|
const cachedItem = this.tiledImage._tileCache.cacheTile({
|
||||||
data: data,
|
data: data,
|
||||||
dataType: type,
|
dataType: type,
|
||||||
tile: this,
|
tile: this,
|
||||||
cacheKey: key,
|
cacheKey: key,
|
||||||
cutoff: _cutoff
|
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.tiledImage = null;
|
||||||
this._caches = [];
|
this._caches = [];
|
||||||
|
this._cacheSize = 0;
|
||||||
this.element = null;
|
this.element = null;
|
||||||
this.imgElement = null;
|
this.imgElement = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.cacheKey = this.originalCacheKey;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
393
src/tilecache.js
393
src/tilecache.js
@ -47,65 +47,197 @@
|
|||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* destroy: function,
|
* destroy: function,
|
||||||
|
* revive: function,
|
||||||
* save: function,
|
* save: function,
|
||||||
* getData: function,
|
* getDataAs: function,
|
||||||
|
* transformTo: function,
|
||||||
* data: ?,
|
* data: ?,
|
||||||
* loaded: boolean
|
* loaded: boolean
|
||||||
* }} OpenSeadragon.CacheRecord
|
* }} OpenSeadragon.CacheRecord
|
||||||
*/
|
*/
|
||||||
$.CacheRecord = class {
|
$.CacheRecord = class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._tiles = [];
|
this.revive();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
get data() {
|
||||||
return this._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() {
|
get type() {
|
||||||
return this._type;
|
return this._type;
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
/**
|
||||||
for (let tile of this._tiles) {
|
* Await ongoing process so that we get cache ready on callback.
|
||||||
tile._needsDraw = true;
|
* @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) {
|
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) {
|
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);
|
this._conversionJobQueue = this._conversionJobQueue || [];
|
||||||
return this._promise;
|
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);
|
this._convert(this._type, type);
|
||||||
}
|
}
|
||||||
return this._promise;
|
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
|
* Add tile dependency on this record
|
||||||
* @param tile
|
* @param tile
|
||||||
@ -113,6 +245,9 @@ $.CacheRecord = class {
|
|||||||
* @param type
|
* @param type
|
||||||
*/
|
*/
|
||||||
addTile(tile, data, type) {
|
addTile(tile, data, type) {
|
||||||
|
if (this._destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||||
|
|
||||||
//allow overriding the cache - existing tile or different type
|
//allow overriding the cache - existing tile or different type
|
||||||
@ -124,28 +259,28 @@ $.CacheRecord = class {
|
|||||||
this._promise = $.Promise.resolve(data);
|
this._promise = $.Promise.resolve(data);
|
||||||
this._data = data;
|
this._data = data;
|
||||||
this.loaded = true;
|
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);
|
this._tiles.push(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove tile dependency on this record.
|
* Remove tile dependency on this record.
|
||||||
* @param tile
|
* @param tile
|
||||||
|
* @returns {Boolean} true if record removed
|
||||||
*/
|
*/
|
||||||
removeTile(tile) {
|
removeTile(tile) {
|
||||||
|
if (this._destroyed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (let i = 0; i < this._tiles.length; i++) {
|
for (let i = 0; i < this._tiles.length; i++) {
|
||||||
if (this._tiles[i] === tile) {
|
if (this._tiles[i] === tile) {
|
||||||
this._tiles.splice(i, 1);
|
this._tiles.splice(i, 1);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,7 +288,57 @@ $.CacheRecord = class {
|
|||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
getTileCount() {
|
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) {
|
if (i >= stepCount) {
|
||||||
_this._data = x;
|
_this._data = x;
|
||||||
_this.loaded = true;
|
_this.loaded = true;
|
||||||
|
_this._checkAwaitsConvert();
|
||||||
return $.Promise.resolve(x);
|
return $.Promise.resolve(x);
|
||||||
}
|
}
|
||||||
let edge = conversionPath[i];
|
let edge = conversionPath[i];
|
||||||
@ -189,7 +375,7 @@ $.CacheRecord = class {
|
|||||||
return originalData;
|
return originalData;
|
||||||
}
|
}
|
||||||
//node.value holds the type string
|
//node.value holds the type string
|
||||||
convertor.destroy(edge.origin.value, x);
|
convertor.destroy(x, edge.origin.value);
|
||||||
return convert(y, i + 1);
|
return convert(y, i + 1);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -232,15 +418,23 @@ $.TileCache = class {
|
|||||||
return this._tilesLoaded.length;
|
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
|
* Caches the specified tile, removing an old tile if necessary to stay under the
|
||||||
* maxImageCacheCount specified on construction. Note that if multiple tiles reference
|
* 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 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
|
* 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.
|
* may temporarily surpass that number, but should eventually come back down to the max specified.
|
||||||
|
* @private
|
||||||
* @param {Object} options - Tile info.
|
* @param {Object} options - Tile info.
|
||||||
* @param {OpenSeadragon.Tile} options.tile - The tile to cache.
|
* @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.
|
* @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache.
|
||||||
* Used if cacheKey not set.
|
* Used if cacheKey not set.
|
||||||
* @param {Image} options.image - The image of the tile to cache. Deprecated.
|
* @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
|
* @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
|
* function will release an old tile. The cutoff option specifies a tile level at or below which
|
||||||
* tiles will not be released.
|
* tiles will not be released.
|
||||||
|
* @returns {OpenSeadragon.CacheRecord} - The cache record the tile was attached to.
|
||||||
*/
|
*/
|
||||||
cacheTile( options ) {
|
cacheTile( options ) {
|
||||||
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
||||||
$.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
|
const theTile = options.tile;
|
||||||
$.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
|
$.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,
|
let cutoff = options.cutoff || 0,
|
||||||
insertionIndex = this._tilesLoaded.length,
|
insertionIndex = this._tilesLoaded.length,
|
||||||
cacheKey = options.cacheKey || options.tile.cacheKey;
|
cacheKey = options.cacheKey || theTile.cacheKey;
|
||||||
|
|
||||||
let cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
|
let cacheRecord = this._cachesLoaded[cacheKey] || this._zombiesLoaded[cacheKey];
|
||||||
if (!cacheRecord) {
|
if (!cacheRecord) {
|
||||||
|
if (options.data === undefined) {
|
||||||
if (!options.data) {
|
|
||||||
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
|
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
|
||||||
"has been deprecated and will be removed in the future.");
|
"has been deprecated and will be removed in the future.");
|
||||||
options.data = options.image;
|
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();
|
cacheRecord = this._cachesLoaded[cacheKey] = new $.CacheRecord();
|
||||||
this._cachesLoadedCount++;
|
this._cachesLoadedCount++;
|
||||||
} else if (!cacheRecord.getTileCount()) {
|
} else if (cacheRecord._destroyed) {
|
||||||
//revive zombie
|
cacheRecord.revive();
|
||||||
delete this._zombiesLoaded[cacheKey];
|
delete this._zombiesLoaded[cacheKey];
|
||||||
this._zombiesLoadedCount--;
|
this._zombiesLoadedCount--;
|
||||||
}
|
}
|
||||||
@ -282,11 +479,12 @@ $.TileCache = class {
|
|||||||
"For easier use of the cache system, use the tile instance API.");
|
"For easier use of the cache system, use the tile instance API.");
|
||||||
options.dataType = $.convertor.guessType(options.data);
|
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
|
// 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.
|
// 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 ) {
|
if ( this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount ) {
|
||||||
//prefer zombie deletion, faster, better
|
//prefer zombie deletion, faster, better
|
||||||
if (this._zombiesLoadedCount > 0) {
|
if (this._zombiesLoadedCount > 0) {
|
||||||
@ -298,7 +496,6 @@ $.TileCache = class {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let worstTile = null;
|
let worstTile = null;
|
||||||
let worstTileIndex = -1;
|
|
||||||
let prevTile, worstTime, worstLevel, prevTime, prevLevel;
|
let prevTile, worstTime, worstLevel, prevTime, prevLevel;
|
||||||
|
|
||||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||||
@ -325,13 +522,20 @@ $.TileCache = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( worstTile && worstTileIndex >= 0 ) {
|
if ( worstTile && worstTileIndex >= 0 ) {
|
||||||
this._unloadTile(worstTile, true);
|
this.unloadTile(worstTile, true);
|
||||||
insertionIndex = worstTileIndex;
|
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;
|
let cacheOverflows = this._cachesLoadedCount + this._zombiesLoadedCount > this._maxCacheItemCount;
|
||||||
if (tiledImage._zombieCache && cacheOverflows && this._zombiesLoadedCount > 0) {
|
if (tiledImage._zombieCache && cacheOverflows && this._zombiesLoadedCount > 0) {
|
||||||
//prefer newer zombies
|
//prefer newer (fresh ;) zombies
|
||||||
for (let zombie in this._zombiesLoaded) {
|
for (let zombie in this._zombiesLoaded) {
|
||||||
this._zombiesLoaded[zombie].destroy();
|
this._zombiesLoaded[zombie].destroy();
|
||||||
delete this._zombiesLoaded[zombie];
|
delete this._zombiesLoaded[zombie];
|
||||||
@ -355,22 +559,60 @@ $.TileCache = class {
|
|||||||
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||||
tile = this._tilesLoaded[ 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) {
|
if (!tile.loaded) {
|
||||||
//iterates from the array end, safe to remove
|
//iterates from the array end, safe to remove
|
||||||
this._tilesLoaded.splice( i, 1 );
|
this._tilesLoaded.splice( i, 1 );
|
||||||
i--;
|
|
||||||
} else if ( tile.tiledImage === tiledImage ) {
|
} 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) {
|
getCacheRecord(cacheKey) {
|
||||||
$.console.assert(cacheKey, '[TileCache.getCacheRecord] cacheKey is required');
|
$.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
|
* @param deleteAtIndex index to remove the tile record at, will not remove from _tiledLoaded if not set
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_unloadTile(tile, destroy, deleteAtIndex) {
|
unloadTile(tile, destroy, deleteAtIndex) {
|
||||||
$.console.assert(tile, '[TileCache._unloadTile] tile is required');
|
$.console.assert(tile, '[TileCache.unloadTile] tile is required');
|
||||||
|
|
||||||
for (let key in tile._caches) {
|
for (let key in tile._caches) {
|
||||||
const cacheRecord = this._cachesLoaded[key];
|
//we are 'ok' to remove tile caches here since we later call destroy on tile, otherwise
|
||||||
if (cacheRecord) {
|
//tile has count of its cache size --> would be inconsistent
|
||||||
cacheRecord.removeTile(tile);
|
this.unloadCacheForTile(tile, key, destroy);
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
//delete also the tile record
|
//delete also the tile record
|
||||||
if (deleteAtIndex !== undefined) {
|
if (deleteAtIndex !== undefined) {
|
||||||
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
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;
|
const tiledImage = tile.tiledImage;
|
||||||
tile.unload();
|
tile.unload();
|
||||||
|
|
||||||
|
@ -83,6 +83,8 @@
|
|||||||
* Defaults to the setting in {@link OpenSeadragon.Options}.
|
* Defaults to the setting in {@link OpenSeadragon.Options}.
|
||||||
* @param {Object} [options.ajaxHeaders={}]
|
* @param {Object} [options.ajaxHeaders={}]
|
||||||
* A set of headers to include when making tile AJAX requests.
|
* 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 ) {
|
$.TiledImage = function( options ) {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
@ -184,7 +186,8 @@ $.TiledImage = function( options ) {
|
|||||||
preload: $.DEFAULT_SETTINGS.preload,
|
preload: $.DEFAULT_SETTINGS.preload,
|
||||||
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
|
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
|
||||||
subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency,
|
subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency,
|
||||||
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame
|
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame,
|
||||||
|
callTileLoadedWithCachedData: $.DEFAULT_SETTINGS.callTileLoadedWithCachedData
|
||||||
}, options );
|
}, options );
|
||||||
|
|
||||||
this._preload = this.preload;
|
this._preload = this.preload;
|
||||||
@ -1531,28 +1534,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!tile.loaded && !tile.loading) {
|
if (!tile.loaded && !tile.loading) {
|
||||||
// Tile was created or its data removed: check whether cache has the data before downloading.
|
// Tile was created or its data removed: check whether cache has the data.
|
||||||
if (!tile.cacheKey) {
|
// this method sets tile.loading=true if data available, which prevents
|
||||||
tile.cacheKey = "";
|
// job creation later on
|
||||||
tile.originalCacheKey = "";
|
this._tryFindTileCacheRecord(tile);
|
||||||
}
|
|
||||||
|
|
||||||
//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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( tile.loaded ) {
|
if ( tile.loaded ) {
|
||||||
@ -1577,6 +1562,45 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
return best;
|
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
|
* @private
|
||||||
* @inner
|
* @inner
|
||||||
@ -1779,8 +1803,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
* @param {?Number} cutoff
|
* @param {?Number} cutoff
|
||||||
* @param {?XMLHttpRequest} tileRequest
|
* @param {?XMLHttpRequest} tileRequest
|
||||||
* @param {?String} [dataType=undefined] data type, derived automatically if not set
|
* @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
|
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
|
||||||
// -> reason why it is not in the constructor
|
// -> reason why it is not in the constructor
|
||||||
tile.setCache(tile.cacheKey, data, dataType, false, cutoff);
|
tile.setCache(tile.cacheKey, data, dataType, false, cutoff);
|
||||||
@ -1811,7 +1836,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
resolver(tile);
|
resolver(tile);
|
||||||
} else if (cache.type !== requiredType) {
|
} else if (cache.type !== requiredType) {
|
||||||
//initiate conversion as soon as possible if incompatible with the drawer
|
//initiate conversion as soon as possible if incompatible with the drawer
|
||||||
cache.getData(requiredType).then(_ => {
|
cache.transformTo(requiredType).then(_ => {
|
||||||
tile.loading = false;
|
tile.loading = false;
|
||||||
tile.loaded = true;
|
tile.loaded = true;
|
||||||
resolver(tile);
|
resolver(tile);
|
||||||
@ -1821,12 +1846,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
tile.loaded = true;
|
tile.loaded = true;
|
||||||
resolver(tile);
|
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() {
|
function getCompletionCallback() {
|
||||||
@ -1839,6 +1858,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fallbackCompletion = getCompletionCallback();
|
const fallbackCompletion = getCompletionCallback();
|
||||||
|
if (!withEvent) {
|
||||||
|
fallbackCompletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when a tile has just been loaded in memory. That means that the
|
* Triggered when a tile has just been loaded in memory. That means that the
|
||||||
|
@ -915,7 +915,8 @@ $.TileSource.prototype = {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
getTileCacheData: function(cacheObject) {
|
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) {
|
getTileCacheDataAsImage: function(cacheObject) {
|
||||||
$.console.error("[TileSource.getTileCacheDataAsImage] has been deprecated. Use cache API of a tile instead.");
|
$.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) {
|
getTileCacheDataAsContext2D: function(cacheObject) {
|
||||||
$.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead.");
|
$.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,
|
loadTilesWithAjax: queueItem.options.loadTilesWithAjax,
|
||||||
ajaxHeaders: queueItem.options.ajaxHeaders,
|
ajaxHeaders: queueItem.options.ajaxHeaders,
|
||||||
debugMode: _this.debugMode,
|
debugMode: _this.debugMode,
|
||||||
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency
|
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency,
|
||||||
|
callTileLoadedWithCachedData: _this.callTileLoadedWithCachedData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_this.collectionMode) {
|
if (_this.collectionMode) {
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
options.minLevel = 0;
|
options.minLevel = 0;
|
||||||
options.maxLevel = options.gridSize.length - 1;
|
options.maxLevel = options.gridSize.length - 1;
|
||||||
|
|
||||||
OpenSeadragon.TileSource.apply(this, [options]);
|
$.TileSource.apply(this, [options]);
|
||||||
};
|
};
|
||||||
|
|
||||||
$.extend($.ZoomifyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ZoomifyTileSource.prototype */ {
|
$.extend($.ZoomifyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ZoomifyTileSource.prototype */ {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
/* global QUnit, testLog */
|
/* global QUnit, testLog */
|
||||||
|
|
||||||
(function() {
|
(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;
|
let viewer;
|
||||||
|
|
||||||
//we override jobs: remember original function
|
//we override jobs: remember original function
|
||||||
@ -15,6 +18,82 @@
|
|||||||
}, 20);
|
}, 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', {
|
QUnit.module('TileCache', {
|
||||||
beforeEach: function () {
|
beforeEach: function () {
|
||||||
@ -42,54 +121,38 @@
|
|||||||
// ----------
|
// ----------
|
||||||
// TODO: this used to be async
|
// TODO: this used to be async
|
||||||
QUnit.test('basics', function(assert) {
|
QUnit.test('basics', function(assert) {
|
||||||
var done = assert.async();
|
const done = assert.async();
|
||||||
var fakeViewer = {
|
const fakeViewer = {
|
||||||
raiseEvent: function() {}
|
raiseEvent: function() {}
|
||||||
};
|
};
|
||||||
var fakeTiledImage0 = {
|
const fakeTiledImage0 = {
|
||||||
viewer: fakeViewer,
|
viewer: fakeViewer,
|
||||||
source: OpenSeadragon.TileSource.prototype
|
source: OpenSeadragon.TileSource.prototype
|
||||||
};
|
};
|
||||||
var fakeTiledImage1 = {
|
const fakeTiledImage1 = {
|
||||||
viewer: fakeViewer,
|
viewer: fakeViewer,
|
||||||
source: OpenSeadragon.TileSource.prototype
|
source: OpenSeadragon.TileSource.prototype
|
||||||
};
|
};
|
||||||
|
|
||||||
var fakeTile0 = {
|
const tile0 = createFakeTile('foo.jpg', fakeTiledImage0);
|
||||||
url: 'foo.jpg',
|
const tile1 = createFakeTile('foo.jpg', fakeTiledImage1);
|
||||||
cacheKey: 'foo.jpg',
|
|
||||||
image: {},
|
|
||||||
loaded: true,
|
|
||||||
tiledImage: fakeTiledImage0,
|
|
||||||
_caches: [],
|
|
||||||
unload: function() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
var fakeTile1 = {
|
const cache = new OpenSeadragon.TileCache();
|
||||||
url: 'foo.jpg',
|
|
||||||
cacheKey: 'foo.jpg',
|
|
||||||
image: {},
|
|
||||||
loaded: true,
|
|
||||||
tiledImage: fakeTiledImage1,
|
|
||||||
_caches: [],
|
|
||||||
unload: function() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
var cache = new OpenSeadragon.TileCache();
|
|
||||||
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
||||||
|
|
||||||
cache.cacheTile({
|
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
||||||
tile: fakeTile0,
|
tile: tile0,
|
||||||
tiledImage: fakeTiledImage0
|
tiledImage: fakeTiledImage0
|
||||||
});
|
});
|
||||||
|
tile0._cacheSize++;
|
||||||
|
|
||||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after cache');
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after cache');
|
||||||
|
|
||||||
cache.cacheTile({
|
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
||||||
tile: fakeTile1,
|
tile: tile1,
|
||||||
tiledImage: fakeTiledImage1
|
tiledImage: fakeTiledImage1
|
||||||
});
|
});
|
||||||
|
tile1._cacheSize++;
|
||||||
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
|
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
|
||||||
|
|
||||||
cache.clearTilesFor(fakeTiledImage0);
|
cache.clearTilesFor(fakeTiledImage0);
|
||||||
@ -105,75 +168,369 @@
|
|||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
QUnit.test('maxImageCacheCount', function(assert) {
|
QUnit.test('maxImageCacheCount', function(assert) {
|
||||||
var done = assert.async();
|
const done = assert.async();
|
||||||
var fakeViewer = {
|
const fakeViewer = {
|
||||||
raiseEvent: function() {}
|
raiseEvent: function() {}
|
||||||
};
|
};
|
||||||
var fakeTiledImage0 = {
|
const fakeTiledImage0 = {
|
||||||
viewer: fakeViewer,
|
viewer: fakeViewer,
|
||||||
source: OpenSeadragon.TileSource.prototype
|
source: OpenSeadragon.TileSource.prototype
|
||||||
};
|
};
|
||||||
|
|
||||||
var fakeTile0 = {
|
const tile0 = createFakeTile('different.jpg', fakeTiledImage0);
|
||||||
url: 'different.jpg',
|
const tile1 = createFakeTile('same.jpg', fakeTiledImage0);
|
||||||
cacheKey: 'different.jpg',
|
const tile2 = createFakeTile('same.jpg', fakeTiledImage0);
|
||||||
image: {},
|
|
||||||
loaded: true,
|
|
||||||
tiledImage: fakeTiledImage0,
|
|
||||||
_caches: [],
|
|
||||||
unload: function() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
var fakeTile1 = {
|
const cache = new OpenSeadragon.TileCache({
|
||||||
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({
|
|
||||||
maxImageCacheCount: 1
|
maxImageCacheCount: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
||||||
|
|
||||||
cache.cacheTile({
|
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
||||||
tile: fakeTile0,
|
tile: tile0,
|
||||||
tiledImage: fakeTiledImage0
|
tiledImage: fakeTiledImage0
|
||||||
});
|
});
|
||||||
|
tile0._cacheSize++;
|
||||||
|
|
||||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add');
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add');
|
||||||
|
|
||||||
cache.cacheTile({
|
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
||||||
tile: fakeTile1,
|
tile: tile1,
|
||||||
tiledImage: fakeTiledImage0
|
tiledImage: fakeTiledImage0
|
||||||
});
|
});
|
||||||
|
tile1._cacheSize++;
|
||||||
|
|
||||||
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add of second image');
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add of second image');
|
||||||
|
|
||||||
cache.cacheTile({
|
tile2._caches[tile2.cacheKey] = cache.cacheTile({
|
||||||
tile: fakeTile2,
|
tile: tile2,
|
||||||
tiledImage: fakeTiledImage0
|
tiledImage: fakeTiledImage0
|
||||||
});
|
});
|
||||||
|
tile2._cacheSize++;
|
||||||
|
|
||||||
assert.equal(cache.numTilesLoaded(), 2, 'tile count after additional same image');
|
assert.equal(cache.numTilesLoaded(), 2, 'tile count after additional same image');
|
||||||
|
|
||||||
done();
|
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) {
|
QUnit.test('Zombie Cache', function(test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
||||||
// other tests will interfere
|
// 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,
|
let imageToCanvas = 0, srcToImage = 0, context2DtoImage = 0, canvasToContext2D = 0, imageToUrl = 0,
|
||||||
canvasToUrl = 0;
|
canvasToUrl = 0;
|
||||||
//set all same costs to get easy testing, know which path will be taken
|
//set all same costs to get easy testing, know which path will be taken
|
||||||
@ -81,6 +82,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QUnit.module('TypeConversion', {
|
QUnit.module('TypeConversion', {
|
||||||
beforeEach: function () {
|
beforeEach: function () {
|
||||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||||
@ -113,7 +115,7 @@
|
|||||||
|
|
||||||
test.ok(Convertor.getConversionPath("__TEST__url", "__TEST__image"),
|
test.ok(Convertor.getConversionPath("__TEST__url", "__TEST__image"),
|
||||||
"Type conversion ok between TEST types.");
|
"Type conversion ok between TEST types.");
|
||||||
test.ok(Convertor.getConversionPath("canvas", "context2d"),
|
test.ok(Convertor.getConversionPath("url", "context2d"),
|
||||||
"Type conversion ok between real types.");
|
"Type conversion ok between real types.");
|
||||||
|
|
||||||
test.equal(Convertor.getConversionPath("url", "__TEST__image"), undefined,
|
test.equal(Convertor.getConversionPath("url", "__TEST__image"), undefined,
|
||||||
@ -124,15 +126,61 @@
|
|||||||
done();
|
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();
|
const done = test.async();
|
||||||
|
|
||||||
//load image object: url -> image
|
//load image object: url -> image
|
||||||
Convertor.convert("/test/data/A.png", "__TEST__url", "__TEST__image").then(i => {
|
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(OpenSeadragon.type(i), "image", "Got image object after conversion.");
|
||||||
test.equal(srcToImage, 1, "Conversion happened.");
|
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(urlDestroy, 1, "Url destructor called.");
|
||||||
|
|
||||||
test.equal(imageDestroy, 0, "Image destructor not called.");
|
test.equal(imageDestroy, 0, "Image destructor not called.");
|
||||||
return Convertor.convert(i, "__TEST__image", "__TEST__canvas");
|
return Convertor.convert(i, "__TEST__image", "__TEST__canvas");
|
||||||
}).then(c => { //path image -> canvas
|
}).then(c => { //path image -> canvas
|
||||||
@ -140,7 +188,7 @@
|
|||||||
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
||||||
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
||||||
test.equal(urlDestroy, 1, "Url destructor not called.");
|
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");
|
return Convertor.convert(c, "__TEST__canvas", "__TEST__image");
|
||||||
}).then(i => { //path canvas, image: canvas -> url -> image
|
}).then(i => { //path canvas, image: canvas -> url -> image
|
||||||
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
|
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(imageToUrl, 0, "Conversion image->url did not happened.");
|
||||||
|
|
||||||
test.equal(urlDestroy, 2, "Url destructor called.");
|
test.equal(urlDestroy, 2, "Url destructor called.");
|
||||||
test.equal(imageDestroy, 1, "Image destructor not called.");
|
test.equal(imageDestroy, 0, "Image destructor not called.");
|
||||||
test.equal(canvasDestroy, 1, "Canvas destructor called.");
|
test.equal(canvasDestroy, 0, "Canvas destructor called.");
|
||||||
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
test.equal(contex2DDestroy, 0, "Image destructor not called.");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -170,19 +218,19 @@
|
|||||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
||||||
|
|
||||||
//load image object: url -> image
|
//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(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
|
||||||
test.equal(srcToImage, 1, "Conversion happened.");
|
test.equal(srcToImage, 1, "Conversion happened.");
|
||||||
test.equal(urlDestroy, 1, "Url destructor called.");
|
test.equal(urlDestroy, 1, "Url destructor called.");
|
||||||
test.equal(imageDestroy, 0, "Image destructor not called.");
|
test.equal(imageDestroy, 0, "Image destructor not called.");
|
||||||
return cache.getData("__TEST__canvas");
|
return cache.transformTo("__TEST__canvas");
|
||||||
}).then(_ => { //path image -> canvas
|
}).then(_ => { //path image -> canvas
|
||||||
test.equal(OpenSeadragon.type(cache.data), "canvas", "Got canvas object after conversion.");
|
test.equal(OpenSeadragon.type(cache.data), "canvas", "Got canvas object after conversion.");
|
||||||
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
|
||||||
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
|
||||||
test.equal(urlDestroy, 1, "Url destructor not called.");
|
test.equal(urlDestroy, 1, "Url destructor not called.");
|
||||||
test.equal(imageDestroy, 1, "Image destructor 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
|
}).then(_ => { //path canvas, image: canvas -> url -> image
|
||||||
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
|
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
|
||||||
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
|
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) {
|
QUnit.test('Deletion cache while being in the conversion process', function (test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
let conversionHappened = false;
|
let conversionHappened = false;
|
||||||
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", _ => {
|
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", value => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
conversionHappened = true;
|
conversionHappened = true;
|
||||||
resolve("some interesting data");
|
resolve("modified " + value);
|
||||||
}, 20);
|
}, 20);
|
||||||
});
|
});
|
||||||
}, 1, 1);
|
}, 1, 1);
|
||||||
@ -231,10 +377,10 @@
|
|||||||
|
|
||||||
const cache = new OpenSeadragon.CacheRecord();
|
const cache = new OpenSeadragon.CacheRecord();
|
||||||
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
|
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(conversionHappened, "Interrupted conversion finished.");
|
||||||
test.ok(cache.loaded, "Cache is loaded.");
|
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(cache.type, "__TEST__longConversionProcessForTesting", "Cache declares new type.");
|
||||||
test.equal(urlDestroy, 1, "Url was destroyed.");
|
test.equal(urlDestroy, 1, "Url was destroyed.");
|
||||||
|
|
||||||
@ -253,6 +399,7 @@
|
|||||||
test.ok(!conversionHappened, "We destroyed cache before conversion finished.");
|
test.ok(!conversionHappened, "We destroyed cache before conversion finished.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// TODO: The ultimate integration test:
|
// TODO: The ultimate integration test:
|
||||||
// three items: one plain image data
|
// three items: one plain image data
|
||||||
// one modified image data by two different plugins
|
// one modified image data by two different plugins
|
@ -33,7 +33,7 @@
|
|||||||
<script src="/test/modules/event-source.js"></script>
|
<script src="/test/modules/event-source.js"></script>
|
||||||
<script src="/test/modules/viewerretrieval.js"></script>
|
<script src="/test/modules/viewerretrieval.js"></script>
|
||||||
<script src="/test/modules/basic.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/strings.js"></script>
|
||||||
<script src="/test/modules/formats.js"></script>
|
<script src="/test/modules/formats.js"></script>
|
||||||
<script src="/test/modules/iiif.js"></script>
|
<script src="/test/modules/iiif.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user