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

@ -47,11 +47,9 @@
*/
class CanvasDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
constructor(options) {
super(options);
this.declareSupportedDataFormats("context2d");
/**
* The HTML element (canvas) that this drawer uses for drawing
* @member {Element} canvas
@ -71,7 +69,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
* @memberof OpenSeadragon.CanvasDrawer#
* @private
*/
this.context = this.canvas.getContext( '2d' );
this.context = this.canvas.getContext('2d');
// Sketch canvas used to temporarily draw tiles which cannot be drawn directly
// to the main canvas due to opacity. Lazily initialized.
@ -100,6 +98,10 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
return 'canvas';
}
getSupportedDataFormats() {
return ["context2d"];
}
/**
* create the HTML element (e.g. canvas, div) that the image will be drawn 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
useSketch = true;
const context = tile.length && this.getCompatibleData(tile);
const context = tile.length && this.getDataToDraw(tile);
if (context) {
sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio);
} else {
@ -572,7 +574,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
return;
}
const rendered = this.getCompatibleData(tile);
const rendered = this.getDataToDraw(tile);
if (!rendered) {
return;
}

View File

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

View File

@ -34,7 +34,14 @@
(function( $ ){
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
/**
* @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
/**
* @class OpenSeadragon.DrawerBase
* @classdesc Base class for Drawers that handle rendering of tiles for an {@link OpenSeadragon.Viewer}.
@ -54,7 +61,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
this.viewer = options.viewer;
this.viewport = options.viewport;
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 );
@ -80,10 +87,24 @@ OpenSeadragon.DrawerBase = class DrawerBase{
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
get canvas(){
return this._renderingTarget;
}
get element(){
$.console.error('Drawer.element is deprecated. Use Drawer.container instead.');
return this.container;
@ -98,24 +119,13 @@ OpenSeadragon.DrawerBase = class DrawerBase{
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
* @abstract
* @return {[string]}
*/
getSupportedDataFormats() {
if (!this._formats || this._formats.length < 1) {
$.console.error("A drawer must define its supported rendering data types using declareSupportedDataFormats!");
}
return this._formats;
throw "Drawer.getSupportedDataFormats must define its supported rendering data types!";
}
/**
@ -124,26 +134,15 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* value, the rendering _MUST NOT_ proceed. It should
* await next animation frames and check again for availability.
* @param {OpenSeadragon.Tile} tile
* @return {any|null|false} null if cache not available
*/
getCompatibleData(tile) {
getDataToDraw(tile) {
const cache = tile.getCache(tile.cacheKey);
if (!cache) {
$.console.warn("Attempt to draw tile %s when not cached!", tile);
return null;
}
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;
return cache.getDataForRendering(this.getSupportedDataFormats(), this.options.detachedCache);
}
/**
@ -230,6 +229,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
*
*/
_checkInterfaceImplementation(){
// TODO: is this necessary? why not throw just in the method itself?
if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){
throw(new Error("[drawer]._createDrawingElement must be implemented by child class"));
}

View File

@ -51,8 +51,6 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
super(options);
this.declareSupportedDataFormats("image");
/**
* The HTML element (div) that this drawer uses for drawing
* @member {Element} canvas
@ -87,6 +85,10 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
return 'html';
}
getSupportedDataFormats() {
return ["image"];
}
/**
* @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.
if ( !tile.element ) {
const image = this.getCompatibleData(tile);
const image = this.getDataToDraw(tile);
if (!image) {
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
* @property {Object} webgl - options if the WebGLDrawer is used. No options are currently supported.
* @property {Object} canvas - options if the CanvasDrawer is used. No options are currently supported.
* @property {Object} html - options if the HTMLDrawer is used. No options are currently supported.
* @property {Object} custom - options if a custom drawer is used. No options are currently supported.
* @property {BaseDrawerOptions} [webgl] - options if the WebGLDrawer is used.
* @property {BaseDrawerOptions} [canvas] - options if the CanvasDrawer is used.
* @property {BaseDrawerOptions} [html] - options if the HTMLDrawer is used.
* @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.
*/
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
$.extend(FILEFORMATS, formats);
},

View File

@ -46,7 +46,7 @@
* this tile failed to load? )
* @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
* * 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 {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
@ -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.");
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
}
/**
* The unique main cache key for this tile. Created automatically
* from the given tiledImage.source.getTileHashKey(...) implementation.
* @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;
this._cKey = cacheKey || "";
this._ocKey = cacheKey || "";
/**
* Is this tile loaded?
* @member {Boolean} loaded
@ -304,6 +290,43 @@ $.Tile.prototype = {
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.
* @member {Object} image
@ -405,21 +428,21 @@ $.Tile.prototype = {
* @deprecated
*/
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];
if (!value) {
this.unsetCache(this.cacheKey);
this.removeCache(this.cacheKey);
} else {
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
* @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
*/
getData: function(type, copy = true) {
@ -439,10 +462,10 @@ $.Tile.prototype = {
/**
* Get the original data data for this tile
* @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
*/
getOriginalData: function(type, copy = true) {
getOriginalData: function(type, copy = false) {
if (!this.tiledImage) {
return null; //async can access outside its lifetime
}
@ -457,12 +480,10 @@ $.Tile.prototype = {
},
/**
* Set cache data
* Set main cache data
* @param {*} value
* @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: function(value, type, preserveOriginalData = true) {
if (!this.tiledImage) {
@ -473,8 +494,9 @@ $.Tile.prototype = {
//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
// todo setting cache key makes the notification trigger ensure we do not do unnecessary stuff
this.cacheKey = "mod://" + this.originalCacheKey;
return this.setCache(this.cacheKey, value, type)._promise;
return this.addCache(this.cacheKey, value, type)._promise;
}
//else overwrite cache
const cache = this.getCache(this.cacheKey);
@ -487,7 +509,7 @@ $.Tile.prototype = {
/**
* 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}
*/
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
* @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
* 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 [_safely=true] private
* @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) {
return null; //async can access outside its lifetime
}
if (!type) {
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.");
this.tiledImage.__typeWarningReported = true;
}
@ -523,20 +547,17 @@ $.Tile.prototype = {
// Need to get the supported type for rendering out of the active drawer.
const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats();
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);
}
if (!this.__cutoff) {
//todo consider caching this on a tiled image level..
this.__cutoff = this.tiledImage.source.getClosestLevel();
}
const cachedItem = this.tiledImage._tileCache.cacheTile({
data: data,
dataType: type,
tile: this,
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];
if (havingRecord !== cachedItem) {
@ -561,12 +582,12 @@ $.Tile.prototype = {
* @param {string} key cache key, required
* @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 !== this.originalCacheKey) {
this.cacheKey = this.originalCacheKey;
} 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)) {
@ -637,7 +658,7 @@ $.Tile.prototype = {
this.imgElement = null;
this.loaded = false;
this.loading = false;
this.cacheKey = this.originalCacheKey;
this._cKey = this._ocKey;
}
};

View File

@ -34,9 +34,12 @@
(function( $ ){
const DRAWER_INTERNAL_CACHE = Symbol("DRAWER_INTERNAL_CACHE");
/**
* Cached Data Record, the cache object.
* Keeps only latest object type required.
* @class CacheRecord
* @memberof OpenSeadragon
* @classdesc Cached Data Record, the cache object. Keeps only latest object type required.
*
* This class acts like the Maybe type:
* - 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
* 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 {
constructor() {
@ -82,7 +75,7 @@
/**
* Await ongoing process so that we get cache ready on callback.
* @returns {null|*}
* @returns {Promise<any>}
*/
await() {
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.
* @param {string?} [type=this.type]
* @param {boolean?} [copy=true] if false and same type is retrieved as the cache type,
* @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).
* introduce race conditions (you get a cache data direct reference you modify).
* @returns {OpenSeadragon.Promise<?>} desired data type in promise, undefined if the cache was destroyed
*/
getDataAs(type = this._type, copy = true) {
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 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
if (this._destroyed) {
return undefined;
@ -153,6 +150,75 @@
return $.convertor.copy(referenceTile, data, type);
}
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.
* @param {string|[string]} type if array provided, the system will
* try to optimize for the best type to convert to.
* @return {OpenSeadragon.Promise<?>|*}
* @return {OpenSeadragon.Promise<?>}
*/
transformTo(type = this._type) {
if (!this.loaded ||
@ -198,6 +264,18 @@
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.
* Must not be called on active cache, e.g. first call destroy().
@ -219,19 +297,21 @@
delete this._conversionJobQueue;
this._destroyed = true;
//make sure this gets destroyed even if loaded=false
// 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;
this._destroySelfUnsafe(this._data, this._type);
} else {
const oldType = this._type;
this._promise.then(x => {
//ensure old data destroyed
$.convertor.destroy(x, oldType);
//might get revived...
this._promise.then(x => this._destroySelfUnsafe(x, oldType));
}
this.loaded = false;
}
_destroySelfUnsafe(data, type) {
// ensure old data destroyed
$.convertor.destroy(data, type);
this.destroyInternalCache();
// might've got revived in meanwhile if async ...
if (!this._destroyed) {
return;
}
@ -239,9 +319,6 @@
this._data = null;
this._type = null;
this._promise = null;
});
}
this.loaded = false;
}
/**
@ -342,6 +419,12 @@
this._type = type;
this._data = 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();
return this._promise;
}
@ -350,6 +433,12 @@
this._type = type;
this._data = 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();
return x;
});
@ -366,7 +455,7 @@
referenceTile = this._tiles[0],
conversionPath = convertor.getConversionPath(from, to);
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
}
@ -381,21 +470,14 @@
return $.Promise.resolve(x);
}
let edge = conversionPath[i];
return $.Promise.resolve(edge.transform(referenceTile, x)).then(
y => {
if (!y) {
$.console.error(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting using %s)`, 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;
let y = edge.transform(referenceTile, x);
if (y === undefined) {
_this.loaded = false;
throw `[CacheRecord._convert] data mid result undefined value (while converting using ${edge}})`;
}
//node.value holds the type string
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;
@ -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
* @memberof OpenSeadragon

View File

@ -1885,34 +1885,27 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {OpenSeadragon.Tile} tile
*/
_tryFindTileCacheRecord: function(tile) {
if (!tile.cacheKey) {
tile.cacheKey = "";
tile.originalCacheKey = "";
if (tile.cacheKey !== tile.originalCacheKey) {
//we found original data: this data will be used to re-execute the pipeline
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);
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.loaded = false;
//set data as null, cache already has data, it does not overwrite
this._setTileLoaded(tile, null, null, null, record.type,
// we could send null as data (cache not re-created), but deprecated events access the data
this._setTileLoaded(tile, record.data, null, 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, null, null, record.type);
return true;
}
}
return false;
},
@ -2103,8 +2096,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType, withEvent = true) {
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
// -> reason why it is not in the constructor
tile.setCache(tile.cacheKey, data, dataType, false);
// does nothing if tile.cacheKey already present
tile.addCache(tile.cacheKey, data, dataType, false);
let resolver = null,
increment = 0,
@ -2127,11 +2120,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
const cache = tile.getCache(tile.cacheKey),
requiredTypes = _this.viewer.drawer.getSupportedDataFormats();
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);
} else if (!requiredTypes.includes(cache.type)) {
//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.loaded = true;
resolver(tile);

View File

@ -99,10 +99,21 @@
// Unique type per drawer: uploads texture to unique webgl context.
this._dataType = `${Date.now()}_TEX_2D`;
this._supportedFormats = [];
this._setupTextureHandlers(this._dataType);
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
@ -315,7 +326,7 @@
);
return;
}
const textureInfo = this.getCompatibleData(tile);
const textureInfo = this.getDataToDraw(tile, true);
if (!textureInfo) {
return;
}
@ -830,8 +841,7 @@
// TextureInfo stored in the cache
return {
texture: texture,
position: position,
cpuData: data,
position: position
};
};
const tex2DCompatibleDestructor = textureInfo => {
@ -839,22 +849,16 @@
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.
const c2dTexType = thisType + ":context2d",
imageTexType = thisType + ":image";
this.declareSupportedDataFormats(imageTexType, c2dTexType);
this._supportedFormats.push(c2dTexType, imageTexType);
// 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
$.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(c2dTexType, "context2d", dataRetrieval, 1, 3);
$.convertor.learn(imageTexType, "image", dataRetrieval, 1, 3);
$.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor);
$.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor);

View File

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

View File

@ -252,15 +252,15 @@
//load data
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);
tile01.setCache(tile01.cacheKey, 0, T_B, false);
tile01.addCache(tile01.cacheKey, 0, T_B, false);
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);
tile11.setCache(tile11.cacheKey, 0, T_C, false);
tile11.addCache(tile11.cacheKey, 0, T_C, false);
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 value = await tile.getData(type, false);
@ -446,15 +446,15 @@
//load data
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);
tile01.setCache(tile01.cacheKey, 0, T_B, false);
tile01.addCache(tile01.cacheKey, 0, T_B, false);
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);
tile11.setCache(tile11.cacheKey, 0, T_C, false);
tile11.addCache(tile11.cacheKey, 0, T_C, false);
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
(async function() {
@ -471,7 +471,7 @@
test.equal(theTileKey, tile00.originalCacheKey, "Original cache key preserved.");
//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.numCachesLoaded(), 5, "The cache has now 5 items.");
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.");
//add and delete cache nothing changes
tile00.setCache("my_custom_cache2", 128, T_C);
tile00.unsetCache("my_custom_cache2");
tile00.addCache("my_custom_cache2", 128, T_C);
tile00.removeCache("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);
tile00.addCache("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);
tile00.removeCache("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);
tile01.addCache("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);
tile01.removeCache("my_custom_cache2", false);
//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.");
//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!");
//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
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);
tile01.addCache("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.");