mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-02-07 02:19:23 +03:00
Merge pull request #2643 from RationAI/fix/fallback-canvas-drawer
Flexible support for drawers to modify cache
This commit is contained in:
commit
dbc72d4d98
@ -38,7 +38,14 @@
|
|||||||
* @typedef BaseDrawerOptions
|
* @typedef BaseDrawerOptions
|
||||||
* @memberOf OpenSeadragon
|
* @memberOf OpenSeadragon
|
||||||
* @property {boolean} [usePrivateCache=false] specify whether the drawer should use
|
* @property {boolean} [usePrivateCache=false] specify whether the drawer should use
|
||||||
* detached (=internal) cache object in case it has to perform type conversion
|
* detached (=internal) cache object in case it has to perform custom type conversion atop
|
||||||
|
* what cache performs. In that case, drawer must implement internalCacheCreate() which gets data in one
|
||||||
|
* of formats the drawer declares as supported. This method must return object to be used during drawing.
|
||||||
|
* You should probably implement also internalCacheFree() to provide cleanup logics.
|
||||||
|
*
|
||||||
|
* @property {boolean} [preloadCache=true]
|
||||||
|
* When internalCacheCreate is used, it can be applied offline (asynchronously) during data processing = preloading,
|
||||||
|
* or just in time before rendering (if necessary). Preloading supports
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
|
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
|
||||||
@ -86,6 +93,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||||||
this.container.appendChild( this.canvas );
|
this.container.appendChild( this.canvas );
|
||||||
|
|
||||||
this._checkInterfaceImplementation();
|
this._checkInterfaceImplementation();
|
||||||
|
this.setInternalCacheNeedsRefresh(); // initializes stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +105,8 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||||||
*/
|
*/
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
usePrivateCache: false
|
usePrivateCache: false,
|
||||||
|
preloadCache: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +216,15 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||||||
$.console.error('Drawer.destroy must be implemented by child class');
|
$.console.error('Drawer.destroy must be implemented by child class');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy internal cache. Should be called within destroy() when
|
||||||
|
* usePrivateCache is set to true. Ensures cleanup of anything created
|
||||||
|
* by internalCacheCreate(...).
|
||||||
|
*/
|
||||||
|
destroyInternalCache() {
|
||||||
|
this.viewer.tileCache.clearDrawerInternalCache(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TiledImage} tiledImage the tiled image that is calling the function
|
* @param {TiledImage} tiledImage the tiled image that is calling the function
|
||||||
* @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.
|
||||||
@ -240,6 +258,31 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||||||
$.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.');
|
$.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If options.usePrivateCache is true, this method MUST RETURN the private cache content
|
||||||
|
* @param {OpenSeadragon.CacheRecord} cache
|
||||||
|
* @param {OpenSeadragon.Tile} tile
|
||||||
|
* @return any
|
||||||
|
*/
|
||||||
|
internalCacheCreate(cache, tile) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is possible to perform any necessary cleanup on internal cache, necessary if you
|
||||||
|
* need to clean up some memory (e.g. destroy canvas by setting with & height to 0).
|
||||||
|
* @param {*} data object returned by internalCacheCreate(...)
|
||||||
|
*/
|
||||||
|
internalCacheFree(data) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call to invalidate internal cache. It will be rebuilt. With synchronous converions,
|
||||||
|
* it will be rebuilt immediatelly. With asynchronous, it will be rebuilt once invalidation
|
||||||
|
* routine happens, e.g. you should call also requestInvalidate() if you need to happen
|
||||||
|
* it as soon as possible.
|
||||||
|
*/
|
||||||
|
setInternalCacheNeedsRefresh() {
|
||||||
|
this._dataNeedsRefresh = $.now();
|
||||||
|
}
|
||||||
|
|
||||||
// Private functions
|
// Private functions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -795,8 +795,6 @@ $.Tile.prototype = {
|
|||||||
this.tiledImage = null;
|
this.tiledImage = null;
|
||||||
this._caches = {};
|
this._caches = {};
|
||||||
this._cacheSize = 0;
|
this._cacheSize = 0;
|
||||||
this.element = null;
|
|
||||||
this.imgElement = null;
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this._cKey = this._ocKey;
|
this._cKey = this._ocKey;
|
||||||
|
364
src/tilecache.js
364
src/tilecache.js
@ -79,7 +79,7 @@
|
|||||||
*/
|
*/
|
||||||
await() {
|
await() {
|
||||||
if (!this._promise) { //if not cache loaded, do not fail
|
if (!this._promise) { //if not cache loaded, do not fail
|
||||||
return $.Promise.resolve();
|
return $.Promise.resolve(this._data);
|
||||||
}
|
}
|
||||||
return this._promise;
|
return this._promise;
|
||||||
}
|
}
|
||||||
@ -162,92 +162,171 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g.
|
* Access of the data by drawers, synchronous function. Should always access a valid main cache.
|
||||||
* cache swap performed on working cache (replaceCache()) must be synchronous such that cache is always
|
* This is ensured by invalidation routine that executes data modification on a copy record, and
|
||||||
* ready to render, and swaps atomically between render calls.
|
* then synchronously swaps records (main caches) to the new data between render calls.
|
||||||
|
*
|
||||||
|
* If a drawer decides to have internal cache with synchronous behavior, it is (if necessary)
|
||||||
|
* performed during this phase.
|
||||||
*
|
*
|
||||||
* @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer
|
* @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer
|
||||||
* defines the supported formats this cache should return **synchronously**
|
* defines the supported formats this cache should return **synchronously**
|
||||||
* @param {OpenSeadragon.Tile} tileToDraw reference to the tile that is in the process of drawing and
|
* @param {OpenSeadragon.Tile} tileToDraw reference to the tile that is in the process of drawing and
|
||||||
* for which we request the data; if we attempt to draw such tile while main cache target is destroyed,
|
* for which we request the data; if we attempt to draw such tile while main cache target is destroyed,
|
||||||
* attempt to reset the tile state to force system to re-download it again
|
* attempt to reset the tile state to force system to re-download it again
|
||||||
* @returns {OpenSeadragon.CacheRecord|OpenSeadragon.SimpleCacheRecord|undefined} desired data if available,
|
* @returns {OpenSeadragon.CacheRecord|OpenSeadragon.InternalCacheRecord|undefined} desired data if available,
|
||||||
* wrapped in the cache container. This data is guaranteed to be loaded & in the type supported by the drawer.
|
* wrapped in the cache container. This data is guaranteed to be loaded & in the type supported by the drawer.
|
||||||
* Returns undefined if the data is not ready for rendering.
|
* Returns undefined if the data is not ready for rendering.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getDataForRendering(drawer, tileToDraw) {
|
getDataForRendering(drawer, tileToDraw) {
|
||||||
const supportedTypes = drawer.getSupportedDataFormats(),
|
const keepInternalCopy = drawer.options.usePrivateCache;
|
||||||
keepInternalCopy = drawer.options.usePrivateCache;
|
|
||||||
if (this.loaded && supportedTypes.includes(this.type)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Test cache state
|
||||||
|
if (!this.loaded) {
|
||||||
|
$.console.error("Attempt to draw tile when not loaded main cache!");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (this._destroyed) {
|
if (this._destroyed) {
|
||||||
$.console.error("Attempt to draw tile with destroyed main cache!");
|
$.console.error("Attempt to draw tile with destroyed main cache!");
|
||||||
tileToDraw._unload(); // try to restore the state so that the tile is later on fetched again
|
tileToDraw._unload(); // try to restore the state so that the tile is later on fetched again
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let internalCache = this[DRAWER_INTERNAL_CACHE];
|
// Ensure cache in a format suitable for the current drawer. If not it is an error, prepareForRendering
|
||||||
internalCache = internalCache && internalCache[drawer.getId()];
|
// should be called at the end of invalidation routine instead. Since the processing is async, we are
|
||||||
if (keepInternalCopy && !internalCache) {
|
// unable to provide the rendering data immediatelly - return.
|
||||||
$.console.warn("Attempt to render cache that is not prepared for current drawer " +
|
const supportedTypes = drawer.getSupportedDataFormats();
|
||||||
"supported format: the preparation should've happened after tile processing has finished.",
|
if (!supportedTypes.includes(this.type)) {
|
||||||
this, tileToDraw);
|
$.console.error("Attempt to draw tile with unsupported target drawer type!");
|
||||||
|
this.prepareForRendering(drawer);
|
||||||
this.prepareForRendering(drawer.getId(), supportedTypes, keepInternalCopy)
|
|
||||||
.then(() => this._triggerNeedsDraw());
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internalCache) {
|
// If we support internal cache
|
||||||
internalCache.withTileReference(this._tRef);
|
if (keepInternalCopy) {
|
||||||
} else {
|
// let sync preparation handle data if no preloading desired
|
||||||
internalCache = this;
|
if (!drawer.options.preloadCache) {
|
||||||
|
return this.prepareInternalCacheSync(drawer);
|
||||||
}
|
}
|
||||||
|
// or check internal cache state before returning
|
||||||
// Cache in the process of loading, no-op
|
const internalCache = this._getInternalCacheRef(drawer);
|
||||||
if (!internalCache.loaded) {
|
if (!internalCache || !internalCache.loaded) {
|
||||||
$.console.warn("Attempt to render cache that is not prepared for current drawer: " +
|
$.console.error("Attempt to draw tile with internal cache non-ready state!");
|
||||||
"internal cache still loading: this should be awaited.",
|
|
||||||
this, tileToDraw);
|
|
||||||
this._triggerNeedsDraw();
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!supportedTypes.includes(internalCache.type)) {
|
|
||||||
let logReference = this[DRAWER_INTERNAL_CACHE];
|
|
||||||
logReference = logReference ? Object.entries(logReference) : this;
|
|
||||||
$.console.warn("Attempt to render cache that is not prepared for current drawer " +
|
|
||||||
"supported format: the preparation should've happened after tile processing has finished.",
|
|
||||||
logReference, tileToDraw);
|
|
||||||
|
|
||||||
internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0])
|
|
||||||
.then(() => this._triggerNeedsDraw());
|
|
||||||
return undefined; // type is NOT compatible
|
|
||||||
}
|
|
||||||
return internalCache;
|
return internalCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// else just return self reference
|
||||||
* Should not be called if cache type is already among supported types
|
return this;
|
||||||
* @private
|
|
||||||
* @param drawerId
|
|
||||||
* @param supportedTypes
|
|
||||||
* @param keepInternalCopy if a drawer requests internal copy, it means it can only use
|
|
||||||
* given cache for itself, cannot be shared -> initialize privately
|
|
||||||
* @return {OpenSeadragon.Promise<OpenSeadragon.SimpleCacheRecord|OpenSeadragon.CacheRecord> | null}
|
|
||||||
* reference to the cache processed for drawer rendering requirements, or null on error
|
|
||||||
*/
|
|
||||||
prepareForRendering(drawerId, supportedTypes, keepInternalCopy = true) {
|
|
||||||
// if not internal copy and we have no data, or we are ready to render, exit
|
|
||||||
if (!this.loaded || supportedTypes.includes(this.type)) {
|
|
||||||
return $.Promise.resolve(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keepInternalCopy) {
|
/**
|
||||||
return this.transformTo(supportedTypes);
|
* Preparation for rendering ensures the CacheRecord is in a format supported by the current
|
||||||
|
* drawer. Furthermore, if internal cache is to be used by a drawer with preloading enabled,
|
||||||
|
* it happens in this step.
|
||||||
|
*
|
||||||
|
* Note: Should not be called if cache type is already among supported types.
|
||||||
|
* @private
|
||||||
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
|
* @return {OpenSeadragon.Promise<*>} reference to the data,
|
||||||
|
* or null if not data yet loaded/ready (usually due to error)
|
||||||
|
*/
|
||||||
|
prepareForRendering(drawer) {
|
||||||
|
const supportedTypes = drawer.getRequiredDataFormats();
|
||||||
|
|
||||||
|
// If not loaded, await until ready and try again
|
||||||
|
if (!this.loaded) {
|
||||||
|
return this.await().then(_ => this.prepareForRendering(drawer));
|
||||||
|
}
|
||||||
|
|
||||||
|
let selfPromise;
|
||||||
|
// If not in one of required types, transform
|
||||||
|
if (!supportedTypes.includes(this.type)) {
|
||||||
|
selfPromise = this.transformTo(supportedTypes);
|
||||||
|
} else {
|
||||||
|
selfPromise = this.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If internal cache wanted and preloading enabled, convert now
|
||||||
|
if (drawer.options.usePrivateCache && drawer.options.preloadCache) {
|
||||||
|
return selfPromise.then(_ => this.prepareInternalCacheAsync(drawer));
|
||||||
|
}
|
||||||
|
return selfPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal cache is defined by a Drawer. Async preparation happens as the last step in the
|
||||||
|
* invalidation routine.
|
||||||
|
* Must not be called if drawer.options.usePrivateCache == false. Called inside prepareForRenderine
|
||||||
|
* by cache itself if preloadCache == true (supports async behavior).
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
|
* @return {OpenSeadragon.Promise<*>} reference to the data wrapped in a promise,
|
||||||
|
* or null if not data yet loaded/ready (usually due to error)
|
||||||
|
*/
|
||||||
|
prepareInternalCacheAsync(drawer) {
|
||||||
|
let internalCache = this._getInternalCacheRef(drawer);
|
||||||
|
if (this._checkInternalCacheUpToDate(internalCache, drawer)) {
|
||||||
|
return internalCache.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reset
|
||||||
|
if (internalCache && !internalCache.loaded) {
|
||||||
|
internalCache.await().then(() => internalCache.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
$.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!");
|
||||||
|
const transformedData = drawer.internalCacheCreate(this, this._tRef);
|
||||||
|
$.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");
|
||||||
|
const drawerID = drawer.getId();
|
||||||
|
internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData,
|
||||||
|
drawerID, (data) => drawer.internalCacheFree(data));
|
||||||
|
return internalCache.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal cache is defined by a Drawer. Sync preparation happens directly before rendering.
|
||||||
|
* Must not be called if drawer.options.usePrivateCache == false. Called inside getDataForRendering
|
||||||
|
* by cache itself if preloadCache == false (without support for async behavior).
|
||||||
|
* @private
|
||||||
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
|
* @return {OpenSeadragon.InternalCacheRecord} reference to the cache
|
||||||
|
*/
|
||||||
|
prepareInternalCacheSync(drawer) {
|
||||||
|
let internalCache = this._getInternalCacheRef(drawer);
|
||||||
|
if (this._checkInternalCacheUpToDate(internalCache, drawer)) {
|
||||||
|
return internalCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reset
|
||||||
|
if (internalCache) {
|
||||||
|
internalCache.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!");
|
||||||
|
const transformedData = drawer.internalCacheCreate(this, this._tRef);
|
||||||
|
$.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!");
|
||||||
|
|
||||||
|
const drawerID = drawer.getId();
|
||||||
|
internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData,
|
||||||
|
drawerID, (data) => drawer.internalCacheFree(data));
|
||||||
|
return internalCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an internal cache reference for given drawer
|
||||||
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
|
* @return {OpenSeadragon.InternalCacheRecord|undefined}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getInternalCacheRef(drawer) {
|
||||||
|
const options = drawer.options;
|
||||||
|
if (!options.usePrivateCache) {
|
||||||
|
$.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false.");
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we can get here only if we want to render incompatible type
|
// we can get here only if we want to render incompatible type
|
||||||
@ -255,32 +334,19 @@
|
|||||||
if (!internalCache) {
|
if (!internalCache) {
|
||||||
internalCache = this[DRAWER_INTERNAL_CACHE] = {};
|
internalCache = this[DRAWER_INTERNAL_CACHE] = {};
|
||||||
}
|
}
|
||||||
|
return internalCache[drawer.getId()];
|
||||||
internalCache = internalCache[drawerId];
|
|
||||||
if (internalCache && supportedTypes.includes(internalCache.type)) {
|
|
||||||
// already done
|
|
||||||
return $.Promise.resolve(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversionPath = $.convertor.getConversionPath(this.type, supportedTypes);
|
/**
|
||||||
if (!conversionPath) {
|
* @param {OpenSeadragon.InternalCacheRecord} internalCache
|
||||||
$.console.error(`[getDataForRendering] Conversion ${this.type} ---> ${supportedTypes} cannot be done!`);
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
return $.Promise.resolve(this);
|
* @return {boolean} false if the internal cache is outdated
|
||||||
}
|
* @private
|
||||||
const newInternalCache = new $.SimpleCacheRecord();
|
*/
|
||||||
|
_checkInternalCacheUpToDate(internalCache, drawer) {
|
||||||
newInternalCache.withTileReference(this._tRef);
|
// We respect existing records, unless they are outdated. Invalidation routine by its nature
|
||||||
const selectedFormat = conversionPath[conversionPath.length - 1].target.value;
|
// destroys internal cache, therefore we do not need to check if internal cache is consistent with its parent.
|
||||||
return $.convertor.convert(this._tRef, this.data, this.type, selectedFormat).then(data => {
|
return internalCache && internalCache.loaded && internalCache.tstamp >= drawer._dataNeedsRefresh;
|
||||||
newInternalCache.setDataAs(data, selectedFormat); // synchronous, SimpleCacheRecord call
|
|
||||||
|
|
||||||
// if existed, delete
|
|
||||||
if (internalCache) {
|
|
||||||
internalCache.destroy();
|
|
||||||
}
|
|
||||||
this[DRAWER_INTERNAL_CACHE][drawerId] = newInternalCache;
|
|
||||||
return newInternalCache;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -330,17 +396,26 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If cache ceases to be the primary one, free data
|
* If cache ceases to be the primary one, free data
|
||||||
|
* @param {string} drawerId if undefined, all caches are freed, else only target one
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
destroyInternalCache() {
|
destroyInternalCache(drawerId = undefined) {
|
||||||
const internal = this[DRAWER_INTERNAL_CACHE];
|
const internal = this[DRAWER_INTERNAL_CACHE];
|
||||||
if (internal) {
|
if (internal) {
|
||||||
|
if (drawerId) {
|
||||||
|
const cache = internal[drawerId];
|
||||||
|
if (cache) {
|
||||||
|
cache.destroy();
|
||||||
|
delete internal[drawerId];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (let iCache in internal) {
|
for (let iCache in internal) {
|
||||||
internal[iCache].destroy();
|
internal[iCache].destroy();
|
||||||
}
|
}
|
||||||
delete this[DRAWER_INTERNAL_CACHE];
|
delete this[DRAWER_INTERNAL_CACHE];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversion requires tile references:
|
* Conversion requires tile references:
|
||||||
@ -623,7 +698,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class SimpleCacheRecord
|
* @class InternalCacheRecord
|
||||||
* @memberof OpenSeadragon
|
* @memberof OpenSeadragon
|
||||||
* @classdesc Simple cache record without robust support for async access. Meant for internal use only.
|
* @classdesc Simple cache record without robust support for async access. Meant for internal use only.
|
||||||
*
|
*
|
||||||
@ -635,12 +710,23 @@
|
|||||||
* It also does not record tiles nor allows cache/tile sharing.
|
* It also does not record tiles nor allows cache/tile sharing.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
$.SimpleCacheRecord = class {
|
$.InternalCacheRecord = class {
|
||||||
constructor(preferredTypes) {
|
constructor(data, type, onDestroy) {
|
||||||
this._data = null;
|
this.tstamp = $.now();
|
||||||
this._type = null;
|
this._ondestroy = onDestroy;
|
||||||
this.loaded = false;
|
this._type = type;
|
||||||
this.format = Array.isArray(preferredTypes) ? preferredTypes : null;
|
|
||||||
|
if (data instanceof $.Promise) {
|
||||||
|
this._promise = data;
|
||||||
|
data.then(data => {
|
||||||
|
this.loaded = true;
|
||||||
|
this._data = data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._promise = null;
|
||||||
|
this.loaded = true;
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -659,83 +745,39 @@
|
|||||||
return this._type;
|
return this._type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Await ongoing process so that we get cache ready on callback.
|
||||||
|
* @returns {OpenSeadragon.Promise<?>}
|
||||||
|
*/
|
||||||
|
await() {
|
||||||
|
if (!this._promise) { //if not cache loaded, do not fail
|
||||||
|
return $.Promise.resolve(this._data);
|
||||||
|
}
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called before transformTo or setDataAs. To keep
|
* Must be called before transformTo or setDataAs. To keep
|
||||||
* compatible api with CacheRecord where tile refs are known.
|
* compatible api with CacheRecord where tile refs are known.
|
||||||
* @param {OpenSeadragon.Tile} referenceTile reference tile for conversion
|
* @param {OpenSeadragon.Tile} referenceTile reference tile for conversion
|
||||||
* @return {OpenSeadragon.SimpleCacheRecord} self reference for builder pattern
|
* @return {OpenSeadragon.InternalCacheRecord} self reference for builder pattern
|
||||||
*/
|
*/
|
||||||
withTileReference(referenceTile) {
|
withTileReference(referenceTile) {
|
||||||
this._temporaryTileRef = referenceTile;
|
this._temporaryTileRef = referenceTile;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 ${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.
|
* Free all the data and call data destructors if defined.
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
$.convertor.destroy(this._data, this._type);
|
if (this.loaded) {
|
||||||
this._data = null;
|
if (this._ondestroy) {
|
||||||
this._type = null;
|
this._ondestroy(this._data);
|
||||||
|
}
|
||||||
|
this._data = null;
|
||||||
|
this.loaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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._type);
|
|
||||||
this._type = type;
|
|
||||||
this._data = data;
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1155,6 +1197,24 @@
|
|||||||
this._cachesLoadedCount = 0;
|
this._cachesLoadedCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up internal drawer data for a given drawer
|
||||||
|
* @param {OpenSeadragon.DrawerBase} drawer
|
||||||
|
*/
|
||||||
|
clearDrawerInternalCache(drawer) {
|
||||||
|
const drawerId = drawer.getId();
|
||||||
|
for (let zombie of this._zombiesLoaded) {
|
||||||
|
if (zombie) {
|
||||||
|
zombie.destroyInternalCache(drawerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let cache of this._cachesLoaded) {
|
||||||
|
if (cache) {
|
||||||
|
cache.destroyInternalCache(drawerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns reference to all tiles loaded by a particular
|
* Returns reference to all tiles loaded by a particular
|
||||||
* tiled image item
|
* tiled image item
|
||||||
|
@ -100,17 +100,15 @@
|
|||||||
this._setupCanvases();
|
this._setupCanvases();
|
||||||
this._setupRenderer();
|
this._setupRenderer();
|
||||||
|
|
||||||
this._supportedFormats = this._setupTextureHandlers();
|
this._supportedFormats = ["context2d", "image"];
|
||||||
this._requiredFormats = this._supportedFormats;
|
|
||||||
this._setupCallCount = 1;
|
|
||||||
|
|
||||||
this.context = this._outputContext; // API required by tests
|
this.context = this._outputContext; // API required by tests
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
// use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref)
|
// use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref)
|
||||||
usePrivateCache: true
|
usePrivateCache: true,
|
||||||
|
preloadCache: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,6 +169,8 @@
|
|||||||
this.viewer.drawer = null;
|
this.viewer.drawer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.destroyInternalCache();
|
||||||
|
|
||||||
// set our destroyed flag to true
|
// set our destroyed flag to true
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
}
|
}
|
||||||
@ -241,16 +241,7 @@
|
|||||||
this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
|
this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
|
||||||
this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
|
this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
|
||||||
this._backupCanvasDrawer.getSupportedDataFormats = () => this._supportedFormats;
|
this._backupCanvasDrawer.getSupportedDataFormats = () => this._supportedFormats;
|
||||||
this._backupCanvasDrawer.getDataToDraw = (tile) => {
|
this._backupCanvasDrawer.getDataToDraw = this.getDataToDraw.bind(this);
|
||||||
const cache = tile.getCache(tile.cacheKey);
|
|
||||||
if (!cache) {
|
|
||||||
$.console.warn("Attempt to draw tile %s when not cached!", tile);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const dataCache = cache.getDataForRendering(this, tile);
|
|
||||||
// Use CPU Data for the drawer instead
|
|
||||||
return dataCache && dataCache.cpuData;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._backupCanvasDrawer;
|
return this._backupCanvasDrawer;
|
||||||
@ -389,7 +380,7 @@
|
|||||||
let numTilesToDraw = indexInDrawArray + 1;
|
let numTilesToDraw = indexInDrawArray + 1;
|
||||||
const textureInfo = this.getDataToDraw(tile);
|
const textureInfo = this.getDataToDraw(tile);
|
||||||
|
|
||||||
if (textureInfo) {
|
if (textureInfo && textureInfo.texture) {
|
||||||
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
|
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
|
||||||
} else {
|
} else {
|
||||||
// console.log('No tile info', tile);
|
// console.log('No tile info', tile);
|
||||||
@ -480,10 +471,6 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequiredDataFormats() {
|
|
||||||
return this._requiredFormats;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API required by all Drawer implementations
|
// Public API required by all Drawer implementations
|
||||||
/**
|
/**
|
||||||
* Sets whether image smoothing is enabled or disabled
|
* Sets whether image smoothing is enabled or disabled
|
||||||
@ -492,15 +479,9 @@
|
|||||||
setImageSmoothingEnabled(enabled){
|
setImageSmoothingEnabled(enabled){
|
||||||
if( this._imageSmoothingEnabled !== enabled ){
|
if( this._imageSmoothingEnabled !== enabled ){
|
||||||
this._imageSmoothingEnabled = enabled;
|
this._imageSmoothingEnabled = enabled;
|
||||||
|
this.setInternalCacheNeedsRefresh();
|
||||||
// Todo consider removing old type handlers if _supportedFormats had already types defined,
|
this.viewer.forceRedraw();
|
||||||
// and remove support for rendering old types...
|
|
||||||
const newFormats = this._setupTextureHandlers(); // re-sets the type to enforce re-initialization
|
|
||||||
this._supportedFormats.push(...newFormats);
|
|
||||||
this._requiredFormats = newFormats;
|
|
||||||
return this.viewer.requestInvalidate();
|
|
||||||
}
|
}
|
||||||
return $.Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -837,7 +818,6 @@
|
|||||||
//bind the frame buffer to the new texture
|
//bind the frame buffer to the new texture
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
@ -883,18 +863,20 @@
|
|||||||
this.viewer.addHandler("resize", this._resizeHandler);
|
this.viewer.addHandler("resize", this._resizeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupTextureHandlers() {
|
internalCacheCreate(cache, tile) {
|
||||||
const tex2DCompatibleLoader = (tile, data) => {
|
|
||||||
let tiledImage = tile.tiledImage;
|
let tiledImage = tile.tiledImage;
|
||||||
let gl = this._gl;
|
let gl = this._gl;
|
||||||
let texture;
|
let texture;
|
||||||
let position;
|
let position;
|
||||||
|
|
||||||
|
let data = cache.data;
|
||||||
|
|
||||||
if (!tiledImage.isTainted()) {
|
if (!tiledImage.isTainted()) {
|
||||||
if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){
|
if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){
|
||||||
tiledImage.setTainted(true);
|
tiledImage.setTainted(true);
|
||||||
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
|
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
|
||||||
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
|
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
|
||||||
|
this.setInternalCacheNeedsRefresh();
|
||||||
} else {
|
} else {
|
||||||
let sourceWidthFraction, sourceHeightFraction;
|
let sourceWidthFraction, sourceHeightFraction;
|
||||||
if (tile.sourceBounds) {
|
if (tile.sourceBounds) {
|
||||||
@ -937,41 +919,40 @@
|
|||||||
// This depends on gl.TEXTURE_2D being bound to the texture
|
// This depends on gl.TEXTURE_2D being bound to the texture
|
||||||
// associated with this canvas before calling this function
|
// associated with this canvas before calling this function
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||||
|
// TextureInfo stored in the cache
|
||||||
|
return {
|
||||||
|
texture: texture,
|
||||||
|
position: position,
|
||||||
|
};
|
||||||
} catch (e){
|
} catch (e){
|
||||||
// Todo a bit dirty re-use of the tainted flag, but makes the code more stable
|
// Todo a bit dirty re-use of the tainted flag, but makes the code more stable
|
||||||
tiledImage.setTainted(true);
|
tiledImage.setTainted(true);
|
||||||
$.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e);
|
$.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e);
|
||||||
this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.');
|
this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.');
|
||||||
|
this.setInternalCacheNeedsRefresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (data instanceof Image) {
|
||||||
// TextureInfo stored in the cache
|
const canvas = document.createElement( 'canvas' );
|
||||||
return {
|
canvas.width = data.width;
|
||||||
texture: texture,
|
canvas.height = data.height;
|
||||||
position: position,
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
cpuData: data // Reference to the outer cache data, used to draw if webgl canont be used
|
context.drawImage( data, 0, 0 );
|
||||||
};
|
data = context;
|
||||||
};
|
}
|
||||||
const tex2DCompatibleDestructor = textureInfo => {
|
if (data instanceof CanvasRenderingContext2D) {
|
||||||
if (textureInfo) {
|
return data;
|
||||||
this._gl.deleteTexture(textureInfo.texture);
|
}
|
||||||
|
$.console.error("Unsupported data used for WebGL Drawer - probably a bug!");
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const thisType = `${this.getId()}_${this._setupCallCount++}_TEX_2D`;
|
internalCacheFree(data) {
|
||||||
// Differentiate type also based on type used to upload data: we can support bidirectional conversion.
|
if (data && data.texture) {
|
||||||
const c2dTexType = thisType + ":context2d",
|
this._gl.deleteTexture(data.texture);
|
||||||
imageTexType = thisType + ":image";
|
data.texture = null;
|
||||||
|
}
|
||||||
// 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, (t, d) => tex2DCompatibleLoader(t, d.canvas), 1, 3);
|
|
||||||
$.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3);
|
|
||||||
|
|
||||||
$.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor);
|
|
||||||
$.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor);
|
|
||||||
return [c2dTexType, imageTexType];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
|
11
src/world.js
11
src/world.js
@ -310,9 +310,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
// We call the event on the parent viewer window no matter what
|
// We call the event on the parent viewer window no matter what
|
||||||
const eventTarget = this.viewer.viewer || this.viewer;
|
const eventTarget = this.viewer.viewer || this.viewer;
|
||||||
// However, we must pick the correct drawer reference (navigator VS viewer)
|
// However, we must pick the correct drawer reference (navigator VS viewer)
|
||||||
const supportedFormats = this.viewer.drawer.getRequiredDataFormats();
|
const drawer = this.viewer.drawer;
|
||||||
const keepInternalCacheCopy = this.viewer.drawer.options.usePrivateCache;
|
|
||||||
const drawerId = this.viewer.drawer.getId();
|
|
||||||
|
|
||||||
const jobList = tileList.map(tile => {
|
const jobList = tileList.map(tile => {
|
||||||
const tiledImage = tile.tiledImage;
|
const tiledImage = tile.tiledImage;
|
||||||
@ -360,7 +358,6 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true);
|
tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @event tile-invalidated
|
* @event tile-invalidated
|
||||||
* @memberof OpenSeadragon.Viewer
|
* @memberof OpenSeadragon.Viewer
|
||||||
@ -391,7 +388,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
}).then(_ => {
|
}).then(_ => {
|
||||||
if (originalCache.__invStamp === tStamp && (tile.loaded || tile.loading)) {
|
if (originalCache.__invStamp === tStamp && (tile.loaded || tile.loading)) {
|
||||||
if (workingCache) {
|
if (workingCache) {
|
||||||
return workingCache.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(c => {
|
return workingCache.prepareForRendering(drawer).then(c => {
|
||||||
if (c && originalCache.__invStamp === tStamp) {
|
if (c && originalCache.__invStamp === tStamp) {
|
||||||
atomicCacheSwap();
|
atomicCacheSwap();
|
||||||
originalCache.__invStamp = null;
|
originalCache.__invStamp = null;
|
||||||
@ -402,7 +399,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
// If we requested restore, perform now
|
// If we requested restore, perform now
|
||||||
if (restoreTiles) {
|
if (restoreTiles) {
|
||||||
const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey);
|
const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey);
|
||||||
return freshOriginalCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then((c) => {
|
return freshOriginalCacheRef.prepareForRendering(drawer).then((c) => {
|
||||||
if (c && originalCache.__invStamp === tStamp) {
|
if (c && originalCache.__invStamp === tStamp) {
|
||||||
atomicCacheSwap();
|
atomicCacheSwap();
|
||||||
originalCache.__invStamp = null;
|
originalCache.__invStamp = null;
|
||||||
@ -412,7 +409,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
|
|
||||||
// Preventive call to ensure we stay compatible
|
// Preventive call to ensure we stay compatible
|
||||||
const freshMainCacheRef = tile.getCache();
|
const freshMainCacheRef = tile.getCache();
|
||||||
return freshMainCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(() => {
|
return freshMainCacheRef.prepareForRendering(drawer).then(() => {
|
||||||
atomicCacheSwap();
|
atomicCacheSwap();
|
||||||
originalCache.__invStamp = null;
|
originalCache.__invStamp = null;
|
||||||
});
|
});
|
||||||
|
@ -86,27 +86,25 @@
|
|||||||
destroyE++;
|
destroyE++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
QUnit.module('TileCache', {
|
||||||
|
beforeEach: function () {
|
||||||
|
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||||
|
|
||||||
|
testLog.reset();
|
||||||
|
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
||||||
|
|
||||||
|
// Reset counters
|
||||||
|
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
||||||
|
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
||||||
|
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
||||||
|
|
||||||
OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase {
|
OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
super(opts);
|
super(opts);
|
||||||
this.testEvents = new OpenSeadragon.EventSource();
|
this.testEvents = new OpenSeadragon.EventSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
getType() {
|
|
||||||
return "test-cache-drawer";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make test use private cache
|
|
||||||
get defaultOptions() {
|
|
||||||
return {
|
|
||||||
usePrivateCache: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportedDataFormats() {
|
|
||||||
return [T_C, T_E];
|
|
||||||
}
|
|
||||||
|
|
||||||
static isSupported() {
|
static isSupported() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -128,12 +126,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internalCacheFree(data) {
|
||||||
|
this.testEvents.raiseEvent('free-data');
|
||||||
|
}
|
||||||
|
|
||||||
canRotate() {
|
canRotate() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
//noop
|
this.destroyInternalCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
setImageSmoothingEnabled(imageSmoothingEnabled){
|
setImageSmoothingEnabled(imageSmoothingEnabled){
|
||||||
@ -149,6 +151,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer {
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return "test-cache-drawer-sync";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupportedDataFormats() {
|
||||||
|
return [T_C, T_E];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make test use private cache
|
||||||
|
get defaultOptions() {
|
||||||
|
return {
|
||||||
|
usePrivateCache: true,
|
||||||
|
preloadCache: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internalCacheCreate(cache, tile) {
|
||||||
|
this.testEvents.raiseEvent('create-data');
|
||||||
|
return cache.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer {
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return "test-cache-drawer-async";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupportedDataFormats() {
|
||||||
|
return [T_A];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make test use private cache
|
||||||
|
get defaultOptions() {
|
||||||
|
return {
|
||||||
|
usePrivateCache: true,
|
||||||
|
preloadCache: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internalCacheCreate(cache, tile) {
|
||||||
|
this.testEvents.raiseEvent('create-data');
|
||||||
|
return cache.getDataAs(T_C, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
internalCacheFree(data) {
|
||||||
|
super.internalCacheFree(data);
|
||||||
|
// Be nice and truly destroy the data copy
|
||||||
|
OpenSeadragon.convertor.destroy(data, T_C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource {
|
OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource {
|
||||||
|
|
||||||
supports( data, url ){
|
supports( data, url ){
|
||||||
@ -177,33 +233,18 @@
|
|||||||
context.finish(0, null, T_A);
|
context.finish(0, null, T_A);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------
|
|
||||||
QUnit.module('TileCache', {
|
|
||||||
beforeEach: function () {
|
|
||||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
|
||||||
|
|
||||||
testLog.reset();
|
|
||||||
|
|
||||||
viewer = OpenSeadragon({
|
|
||||||
id: 'example',
|
|
||||||
prefixUrl: '/build/openseadragon/images/',
|
|
||||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
|
||||||
springStiffness: 100, // Faster animation = faster tests
|
|
||||||
drawer: 'test-cache-drawer',
|
|
||||||
});
|
|
||||||
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
|
||||||
|
|
||||||
// Reset counters
|
|
||||||
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
|
||||||
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
|
||||||
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
|
||||||
},
|
},
|
||||||
afterEach: function () {
|
afterEach: function () {
|
||||||
if (viewer && viewer.close) {
|
if (viewer && viewer.close) {
|
||||||
viewer.close();
|
viewer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some tests test all drawers - remove test drawers to avoid collision with other tests
|
||||||
|
OpenSeadragon.EmptyTestT_ATileSource = null;
|
||||||
|
OpenSeadragon.AsnycInternalCacheDrawer = null;
|
||||||
|
OpenSeadragon.SyncInternalCacheDrawer = null;
|
||||||
|
OpenSeadragon.TestCacheDrawer = null;
|
||||||
|
|
||||||
viewer = null;
|
viewer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -313,18 +354,34 @@
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Tile API and cache interaction
|
// Tile API and cache interaction
|
||||||
QUnit.test('Tile: basic rendering & test setup', function(test) {
|
QUnit.test('Tile: basic rendering & test setup (sync drawer)', function(test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
|
viewer = OpenSeadragon({
|
||||||
|
id: 'example',
|
||||||
|
prefixUrl: '/build/openseadragon/images/',
|
||||||
|
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||||
|
springStiffness: 100, // Faster animation = faster tests
|
||||||
|
drawer: 'test-cache-drawer-sync',
|
||||||
|
});
|
||||||
|
|
||||||
const tileCache = viewer.tileCache;
|
const tileCache = viewer.tileCache;
|
||||||
const drawer = viewer.drawer;
|
const drawer = viewer.drawer;
|
||||||
|
|
||||||
let testTileCalled = false;
|
let testTileCalled = false;
|
||||||
|
let countFreeCalled = 0;
|
||||||
|
let countCreateCalled = 0;
|
||||||
drawer.testEvents.addHandler('test-tile', e => {
|
drawer.testEvents.addHandler('test-tile', e => {
|
||||||
testTileCalled = true;
|
testTileCalled = true;
|
||||||
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
|
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
|
||||||
});
|
});
|
||||||
|
drawer.testEvents.addHandler('create-data', e => {
|
||||||
|
countCreateCalled++;
|
||||||
|
});
|
||||||
|
drawer.testEvents.addHandler('free-data', e => {
|
||||||
|
countFreeCalled++;
|
||||||
|
});
|
||||||
|
|
||||||
viewer.addHandler('open', async () => {
|
viewer.addHandler('open', async () => {
|
||||||
await viewer.waitForFinishedJobsForTest();
|
await viewer.waitForFinishedJobsForTest();
|
||||||
@ -339,13 +396,17 @@
|
|||||||
|
|
||||||
for (let tile of tileCache._tilesLoaded) {
|
for (let tile of tileCache._tilesLoaded) {
|
||||||
const cache = tile.getCache();
|
const cache = tile.getCache();
|
||||||
test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache.");
|
test.equal(cache.type, T_C, "Cache data was affected, the drawer supports only T_C since there is no way to get to T_E.");
|
||||||
|
|
||||||
const internalCache = cache.getDataForRendering(drawer, tile);
|
const internalCache = cache.getDataForRendering(drawer, tile);
|
||||||
test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E.");
|
test.equal(internalCache.type, viewer.drawer.getId(), "Sync conversion routine means T_C is also internal since dataCreate only creates data. However, internal cache keeps type of the drawer ID.");
|
||||||
test.ok(internalCache.loaded, "Internal cache ready.");
|
test.ok(internalCache.loaded, "Internal cache ready.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test.ok(countCreateCalled > 0, "Internal cache creation called.");
|
||||||
|
viewer.drawer.destroyInternalCache();
|
||||||
|
test.equal(countCreateCalled, countFreeCalled, "Free called as many times as create.");
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
viewer.open([
|
viewer.open([
|
||||||
@ -357,6 +418,13 @@
|
|||||||
QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) {
|
QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
|
viewer = OpenSeadragon({
|
||||||
|
id: 'example',
|
||||||
|
prefixUrl: '/build/openseadragon/images/',
|
||||||
|
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||||
|
springStiffness: 100, // Faster animation = faster tests
|
||||||
|
drawer: 'test-cache-drawer-async',
|
||||||
|
});
|
||||||
const tileCache = viewer.tileCache;
|
const tileCache = viewer.tileCache;
|
||||||
const drawer = viewer.drawer;
|
const drawer = viewer.drawer;
|
||||||
|
|
||||||
@ -376,7 +444,6 @@
|
|||||||
_currentTestVal = value;
|
_currentTestVal = value;
|
||||||
viewer.world.needsDraw();
|
viewer.world.needsDraw();
|
||||||
viewer.world.draw();
|
viewer.world.draw();
|
||||||
previousTestValue = value;
|
|
||||||
_currentTestVal = undefined;
|
_currentTestVal = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,8 +469,6 @@
|
|||||||
|
|
||||||
viewer.addHandler('tile-invalidated', testHandler);
|
viewer.addHandler('tile-invalidated', testHandler);
|
||||||
await viewer.world.requestInvalidate(true);
|
await viewer.world.requestInvalidate(true);
|
||||||
await sleep(1); // necessary to make space for internal updates
|
|
||||||
testDrawingRoutine(2);
|
|
||||||
|
|
||||||
//test for each level only single cache was processed
|
//test for each level only single cache was processed
|
||||||
const processedLevels = {};
|
const processedLevels = {};
|
||||||
@ -421,37 +486,37 @@
|
|||||||
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache.");
|
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache.");
|
||||||
|
|
||||||
const cache = tile.getCache();
|
const cache = tile.getCache();
|
||||||
test.equal(cache.type, T_C, "Main Cache Updated (suite 1)");
|
test.equal(cache.type, T_A, "Main Cache Converted T_C -> T_A (drawer supports type A) (suite 1)");
|
||||||
test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)");
|
test.equal(cache.data, 3, "Conversion step increases plugin-stored value 2 to 3");
|
||||||
|
|
||||||
const internalCache = cache.getDataForRendering(drawer, tile);
|
const internalCache = cache.getDataForRendering(drawer, tile);
|
||||||
test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E.");
|
test.equal(internalCache.type, viewer.drawer.getId(), "Internal cache has type of the drawer ID.");
|
||||||
test.ok(internalCache.loaded, "Internal cache ready.");
|
test.ok(internalCache.loaded, "Internal cache ready.");
|
||||||
}
|
}
|
||||||
|
// Internal cache will have value 5: main cache is 3, type is T_A,
|
||||||
|
testDrawingRoutine(5); // internal cache transforms to T_C: two steps, TA->TB->TC 3+2
|
||||||
|
|
||||||
// Test that basic scenario with reset data false starts from the main cache data of previous round
|
// Test that basic scenario with reset data false starts from the main cache data of previous round
|
||||||
const modificationConstant = 50;
|
const modificationConstant = 50;
|
||||||
viewer.removeHandler('tile-invalidated', testHandler);
|
viewer.removeHandler('tile-invalidated', testHandler);
|
||||||
testHandler = async e => {
|
testHandler = async e => {
|
||||||
const data = await e.getData(T_B);
|
const data = await e.getData(T_B);
|
||||||
test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened.");
|
test.equal(data, 4, "A -> B conversion happened, we started from value 3 in the main cache.");
|
||||||
await e.setData(data + modificationConstant, T_B);
|
await e.setData(data + modificationConstant, T_B);
|
||||||
console.log(data + modificationConstant);
|
|
||||||
test.notOk(e.outdated(), "Event is still valid.");
|
test.notOk(e.outdated(), "Event is still valid.");
|
||||||
};
|
};
|
||||||
console.log(previousTestValue, modificationConstant)
|
|
||||||
|
|
||||||
viewer.addHandler('tile-invalidated', testHandler);
|
viewer.addHandler('tile-invalidated', testHandler);
|
||||||
await viewer.world.requestInvalidate(false);
|
await viewer.world.requestInvalidate(false);
|
||||||
await sleep(1); // necessary to make space for a draw call
|
|
||||||
// We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round
|
|
||||||
let newValue = previousTestValue + modificationConstant + 3;
|
|
||||||
testDrawingRoutine(newValue);
|
|
||||||
|
|
||||||
newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less
|
// We set data as TB - there is required T_A: T_B -> T_C -> T_A conversion round on the main cache
|
||||||
|
let newValue = modificationConstant + 4 + 2;
|
||||||
|
// and there is still requirement of T_C on internal data, +2 steps
|
||||||
|
testDrawingRoutine(newValue + 2);
|
||||||
|
|
||||||
for (let tile of tileCache._tilesLoaded) {
|
for (let tile of tileCache._tilesLoaded) {
|
||||||
const cache = tile.getCache();
|
const cache = tile.getCache();
|
||||||
test.equal(cache.type, T_B, "Main Cache Updated (suite 2).");
|
test.equal(cache.type, T_A, "Main Cache Updated (suite 2).");
|
||||||
test.equal(cache.data, newValue, "Main Cache Updated (suite 2).");
|
test.equal(cache.data, newValue, "Main Cache Updated (suite 2).");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,14 +550,22 @@
|
|||||||
viewer.removeHandler('tile-invalidated', testHandler);
|
viewer.removeHandler('tile-invalidated', testHandler);
|
||||||
testHandler = async e => {
|
testHandler = async e => {
|
||||||
const data = await e.getData(T_B);
|
const data = await e.getData(T_B);
|
||||||
test.equal(data, 42, "Copy: 41 + 1.");
|
test.equal(data, 44, "Copy: 41 +2 (previous request invalidate ends at T_A) + 1 (we request type B).");
|
||||||
await e.setData(data, T_E);
|
await e.setData(data, T_E); // there is no way to convert T_E -> T_A, this would throw an error
|
||||||
e.resetData();
|
e.resetData(); // reset data will revert to original cache
|
||||||
};
|
};
|
||||||
viewer.addHandler('tile-invalidated', testHandler);
|
viewer.addHandler('tile-invalidated', testHandler);
|
||||||
|
|
||||||
|
// The data will be 45 since no change has been made:
|
||||||
|
// last main cache set was 41 T_B, supported T_A = +2
|
||||||
|
// and internal requirement T_C = +2
|
||||||
|
const checkNotCalled = e => {
|
||||||
|
test.ok(false, "Create data must not be called when there is no change!");
|
||||||
|
};
|
||||||
|
drawer.testEvents.addHandler('create-data', checkNotCalled);
|
||||||
|
|
||||||
await viewer.world.requestInvalidate(false);
|
await viewer.world.requestInvalidate(false);
|
||||||
await sleep(1); // necessary to make space for a draw call
|
testDrawingRoutine(45);
|
||||||
testDrawingRoutine(42);
|
|
||||||
|
|
||||||
for (let tile of tileCache._tilesLoaded) {
|
for (let tile of tileCache._tilesLoaded) {
|
||||||
const origCache = tile.getCache(tile.originalCacheKey);
|
const origCache = tile.getCache(tile.originalCacheKey);
|
||||||
@ -501,6 +574,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.ok(testTileCalled, "Drawer tested at least one tile.");
|
test.ok(testTileCalled, "Drawer tested at least one tile.");
|
||||||
|
viewer.destroy();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
viewer.open([
|
viewer.open([
|
||||||
@ -632,6 +706,14 @@
|
|||||||
QUnit.test('Zombie Cache', function(test) {
|
QUnit.test('Zombie Cache', function(test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
|
viewer = OpenSeadragon({
|
||||||
|
id: 'example',
|
||||||
|
prefixUrl: '/build/openseadragon/images/',
|
||||||
|
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||||
|
springStiffness: 100, // Faster animation = faster tests
|
||||||
|
drawer: 'test-cache-drawer-sync',
|
||||||
|
});
|
||||||
|
|
||||||
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
|
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
|
||||||
let jobCounter = 0, coverage = undefined;
|
let jobCounter = 0, coverage = undefined;
|
||||||
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
||||||
@ -711,6 +793,14 @@
|
|||||||
QUnit.test('Zombie Cache Replace Item', function(test) {
|
QUnit.test('Zombie Cache Replace Item', function(test) {
|
||||||
const done = test.async();
|
const done = test.async();
|
||||||
|
|
||||||
|
viewer = OpenSeadragon({
|
||||||
|
id: 'example',
|
||||||
|
prefixUrl: '/build/openseadragon/images/',
|
||||||
|
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||||
|
springStiffness: 100, // Faster animation = faster tests
|
||||||
|
drawer: 'test-cache-drawer-sync',
|
||||||
|
});
|
||||||
|
|
||||||
let jobCounter = 0, coverage = undefined;
|
let jobCounter = 0, coverage = undefined;
|
||||||
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
||||||
jobCounter++;
|
jobCounter++;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user