Add base drawer options and fix docs. Implement 'simple internal cache' for drawer data, optional to use.

This commit is contained in:
Aiosa 2024-02-11 11:27:02 +01:00
parent cae6ec6bee
commit d91df0126b
11 changed files with 454 additions and 215 deletions

View File

@ -50,8 +50,6 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
constructor(options) { constructor(options) {
super(options); super(options);
this.declareSupportedDataFormats("context2d");
/** /**
* The HTML element (canvas) that this drawer uses for drawing * The HTML element (canvas) that this drawer uses for drawing
* @member {Element} canvas * @member {Element} canvas
@ -100,6 +98,10 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
return 'canvas'; return 'canvas';
} }
getSupportedDataFormats() {
return ["context2d"];
}
/** /**
* create the HTML element (e.g. canvas, div) that the image will be drawn into * create the HTML element (e.g. canvas, div) that the image will be drawn into
* @returns {Element} the canvas to draw into * @returns {Element} the canvas to draw into
@ -287,7 +289,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
// Note: Disabled on iOS devices per default as it causes a native crash // Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true; useSketch = true;
const context = tile.length && this.getCompatibleData(tile); const context = tile.length && this.getDataToDraw(tile);
if (context) { if (context) {
sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio); sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio);
} else { } else {
@ -572,7 +574,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
return; return;
} }
const rendered = this.getCompatibleData(tile); const rendered = this.getDataToDraw(tile);
if (!rendered) { if (!rendered) {
return; return;
} }

View File

@ -354,8 +354,8 @@ $.DataTypeConvertor = class {
} }
let edge = conversionPath[i]; let edge = conversionPath[i];
let y = edge.transform(tile, x); let y = edge.transform(tile, x);
if (!y) { if (y === undefined) {
$.console.warn(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting to %s)`, edge.target); $.console.error(`[OpenSeadragon.convertor.convert] data mid result undefined value (while converting using %s)`, edge);
return $.Promise.resolve(); return $.Promise.resolve();
} }
//node.value holds the type string //node.value holds the type string
@ -389,8 +389,8 @@ $.DataTypeConvertor = class {
/** /**
* Destroy the data item given. * Destroy the data item given.
* @param {string} type data type * @param {string} type data type
* @param {?} data * @param {any} data
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor, or undefined * @return {OpenSeadragon.Promise<any>|undefined} promise resolution with data passed from constructor, or undefined
* if not such conversion exists * if not such conversion exists
*/ */
destroy(data, type) { destroy(data, type) {

View File

@ -34,6 +34,13 @@
(function( $ ){ (function( $ ){
/**
* @typedef BaseDrawerOptions
* @memberOf OpenSeadragon
* @property {boolean} [detachedCache=false] specify whether the drawer should use
* detached (=internal) cache object in case it has to perform type conversion
*/
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
/** /**
* @class OpenSeadragon.DrawerBase * @class OpenSeadragon.DrawerBase
@ -54,7 +61,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
this.viewer = options.viewer; this.viewer = options.viewer;
this.viewport = options.viewport; this.viewport = options.viewport;
this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor; this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
this.options = options.options || {}; this.options = $.extend({}, this.defaultOptions, options.options);
this.container = $.getElement( options.element ); this.container = $.getElement( options.element );
@ -80,10 +87,24 @@ OpenSeadragon.DrawerBase = class DrawerBase{
this._checkInterfaceImplementation(); this._checkInterfaceImplementation();
} }
/**
* Retrieve default options for the current drawer.
* The base implementation provides default shared options.
* Overrides should enumerate all defaults or extend from this implementation.
* return $.extend({}, super.options, { ... custom drawer instance options ... });
* @returns {BaseDrawerOptions} common options
*/
get defaultOptions() {
return {
detachedCache: false
};
}
// protect the canvas member with a getter // protect the canvas member with a getter
get canvas(){ get canvas(){
return this._renderingTarget; return this._renderingTarget;
} }
get element(){ get element(){
$.console.error('Drawer.element is deprecated. Use Drawer.container instead.'); $.console.error('Drawer.element is deprecated. Use Drawer.container instead.');
return this.container; return this.container;
@ -98,24 +119,13 @@ OpenSeadragon.DrawerBase = class DrawerBase{
return undefined; return undefined;
} }
/**
* Define which data types are compatible for this drawer to work with.
* See default type list in OpenSeadragon.DataTypeConvertor
* @param formats
*/
declareSupportedDataFormats(...formats) {
this._formats = formats;
}
/** /**
* Retrieve data types * Retrieve data types
* @abstract
* @return {[string]} * @return {[string]}
*/ */
getSupportedDataFormats() { getSupportedDataFormats() {
if (!this._formats || this._formats.length < 1) { throw "Drawer.getSupportedDataFormats must define its supported rendering data types!";
$.console.error("A drawer must define its supported rendering data types using declareSupportedDataFormats!");
}
return this._formats;
} }
/** /**
@ -124,26 +134,15 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* value, the rendering _MUST NOT_ proceed. It should * value, the rendering _MUST NOT_ proceed. It should
* await next animation frames and check again for availability. * await next animation frames and check again for availability.
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile
* @return {any|null|false} null if cache not available
*/ */
getCompatibleData(tile) { getDataToDraw(tile) {
const cache = tile.getCache(tile.cacheKey); const cache = tile.getCache(tile.cacheKey);
if (!cache) { if (!cache) {
$.console.warn("Attempt to draw tile %s when not cached!", tile);
return null; return null;
} }
return cache.getDataForRendering(this.getSupportedDataFormats(), this.options.detachedCache);
const formats = this.getSupportedDataFormats();
if (!formats.includes(cache.type)) {
cache.transformTo(formats.length > 1 ? formats : formats[0]);
return false; // type is NOT compatible
}
// Cache in the process of loading, no-op
if (!cache.loaded) {
return false; // cache is NOT ready
}
// Ensured compatible
return cache.data;
} }
/** /**
@ -230,6 +229,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* *
*/ */
_checkInterfaceImplementation(){ _checkInterfaceImplementation(){
// TODO: is this necessary? why not throw just in the method itself?
if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){ if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){
throw(new Error("[drawer]._createDrawingElement must be implemented by child class")); throw(new Error("[drawer]._createDrawingElement must be implemented by child class"));
} }

View File

@ -51,8 +51,6 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){ constructor(options){
super(options); super(options);
this.declareSupportedDataFormats("image");
/** /**
* The HTML element (div) that this drawer uses for drawing * The HTML element (div) that this drawer uses for drawing
* @member {Element} canvas * @member {Element} canvas
@ -87,6 +85,10 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
return 'html'; return 'html';
} }
getSupportedDataFormats() {
return ["image"];
}
/** /**
* @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
*/ */
@ -212,7 +214,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
// content during animation of the container size. // content during animation of the container size.
if ( !tile.element ) { if ( !tile.element ) {
const image = this.getCompatibleData(tile); const image = this.getDataToDraw(tile);
if (!image) { if (!image) {
return; return;
} }

View File

@ -761,12 +761,16 @@
*/ */
/** /**
* @typedef {Object} DrawerOptions * @typedef {Object.<string, Object>} DrawerOptions - give the renderer options (both shared - BaseDrawerOptions, and custom).
* Supports arbitrary keys: you can register any drawer on the OpenSeadragon namespace, it will get automatically recognized
* and its getType() implementation will define what key to specify the options with.
* @memberof OpenSeadragon * @memberof OpenSeadragon
* @property {Object} webgl - options if the WebGLDrawer is used. No options are currently supported. * @property {BaseDrawerOptions} [webgl] - options if the WebGLDrawer is used.
* @property {Object} canvas - options if the CanvasDrawer is used. No options are currently supported. * @property {BaseDrawerOptions} [canvas] - options if the CanvasDrawer is used.
* @property {Object} html - options if the HTMLDrawer is used. No options are currently supported. * @property {BaseDrawerOptions} [html] - options if the HTMLDrawer is used.
* @property {Object} custom - options if a custom drawer is used. No options are currently supported. * @property {BaseDrawerOptions} [custom] - options if a custom drawer is used.
*
* //Note: if you want to add change options for target drawer change type to {BaseDrawerOptions & MyDrawerOpts}
*/ */
@ -2637,6 +2641,10 @@ function OpenSeadragon( options ){
* keys and booleans as values. * keys and booleans as values.
*/ */
setImageFormatsSupported: function(formats) { setImageFormatsSupported: function(formats) {
//TODO: how to deal with this within the data pipeline?
// $.console.warn("setImageFormatsSupported method is deprecated. You should check that" +
// " the system supports your TileSources by implementing corresponding data type convertors.");
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
$.extend(FILEFORMATS, formats); $.extend(FILEFORMATS, formats);
}, },

View File

@ -46,7 +46,7 @@
* this tile failed to load? ) * this tile failed to load? )
* @param {String|Function} url The URL of this tile's image or a function that returns a url. * @param {String|Function} url The URL of this tile's image or a function that returns a url.
* @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it * @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it
* * is provided directly by the tile source. Deprecated: use Tile::setCache(...) instead. * * is provided directly by the tile source. Deprecated: use Tile::addCache(...) instead.
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request . * @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
* @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable). * @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable).
* @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the * @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the
@ -143,24 +143,10 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
" in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used."); " in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used.");
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData); cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
} }
/**
* The unique main cache key for this tile. Created automatically this._cKey = cacheKey || "";
* from the given tiledImage.source.getTileHashKey(...) implementation. this._ocKey = cacheKey || "";
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
this.cacheKey = cacheKey;
/**
* By default equal to tile.cacheKey, marks a cache associated with this tile
* that holds the cache original data (it was loaded with). In case you
* change the tile data, the tile original data should be left with the cache
* 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'.
* This key is used in cache resolution: in case new tile data is requested, if
* this cache key exists in the cache it is loaded.
* @member {String} originalCacheKey
* @memberof OpenSeadragon.Tile#
*/
this.originalCacheKey = this.cacheKey;
/** /**
* Is this tile loaded? * Is this tile loaded?
* @member {Boolean} loaded * @member {Boolean} loaded
@ -304,6 +290,43 @@ $.Tile.prototype = {
return this.level + "/" + this.x + "_" + this.y; return this.level + "/" + this.x + "_" + this.y;
}, },
/**
* The unique main cache key for this tile. Created automatically
* from the given tiledImage.source.getTileHashKey(...) implementation.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
get cacheKey() {
return this._cKey;
},
set cacheKey(value) {
if (this._cKey !== value) {
let ref = this._caches[this._cKey];
if (ref) {
// make sure we free drawer internal cache
ref.destroyInternalCache();
}
this._cKey = value;
}
},
/**
* By default equal to tile.cacheKey, marks a cache associated with this tile
* that holds the cache original data (it was loaded with). In case you
* change the tile data, the tile original data should be left with the cache
* 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'.
* This key is used in cache resolution: in case new tile data is requested, if
* this cache key exists in the cache it is loaded.
* @member {String} originalCacheKey
* @memberof OpenSeadragon.Tile#
*/
set originalCacheKey(value) {
throw "Original Cache Key cannot be managed manually!";
},
get originalCacheKey() {
return this._ocKey;
},
/** /**
* The Image object for this tile. * The Image object for this tile.
* @member {Object} image * @member {Object} image
@ -405,21 +428,21 @@ $.Tile.prototype = {
* @deprecated * @deprecated
*/ */
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::addCache.");
const cache = this._caches[this.cacheKey]; const cache = this._caches[this.cacheKey];
if (!value) { if (!value) {
this.unsetCache(this.cacheKey); this.removeCache(this.cacheKey);
} else { } else {
const _this = this; const _this = this;
cache.await().then(x => _this.setCache(this.cacheKey, x, cache.type, false)); cache.await().then(x => _this.addCache(this.cacheKey, x, cache.type, false));
} }
}, },
/** /**
* Get the data to render for this tile * Get the data to render for this tile
* @param {string} type data type to require * @param {string} type data type to require
* @param {boolean?} [copy=true] whether to force copy retrieval * @param {boolean} [copy=true] 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: function(type, copy = true) { getData: function(type, copy = true) {
@ -439,10 +462,10 @@ $.Tile.prototype = {
/** /**
* Get the original data data for this tile * Get the original data data for this tile
* @param {string} type data type to require * @param {string} type data type to require
* @param {boolean?} [copy=this.loaded] whether to force copy retrieval * @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
*/ */
getOriginalData: function(type, copy = true) { getOriginalData: function(type, copy = false) {
if (!this.tiledImage) { if (!this.tiledImage) {
return null; //async can access outside its lifetime return null; //async can access outside its lifetime
} }
@ -457,12 +480,10 @@ $.Tile.prototype = {
}, },
/** /**
* Set cache data * Set main cache data
* @param {*} value * @param {*} value
* @param {?string} type data type to require * @param {?string} type data type to require
* @param {boolean} [preserveOriginalData=true] if true and cacheKey === originalCacheKey, * @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: function(value, type, preserveOriginalData = true) { setData: function(value, type, preserveOriginalData = true) {
if (!this.tiledImage) { if (!this.tiledImage) {
@ -473,8 +494,9 @@ $.Tile.prototype = {
//caches equality means we have only one cache: //caches equality means we have only one cache:
// change current pointer to a new cache and create it: new tiles will // change current pointer to a new cache and create it: new tiles will
// not arrive at this data, but at originalCacheKey state // not arrive at this data, but at originalCacheKey state
// todo setting cache key makes the notification trigger ensure we do not do unnecessary stuff
this.cacheKey = "mod://" + this.originalCacheKey; this.cacheKey = "mod://" + this.originalCacheKey;
return this.setCache(this.cacheKey, value, type)._promise; return this.addCache(this.cacheKey, value, type)._promise;
} }
//else overwrite cache //else overwrite cache
const cache = this.getCache(this.cacheKey); const cache = this.getCache(this.cacheKey);
@ -487,7 +509,7 @@ $.Tile.prototype = {
/** /**
* Read tile cache data object (CacheRecord) * Read tile cache data object (CacheRecord)
* @param {string?} [key=this.cacheKey] 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 = this.cacheKey) { getCache: function(key = this.cacheKey) {
@ -495,23 +517,25 @@ $.Tile.prototype = {
}, },
/** /**
* TODO: set cache might be misleading name since we do not update data,
* this should be either changed or method renamed...
* Set tile cache, possibly multiple with custom key * Set tile cache, possibly multiple with custom key
* @param {string} key cache key, must be unique (we recommend re-using this.cacheTile * @param {string} key cache key, must be unique (we recommend re-using this.cacheTile
* value and extend it with some another unique content, by default overrides the existing * value and extend it with some another unique content, by default overrides the existing
* main cache used for drawing, if not existing. * main cache used for drawing, if not existing.
* @param {*} data data to cache - this data will be sent to the TileSource API for refinement. * @param {*} data data to cache - this data will be IGNORED if cache already exists!
* @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
* @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to. * @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to.
*/ */
setCache: function(key, data, type = undefined, _safely = true) { addCache: function(key, data, type = undefined, _safely = true) {
if (!this.tiledImage) { if (!this.tiledImage) {
return null; //async can access outside its lifetime return null; //async can access outside its lifetime
} }
if (!type) { if (!type) {
if (!this.tiledImage.__typeWarningReported) { if (!this.tiledImage.__typeWarningReported) {
$.console.warn(this, "[Tile.setCache] called without type specification. " + $.console.warn(this, "[Tile.addCache] 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;
} }
@ -523,20 +547,17 @@ $.Tile.prototype = {
// Need to get the supported type for rendering out of the active drawer. // Need to get the supported type for rendering out of the active drawer.
const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats(); const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(type, supportedTypes); const conversion = $.convertor.getConversionPath(type, supportedTypes);
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" + $.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" +
"to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type); "to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type);
} }
if (!this.__cutoff) {
//todo consider caching this on a tiled image level..
this.__cutoff = this.tiledImage.source.getClosestLevel();
}
const cachedItem = 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: this.__cutoff, //todo consider caching this on a tiled image level
cutoff: this.__cutoff || this.tiledImage.source.getClosestLevel(),
}); });
const havingRecord = this._caches[key]; const havingRecord = this._caches[key];
if (havingRecord !== cachedItem) { if (havingRecord !== cachedItem) {
@ -561,12 +582,12 @@ $.Tile.prototype = {
* @param {string} key cache key, required * @param {string} key cache key, required
* @param {boolean} [freeIfUnused=true] set to false if zombie should be created * @param {boolean} [freeIfUnused=true] set to false if zombie should be created
*/ */
unsetCache: function(key, freeIfUnused = true) { removeCache: function(key, freeIfUnused = true) {
if (this.cacheKey === key) { if (this.cacheKey === key) {
if (this.cacheKey !== this.originalCacheKey) { if (this.cacheKey !== this.originalCacheKey) {
this.cacheKey = this.originalCacheKey; this.cacheKey = this.originalCacheKey;
} else { } else {
$.console.warn("[Tile.unsetCache] trying to remove the only cache that is used to draw the tile!"); $.console.warn("[Tile.removeCache] trying to remove the only cache that is used to draw the tile!");
} }
} }
if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused)) { if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused)) {
@ -637,7 +658,7 @@ $.Tile.prototype = {
this.imgElement = null; this.imgElement = null;
this.loaded = false; this.loaded = false;
this.loading = false; this.loading = false;
this.cacheKey = this.originalCacheKey; this._cKey = this._ocKey;
} }
}; };

View File

@ -34,9 +34,12 @@
(function( $ ){ (function( $ ){
const DRAWER_INTERNAL_CACHE = Symbol("DRAWER_INTERNAL_CACHE");
/** /**
* Cached Data Record, the cache object. * @class CacheRecord
* Keeps only latest object type required. * @memberof OpenSeadragon
* @classdesc Cached Data Record, the cache object. Keeps only latest object type required.
* *
* This class acts like the Maybe type: * This class acts like the Maybe type:
* - it has 'loaded' flag indicating whether the tile data is ready * - it has 'loaded' flag indicating whether the tile data is ready
@ -44,16 +47,6 @@
* *
* Furthermore, it has a 'getData' function that returns a promise resolving * Furthermore, it has a 'getData' function that returns a promise resolving
* with the value on the desired type passed to the function. * with the value on the desired type passed to the function.
*
* @typedef {{
* destroy: function,
* revive: function,
* save: function,
* getDataAs: function,
* transformTo: function,
* data: ?,
* loaded: boolean
* }} OpenSeadragon.CacheRecord
*/ */
$.CacheRecord = class { $.CacheRecord = class {
constructor() { constructor() {
@ -82,7 +75,7 @@
/** /**
* Await ongoing process so that we get cache ready on callback. * Await ongoing process so that we get cache ready on callback.
* @returns {null|*} * @returns {Promise<any>}
*/ */
await() { await() {
if (!this._promise) { //if not cache loaded, do not fail if (!this._promise) { //if not cache loaded, do not fail
@ -128,20 +121,24 @@
/** /**
* Access the cache record data indirectly. Preferred way of data access. Asynchronous. * Access the cache record data indirectly. Preferred way of data access. Asynchronous.
* @param {string?} [type=this.type] * @param {string} [type=this.type]
* @param {boolean?} [copy=true] if false and same type is retrieved as the cache 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 * 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, * 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 * @returns {OpenSeadragon.Promise<?>} desired data type in promise, undefined if the cache was destroyed
*/ */
getDataAs(type = this._type, copy = true) { getDataAs(type = this._type, copy = true) {
const referenceTile = this._tiles[0]; const referenceTile = this._tiles[0];
if (this.loaded && type === this._type) { if (this.loaded) {
if (type === this._type) {
return copy ? $.convertor.copy(referenceTile, this._data, type) : this._promise; return copy ? $.convertor.copy(referenceTile, this._data, type) : this._promise;
} }
return this._getDataAsUnsafe(referenceTile, this._data, type, copy);
}
return this._promise.then(data => this._getDataAsUnsafe(referenceTile, data, type, copy));
}
return this._promise.then(data => { _getDataAsUnsafe(referenceTile, data, type, copy) {
//might get destroyed in meanwhile //might get destroyed in meanwhile
if (this._destroyed) { if (this._destroyed) {
return undefined; return undefined;
@ -153,6 +150,75 @@
return $.convertor.copy(referenceTile, data, type); return $.convertor.copy(referenceTile, data, type);
} }
return data; return data;
}
/**
* @private
* Access of the data by drawers, synchronous function.
*
* When drawers access data, they can choose to access this data as internal copy
*
* @param {Array<string>} supportedTypes required data (or one of) type(s)
* @param {boolean} keepInternalCopy if true, the cache keeps internally the drawer data
* until 'setData' is called
* todo: keep internal copy is not configurable and always enforced -> set as option for osd?
* @returns {any|undefined} desired data if available, undefined if conversion must be done
*/
getDataForRendering(supportedTypes, keepInternalCopy = true) {
if (this.loaded && supportedTypes.includes(this.type)) {
return this.data;
}
let internalCache = this[DRAWER_INTERNAL_CACHE];
if (keepInternalCopy && !internalCache) {
this.prepareForRendering(supportedTypes, keepInternalCopy);
return undefined;
}
if (internalCache) {
internalCache.withTemporaryTileRef(this._tiles[0]);
} else {
internalCache = this;
}
// Cache in the process of loading, no-op
if (!internalCache.loaded) {
return undefined;
}
if (!supportedTypes.includes(internalCache.type)) {
internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0]);
return undefined; // type is NOT compatible
}
return internalCache.data;
}
/**
* @private
* @param supportedTypes
* @param keepInternalCopy
* @return {OpenSeadragon.Promise<OpenSeadragon.SimpleCacheRecord|OpenSeadragon.CacheRecord>}
*/
prepareForRendering(supportedTypes, keepInternalCopy = true) {
const referenceTile = this._tiles[0];
// if not internal copy and we have no data, bypass rendering
if (!this.loaded) {
return $.Promise.resolve(this);
}
// we can get here only if we want to render incompatible type
let internalCache = this[DRAWER_INTERNAL_CACHE] = new $.SimpleCacheRecord();
const conversionPath = $.convertor.getConversionPath(this.type, supportedTypes);
if (!conversionPath) {
$.console.error(`[getDataForRendering] Conversion conversion ${this.type} ---> ${supportedTypes} cannot be done!`);
return $.Promise.resolve(this);
}
internalCache.withTemporaryTileRef(referenceTile);
const selectedFormat = conversionPath[conversionPath.length - 1].target.value;
return $.convertor.convert(referenceTile, this.data, this.type, selectedFormat).then(data => {
internalCache.setDataAs(data, selectedFormat);
return internalCache;
}); });
} }
@ -161,7 +227,7 @@
* Does nothing if the type equals to the current type. Asynchronous. * Does nothing if the type equals to the current type. Asynchronous.
* @param {string|[string]} type if array provided, the system will * @param {string|[string]} type if array provided, the system will
* try to optimize for the best type to convert to. * try to optimize for the best type to convert to.
* @return {OpenSeadragon.Promise<?>|*} * @return {OpenSeadragon.Promise<?>}
*/ */
transformTo(type = this._type) { transformTo(type = this._type) {
if (!this.loaded || if (!this.loaded ||
@ -198,6 +264,18 @@
return this._promise; return this._promise;
} }
/**
* If cache ceases to be the primary one, free data
* @private
*/
destroyInternalCache() {
const internal = this[DRAWER_INTERNAL_CACHE];
if (internal) {
internal.destroy();
delete this[DRAWER_INTERNAL_CACHE];
}
}
/** /**
* Set initial state, prepare for usage. * Set initial state, prepare for usage.
* Must not be called on active cache, e.g. first call destroy(). * Must not be called on active cache, e.g. first call destroy().
@ -221,17 +299,19 @@
// make sure this gets destroyed even if loaded=false // make sure this gets destroyed even if loaded=false
if (this.loaded) { if (this.loaded) {
$.convertor.destroy(this._data, this._type); this._destroySelfUnsafe(this._data, this._type);
this._tiles = null;
this._data = null;
this._type = null;
this._promise = null;
} else { } else {
const oldType = this._type; const oldType = this._type;
this._promise.then(x => { this._promise.then(x => this._destroySelfUnsafe(x, oldType));
}
this.loaded = false;
}
_destroySelfUnsafe(data, type) {
// ensure old data destroyed // ensure old data destroyed
$.convertor.destroy(x, oldType); $.convertor.destroy(data, type);
//might get revived... this.destroyInternalCache();
// might've got revived in meanwhile if async ...
if (!this._destroyed) { if (!this._destroyed) {
return; return;
} }
@ -239,9 +319,6 @@
this._data = null; this._data = null;
this._type = null; this._type = null;
this._promise = null; this._promise = null;
});
}
this.loaded = false;
} }
/** /**
@ -342,6 +419,12 @@
this._type = type; this._type = type;
this._data = data; this._data = data;
this._promise = $.Promise.resolve(data); this._promise = $.Promise.resolve(data);
const internal = this[DRAWER_INTERNAL_CACHE];
if (internal) {
// TODO: if update will be greedy uncomment (see below)
//internal.withTemporaryTileRef(this._tiles[0]);
internal.setDataAs(data, type);
}
this._triggerNeedsDraw(); this._triggerNeedsDraw();
return this._promise; return this._promise;
} }
@ -350,6 +433,12 @@
this._type = type; this._type = type;
this._data = data; this._data = data;
this._promise = $.Promise.resolve(data); this._promise = $.Promise.resolve(data);
const internal = this[DRAWER_INTERNAL_CACHE];
if (internal) {
// TODO: if update will be greedy uncomment (see below)
//internal.withTemporaryTileRef(this._tiles[0]);
internal.setDataAs(data, type);
}
this._triggerNeedsDraw(); this._triggerNeedsDraw();
return x; return x;
}); });
@ -366,7 +455,7 @@
referenceTile = this._tiles[0], referenceTile = this._tiles[0],
conversionPath = convertor.getConversionPath(from, to); conversionPath = convertor.getConversionPath(from, to);
if (!conversionPath) { if (!conversionPath) {
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`); $.console.error(`[CacheRecord._convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
return; //no-op return; //no-op
} }
@ -381,21 +470,14 @@
return $.Promise.resolve(x); return $.Promise.resolve(x);
} }
let edge = conversionPath[i]; let edge = conversionPath[i];
return $.Promise.resolve(edge.transform(referenceTile, x)).then( let y = edge.transform(referenceTile, x);
y => { if (y === undefined) {
if (!y) { _this.loaded = false;
$.console.error(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting using %s)`, edge); throw `[CacheRecord._convert] data mid result undefined value (while converting using ${edge}})`;
//try to recover using original data, but it returns inconsistent type (the log be hopefully enough)
_this._data = from;
_this._type = from;
_this.loaded = true;
return originalData;
} }
//node.value holds the type string
convertor.destroy(x, edge.origin.value); convertor.destroy(x, edge.origin.value);
return convert(y, i + 1); const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
} return result.then(res => convert(res, i + 1));
);
}; };
this.loaded = false; this.loaded = false;
@ -406,6 +488,129 @@
} }
}; };
/**
* @class SimpleCacheRecord
* @memberof OpenSeadragon
* @classdesc Simple cache record without robust support for async access. Meant for internal use only.
*
* This class acts like the Maybe type:
* - it has 'loaded' flag indicating whether the tile data is ready
* - it has 'data' property that has value if loaded=true
*
* This class supposes synchronous access, no collision of transform calls.
* It also does not record tiles nor allows cache/tile sharing.
* @private
*/
$.SimpleCacheRecord = class {
constructor(preferredTypes) {
this._data = null;
this._type = null;
this.loaded = false;
this.format = Array.isArray(preferredTypes) ? preferredTypes : null;
}
/**
* Sync access to the data
* @returns {any}
*/
get data() {
return this._data;
}
/**
* Sync access to the current type
* @returns {string}
*/
get type() {
return this._type;
}
/**
* Must be called before transformTo or setDataAs. To keep
* compatible api with CacheRecord where tile refs are known.
* @param {OpenSeadragon.Tile} referenceTile reference tile for conversion
*/
withTemporaryTileRef(referenceTile) {
this._temporaryTileRef = referenceTile;
}
/**
* Transform cache to desired type and get the data after conversion.
* Does nothing if the type equals to the current type. Asynchronous.
* @param {string|[string]} type if array provided, the system will
* try to optimize for the best type to convert to.
* @returns {OpenSeadragon.Promise<?>}
*/
transformTo(type) {
$.console.assert(this._temporaryTileRef, "SimpleCacheRecord needs tile reference set before update operation!");
const convertor = $.convertor,
conversionPath = convertor.getConversionPath(this._type, type);
if (!conversionPath) {
$.console.error(`[SimpleCacheRecord.transformTo] Conversion conversion ${this._type} ---> ${type} cannot be done!`);
return $.Promise.resolve(); //no-op
}
const stepCount = conversionPath.length,
_this = this,
convert = (x, i) => {
if (i >= stepCount) {
_this._data = x;
_this.loaded = true;
_this._temporaryTileRef = null;
return $.Promise.resolve(x);
}
let edge = conversionPath[i];
try {
// no test for y - less robust approach
let y = edge.transform(this._temporaryTileRef, x);
convertor.destroy(x, edge.origin.value);
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
return result.then(res => convert(res, i + 1));
} catch (e) {
_this.loaded = false;
_this._temporaryTileRef = null;
throw e;
}
};
this.loaded = false;
// Read target type from the conversion path: [edge.target] = Vertex, its value=type
this._type = conversionPath[stepCount - 1].target.value;
const promise = convert(this._data, 0);
this._data = undefined;
return promise;
}
/**
* Free all the data and call data destructors if defined.
*/
destroy() {
$.convertor.destroy(this._data, this._type);
this._data = null;
this._type = null;
}
/**
* Safely overwrite the cache data and return the old data
* @private
*/
setDataAs(data, type) {
// no check for state, users must ensure compatibility manually
$.convertor.destroy(this._data, this._data);
this._type = type;
this._data = data;
this.loaded = true;
// TODO: if done greedily, we transform each plugin set call
// pros: we can show midresults
// cons: unecessary work
// might be solved by introducing explicit tile update pipeline (already attemps)
// --> flag that knows which update is last
// if (this.format && !this.format.includes(type)) {
// this.transformTo(this.format);
// }
}
};
/** /**
* @class TileCache * @class TileCache
* @memberof OpenSeadragon * @memberof OpenSeadragon

View File

@ -1885,34 +1885,27 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile
*/ */
_tryFindTileCacheRecord: function(tile) { _tryFindTileCacheRecord: function(tile) {
if (!tile.cacheKey) { if (tile.cacheKey !== tile.originalCacheKey) {
tile.cacheKey = ""; //we found original data: this data will be used to re-execute the pipeline
tile.originalCacheKey = ""; let record = this._tileCache.getCacheRecord(tile.originalCacheKey);
if (record) {
tile.loading = true;
tile.loaded = false;
this._setTileLoaded(tile, record.data, null, null, record.type);
return true;
}
} }
let record = this._tileCache.getCacheRecord(tile.cacheKey); let record = this._tileCache.getCacheRecord(tile.cacheKey);
if (record) { if (record) {
// setup without calling tile loaded event! tile cache is ready for usage, // setup without calling tile loaded event! tile cache is ready for usage,
tile.loading = true; tile.loading = true;
tile.loaded = false; tile.loaded = false;
//set data as null, cache already has data, it does not overwrite // we could send null as data (cache not re-created), but deprecated events access the data
this._setTileLoaded(tile, null, null, null, record.type, this._setTileLoaded(tile, record.data, null, null, record.type,
this.callTileLoadedWithCachedData); this.callTileLoadedWithCachedData);
return true; 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, null, null, record.type);
return true;
}
}
return false; return false;
}, },
@ -2103,8 +2096,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/ */
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType, withEvent = true) { _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 // does nothing if tile.cacheKey already present
tile.setCache(tile.cacheKey, data, dataType, false); tile.addCache(tile.cacheKey, data, dataType, false);
let resolver = null, let resolver = null,
increment = 0, increment = 0,
@ -2127,11 +2120,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
const cache = tile.getCache(tile.cacheKey), const cache = tile.getCache(tile.cacheKey),
requiredTypes = _this.viewer.drawer.getSupportedDataFormats(); requiredTypes = _this.viewer.drawer.getSupportedDataFormats();
if (!cache) { if (!cache) {
$.console.warn("Tile %s not cached at the end of tile-loaded event: tile will not be drawn - it has no data!", tile); $.console.warn("Tile %s not cached or not loaded at the end of tile-loaded event: tile will not be drawn - it has no data!", tile);
resolver(tile); resolver(tile);
} else if (!requiredTypes.includes(cache.type)) { } else if (!requiredTypes.includes(cache.type)) {
//initiate conversion as soon as possible if incompatible with the drawer //initiate conversion as soon as possible if incompatible with the drawer
cache.transformTo(requiredTypes).then(_ => { cache.prepareForRendering(requiredTypes).then(cacheRef => {
if (!cacheRef) {
return cache.transformTo(requiredTypes);
}
return cacheRef;
}).then(_ => {
tile.loading = false; tile.loading = false;
tile.loaded = true; tile.loaded = true;
resolver(tile); resolver(tile);

View File

@ -99,10 +99,21 @@
// Unique type per drawer: uploads texture to unique webgl context. // Unique type per drawer: uploads texture to unique webgl context.
this._dataType = `${Date.now()}_TEX_2D`; this._dataType = `${Date.now()}_TEX_2D`;
this._supportedFormats = [];
this._setupTextureHandlers(this._dataType); this._setupTextureHandlers(this._dataType);
this.context = this._outputContext; // API required by tests this.context = this._outputContext; // API required by tests
}
get defaultOptions() {
return {
// use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref)
detachedCache: true
};
}
getSupportedDataFormats() {
return this._supportedFormats;
} }
// Public API required by all Drawer implementations // Public API required by all Drawer implementations
@ -315,7 +326,7 @@
); );
return; return;
} }
const textureInfo = this.getCompatibleData(tile); const textureInfo = this.getDataToDraw(tile, true);
if (!textureInfo) { if (!textureInfo) {
return; return;
} }
@ -830,8 +841,7 @@
// TextureInfo stored in the cache // TextureInfo stored in the cache
return { return {
texture: texture, texture: texture,
position: position, position: position
cpuData: data,
}; };
}; };
const tex2DCompatibleDestructor = textureInfo => { const tex2DCompatibleDestructor = textureInfo => {
@ -839,22 +849,16 @@
this._gl.deleteTexture(textureInfo.texture); this._gl.deleteTexture(textureInfo.texture);
} }
}; };
const dataRetrieval = (tile, data) => {
return data.cpuData;
};
// Differentiate type also based on type used to upload data: we can support bidirectional conversion. // Differentiate type also based on type used to upload data: we can support bidirectional conversion.
const c2dTexType = thisType + ":context2d", const c2dTexType = thisType + ":context2d",
imageTexType = thisType + ":image"; imageTexType = thisType + ":image";
this._supportedFormats.push(c2dTexType, imageTexType);
this.declareSupportedDataFormats(imageTexType, c2dTexType);
// We should be OK uploading any of these types. The complexity is selected to be O(3n), should be // We should be OK uploading any of these types. The complexity is selected to be O(3n), should be
// more than linear pass over pixels // more than linear pass over pixels
$.convertor.learn("context2d", c2dTexType, tex2DCompatibleLoader, 1, 3); $.convertor.learn("context2d", c2dTexType, (t, d) => tex2DCompatibleLoader(t, d.canvas), 1, 3);
$.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3); $.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3);
$.convertor.learn(c2dTexType, "context2d", dataRetrieval, 1, 3);
$.convertor.learn(imageTexType, "image", dataRetrieval, 1, 3);
$.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor); $.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor);
$.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor); $.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor);

View File

@ -83,14 +83,13 @@
if (processors.length === 0) { if (processors.length === 0) {
//restore the original data //restore the original data
const context = await tile.getOriginalData('context2d', const context = await tile.getOriginalData('context2d', false);
false);
tile.setData(context, 'context2d'); tile.setData(context, 'context2d');
tile._filterIncrement = self.filterIncrement; tile._filterIncrement = self.filterIncrement;
return; return;
} }
const contextCopy = await tile.getOriginalData('context2d'); const contextCopy = await tile.getOriginalData('context2d', true);
const currentIncrement = self.filterIncrement; const currentIncrement = self.filterIncrement;
for (let i = 0; i < processors.length; i++) { for (let i = 0; i < processors.length; i++) {
if (self.filterIncrement !== currentIncrement) { if (self.filterIncrement !== currentIncrement) {

View File

@ -252,15 +252,15 @@
//load data //load data
const tile00 = createFakeTile('foo.jpg', fakeTiledImage0); const tile00 = createFakeTile('foo.jpg', fakeTiledImage0);
tile00.setCache(tile00.cacheKey, 0, T_A, false); tile00.addCache(tile00.cacheKey, 0, T_A, false);
const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0); const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0);
tile01.setCache(tile01.cacheKey, 0, T_B, false); tile01.addCache(tile01.cacheKey, 0, T_B, false);
const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1); const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1);
tile10.setCache(tile10.cacheKey, 0, T_C, false); tile10.addCache(tile10.cacheKey, 0, T_C, false);
const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1); const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1);
tile11.setCache(tile11.cacheKey, 0, T_C, false); tile11.addCache(tile11.cacheKey, 0, T_C, false);
const tile12 = createFakeTile('foo.jpg', fakeTiledImage1); const tile12 = createFakeTile('foo.jpg', fakeTiledImage1);
tile12.setCache(tile12.cacheKey, 0, T_A, false); tile12.addCache(tile12.cacheKey, 0, T_A, false);
const collideGetSet = async (tile, type) => { const collideGetSet = async (tile, type) => {
const value = await tile.getData(type, false); const value = await tile.getData(type, false);
@ -446,15 +446,15 @@
//load data //load data
const tile00 = createFakeTile('foo.jpg', fakeTiledImage0); const tile00 = createFakeTile('foo.jpg', fakeTiledImage0);
tile00.setCache(tile00.cacheKey, 0, T_A, false); tile00.addCache(tile00.cacheKey, 0, T_A, false);
const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0); const tile01 = createFakeTile('foo2.jpg', fakeTiledImage0);
tile01.setCache(tile01.cacheKey, 0, T_B, false); tile01.addCache(tile01.cacheKey, 0, T_B, false);
const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1); const tile10 = createFakeTile('foo3.jpg', fakeTiledImage1);
tile10.setCache(tile10.cacheKey, 0, T_C, false); tile10.addCache(tile10.cacheKey, 0, T_C, false);
const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1); const tile11 = createFakeTile('foo3.jpg', fakeTiledImage1);
tile11.setCache(tile11.cacheKey, 0, T_C, false); tile11.addCache(tile11.cacheKey, 0, T_C, false);
const tile12 = createFakeTile('foo.jpg', fakeTiledImage1); const tile12 = createFakeTile('foo.jpg', fakeTiledImage1);
tile12.setCache(tile12.cacheKey, 0, T_A, false); tile12.addCache(tile12.cacheKey, 0, T_A, false);
//test set/get data in async env //test set/get data in async env
(async function() { (async function() {
@ -471,7 +471,7 @@
test.equal(theTileKey, tile00.originalCacheKey, "Original cache key preserved."); test.equal(theTileKey, tile00.originalCacheKey, "Original cache key preserved.");
//now add artifically another record //now add artifically another record
tile00.setCache("my_custom_cache", 128, T_C); tile00.addCache("my_custom_cache", 128, T_C);
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles."); test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items."); 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(c00.getTileCount(), 2, "The cache still has only two tiles attached.");
@ -483,32 +483,32 @@
test.equal(tile12.getCacheSize(), 1, "Related tile cache did not increase."); test.equal(tile12.getCacheSize(), 1, "Related tile cache did not increase.");
//add and delete cache nothing changes //add and delete cache nothing changes
tile00.setCache("my_custom_cache2", 128, T_C); tile00.addCache("my_custom_cache2", 128, T_C);
tile00.unsetCache("my_custom_cache2"); tile00.removeCache("my_custom_cache2");
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles."); test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items."); test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items.");
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects."); test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
//delete cache as a zombie //delete cache as a zombie
tile00.setCache("my_custom_cache2", 17, T_C); tile00.addCache("my_custom_cache2", 17, T_C);
//direct access shoes correct value although we set key! //direct access shoes correct value although we set key!
const myCustomCache2Data = tile00.getCache("my_custom_cache2").data; const myCustomCache2Data = tile00.getCache("my_custom_cache2").data;
test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene."); test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene.");
test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6."); test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6.");
//keep zombie //keep zombie
tile00.unsetCache("my_custom_cache2", false); tile00.removeCache("my_custom_cache2", false);
test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change."); test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects."); test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
//revive zombie //revive zombie
tile01.setCache("my_custom_cache2", 18, T_C); tile01.addCache("my_custom_cache2", 18, T_C);
const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data; const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data;
test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived."); test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived.");
//again, keep zombie //again, keep zombie
tile01.unsetCache("my_custom_cache2", false); tile01.removeCache("my_custom_cache2", false);
//first create additional cache so zombie is not the youngest //first create additional cache so zombie is not the youngest
tile01.setCache("some weird cache", 11, T_A); tile01.addCache("some weird cache", 11, T_A);
test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys."); 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 //insertion aadditional cache clears the zombie first although it is not the youngest one
@ -528,12 +528,12 @@
test.equal(tile12.getCache().data, 42, "The value is not 43 as setData triggers cache share!"); 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' //triggers insertion - deletion of zombie cache 'my_custom_cache2'
tile00.setCache("trigger-max-cache-handler", 5, T_C); tile00.addCache("trigger-max-cache-handler", 5, T_C);
//reset CAP //reset CAP
tileCache._maxCacheItemCount = OpenSeadragon.DEFAULT_SETTINGS.maxImageCacheCount; tileCache._maxCacheItemCount = OpenSeadragon.DEFAULT_SETTINGS.maxImageCacheCount;
//try to revive zombie will fail: the zombie was deleted, we will find 18 //try to revive zombie will fail: the zombie was deleted, we will find 18
tile01.setCache("my_custom_cache2", 18, T_C); tile01.addCache("my_custom_cache2", 18, T_C);
const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data; const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because created."); test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because created.");
test.equal(myCustomCache2RecreatedData, 18, "Cache data is actually as set to 18."); test.equal(myCustomCache2RecreatedData, 18, "Cache data is actually as set to 18.");