mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-26 23:26:10 +03:00
Implement asynchronous tile processing logic wrt. tile cache conversion.
This commit is contained in:
parent
f01a7a4b3c
commit
750d45be81
@ -34,7 +34,10 @@
|
|||||||
|
|
||||||
(function($){
|
(function($){
|
||||||
|
|
||||||
//modified from https://gist.github.com/Prottoy2938/66849e04b0bac459606059f5f9f3aa1a
|
/**
|
||||||
|
* modified from https://gist.github.com/Prottoy2938/66849e04b0bac459606059f5f9f3aa1a
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
class WeightedGraph {
|
class WeightedGraph {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.adjacencyList = {};
|
this.adjacencyList = {};
|
||||||
@ -48,13 +51,12 @@ class WeightedGraph {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
addEdge(vertex1, vertex2, weight, data) {
|
addEdge(vertex1, vertex2, weight, transform) {
|
||||||
this.adjacencyList[vertex1].push({ target: this.vertices[vertex2], weight, data });
|
this.adjacencyList[vertex1].push({ target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {{path: *[], cost: number}|undefined} cheapest path for
|
* @return {{path: *[], cost: number}|undefined} cheapest path for
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
dijkstra(start, finish) {
|
dijkstra(start, finish) {
|
||||||
let path = []; //to return at end
|
let path = []; //to return at end
|
||||||
@ -95,7 +97,7 @@ class WeightedGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!smallestNode._previous) {
|
if (!smallestNode || !smallestNode._previous) {
|
||||||
return undefined; //no path
|
return undefined; //no path
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,17 +121,47 @@ class WeightedGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataTypeConvertor {
|
/**
|
||||||
|
* Node on the conversion path in OpenSeadragon.converter.getConversionPath().
|
||||||
|
* It can be also conversion to undefined if used as destructor implementation.
|
||||||
|
*
|
||||||
|
* @callback TypeConvertor
|
||||||
|
* @memberof OpenSeadragon
|
||||||
|
* @param {?} data data in the input format
|
||||||
|
* @return {?} data in the output format
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node on the conversion path in OpenSeadragon.converter.getConversionPath().
|
||||||
|
*
|
||||||
|
* @typedef {Object} ConversionStep
|
||||||
|
* @memberof OpenSeadragon
|
||||||
|
* @param {OpenSeadragon.PriorityQueue.Node} target - Target node of the conversion step.
|
||||||
|
* Its value is the target format.
|
||||||
|
* @param {OpenSeadragon.PriorityQueue.Node} origin - Origin node of the conversion step.
|
||||||
|
* Its value is the origin format.
|
||||||
|
* @param {number} weight cost of the conversion
|
||||||
|
* @param {TypeConvertor} transform the conversion itself
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that orchestrates automated data types conversion. Do not instantiate
|
||||||
|
* this class, use OpenSeadragon.convertor - a global instance, instead.
|
||||||
|
* @class DataTypeConvertor
|
||||||
|
* @memberOf OpenSeadragon
|
||||||
|
*/
|
||||||
|
$.DataTypeConvertor = class {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.graph = new WeightedGraph();
|
this.graph = new WeightedGraph();
|
||||||
|
this.destructors = {};
|
||||||
|
|
||||||
this.learn("canvas", "string", (canvas) => canvas.toDataURL(), 1, 1);
|
// Teaching OpenSeadragon built-in conversions:
|
||||||
this.learn("image", "string", (image) => image.url);
|
|
||||||
|
this.learn("canvas", "rasterUrl", (canvas) => canvas.toDataURL(), 1, 1);
|
||||||
|
this.learn("image", "rasterUrl", (image) => image.url);
|
||||||
this.learn("canvas", "context2d", (canvas) => canvas.getContext("2d"));
|
this.learn("canvas", "context2d", (canvas) => canvas.getContext("2d"));
|
||||||
this.learn("context2d", "canvas", (context2D) => context2D.canvas);
|
this.learn("context2d", "canvas", (context2D) => context2D.canvas);
|
||||||
|
|
||||||
//OpenSeadragon supports two conversions out of the box: canvas and image.
|
|
||||||
this.learn("image", "canvas", (image) => {
|
this.learn("image", "canvas", (image) => {
|
||||||
const canvas = document.createElement( 'canvas' );
|
const canvas = document.createElement( 'canvas' );
|
||||||
canvas.width = image.width;
|
canvas.width = image.width;
|
||||||
@ -138,20 +170,13 @@ class DataTypeConvertor {
|
|||||||
context.drawImage( image, 0, 0 );
|
context.drawImage( image, 0, 0 );
|
||||||
return canvas;
|
return canvas;
|
||||||
}, 1, 1);
|
}, 1, 1);
|
||||||
|
this.learn("rasterUrl", "image", (url) => {
|
||||||
this.learn("string", "image", (url) => {
|
return new $.Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
img.onerror = img.onabort = reject;
|
||||||
|
img.onload = () => resolve(img);
|
||||||
img.src = url;
|
img.src = url;
|
||||||
//FIXME: support async functions! some function conversions are async (like image here)
|
});
|
||||||
// and returning immediatelly will possibly cause the system work with incomplete data
|
|
||||||
// - a) remove canvas->image conversion path support
|
|
||||||
// - b) busy wait cycle (ugly as..)
|
|
||||||
// - c) async conversion execution (makes the whole cache -> transitively rendering async)
|
|
||||||
// - d) callbacks (makes the cache API more complicated)
|
|
||||||
while (!img.complete) {
|
|
||||||
console.log("Burning through CPU :)");
|
|
||||||
}
|
|
||||||
return img;
|
|
||||||
}, 1, 1);
|
}, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +223,6 @@ class DataTypeConvertor {
|
|||||||
return guessType.nodeName.toLowerCase();
|
return guessType.nodeName.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo consider event...
|
|
||||||
if (guessType === "object") {
|
if (guessType === "object") {
|
||||||
if ($.isFunction(x.getType)) {
|
if ($.isFunction(x.getType)) {
|
||||||
return x.getType();
|
return x.getType();
|
||||||
@ -208,9 +232,12 @@ class DataTypeConvertor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Teach the system to convert data type 'from' -> 'to'
|
||||||
* @param {string} from unique ID of the data item 'from'
|
* @param {string} from unique ID of the data item 'from'
|
||||||
* @param {string} to unique ID of the data item 'to'
|
* @param {string} to unique ID of the data item 'to'
|
||||||
* @param {function} callback convertor that takes type 'from', and converts to type 'to'
|
* @param {OpenSeadragon.TypeConvertor} callback convertor that takes type 'from', and converts to type 'to'.
|
||||||
|
* Callback can return function. This function returns the data in type 'to',
|
||||||
|
* it can return also the value wrapped in a Promise (returned in resolve) or it can be async function.
|
||||||
* @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7.
|
* @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7.
|
||||||
* Should reflect the actual cost of the conversion:
|
* Should reflect the actual cost of the conversion:
|
||||||
* - if nothing must be done and only reference is retrieved (or a constant operation done),
|
* - if nothing must be done and only reference is retrieved (or a constant operation done),
|
||||||
@ -231,78 +258,130 @@ class DataTypeConvertor {
|
|||||||
this.graph.addVertex(from);
|
this.graph.addVertex(from);
|
||||||
this.graph.addVertex(to);
|
this.graph.addVertex(to);
|
||||||
this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
|
this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
|
||||||
this._known = {};
|
this._known = {}; //invalidate precomputed paths :/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIXME: we could convert as 'convert(x, from, ...to)' and get cheapest path to any of the data
|
* Teach the system to destroy data type 'type'
|
||||||
* for example, we could say tile.getCache(key)..getData("image", "canvas") if we do not care what we use and
|
* for example, textures loaded to GPU have to be also manually removed when not needed anymore.
|
||||||
* our system would then choose the cheapest option (both can be rendered by html for example).
|
* Needs to be defined only when the created object has extra deletion process.
|
||||||
*
|
* @param {string} type
|
||||||
* FIXME: conversion should be allowed to await results (e.g. image creation), now it is buggy,
|
* @param {OpenSeadragon.TypeConvertor} callback destructor, receives the object created,
|
||||||
* because we do not await image creation...
|
* it is basically a type conversion to 'undefined' - thus the type.
|
||||||
*
|
*/
|
||||||
|
learnDestroy(type, callback) {
|
||||||
|
this.destructors[type] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert data item x of type 'from' to any of the 'to' types, chosen is the cheapest known conversion.
|
||||||
|
* Data is destroyed upon conversion. For different behavior, implement your conversion using the
|
||||||
|
* path rules obtained from getConversionPath().
|
||||||
* @param {*} x data item to convert
|
* @param {*} x data item to convert
|
||||||
* @param {string} from data item type
|
* @param {string} from data item type
|
||||||
* @param {string} to desired type
|
* @param {string} to desired type(s)
|
||||||
* @return {*} data item with type 'to', or undefined if the conversion failed
|
* @return {OpenSeadragon.Promise<?>} promise resolution with type 'to' or undefined if the conversion failed
|
||||||
*/
|
*/
|
||||||
convert(x, from, to) {
|
convert(x, from, ...to) {
|
||||||
const conversionPath = this.getConversionPath(from, to);
|
const conversionPath = this.getConversionPath(from, to);
|
||||||
|
|
||||||
if (!conversionPath) {
|
if (!conversionPath) {
|
||||||
$.console.warn(`[DataTypeConvertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
|
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
|
||||||
return undefined;
|
return $.Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let node of conversionPath) {
|
const stepCount = conversionPath.length,
|
||||||
x = node.data(x);
|
_this = this;
|
||||||
if (!x) {
|
const step = (x, i) => {
|
||||||
$.console.warn(`[DataTypeConvertor.convert] data mid result falsey value (conversion to ${node.node})`);
|
if (i >= stepCount) {
|
||||||
return undefined;
|
return $.Promise.resolve(x);
|
||||||
}
|
}
|
||||||
|
let edge = conversionPath[i];
|
||||||
|
let y = edge.transform(x);
|
||||||
|
if (!y) {
|
||||||
|
$.console.warn(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting to %s)`, edge.target);
|
||||||
|
return $.Promise.resolve();
|
||||||
|
}
|
||||||
|
//node.value holds the type string
|
||||||
|
_this.destroy(edge.origin.value, x);
|
||||||
|
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
|
||||||
|
return result.then(res => step(res, i + 1));
|
||||||
|
};
|
||||||
|
return step(x, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the data item given.
|
||||||
|
* @param {string} type data type
|
||||||
|
* @param {?} data
|
||||||
|
*/
|
||||||
|
destroy(type, data) {
|
||||||
|
const destructor = this.destructors[type];
|
||||||
|
if (destructor) {
|
||||||
|
destructor(data);
|
||||||
}
|
}
|
||||||
return x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get possible system type conversions and cache result.
|
* Get possible system type conversions and cache result.
|
||||||
* @param {string} from data item type
|
* @param {string} from data item type
|
||||||
* @param {string} to desired type
|
* @param {string|string[]} to array of accepted types
|
||||||
* @return {[object]|undefined} array of required conversions (returns empty array
|
* @return {[ConversionStep]|undefined} array of required conversions (returns empty array
|
||||||
* for from===to), or undefined if the system cannot convert between given types.
|
* for from===to), or undefined if the system cannot convert between given types.
|
||||||
|
* Each object has 'transform' function that converts between neighbouring types, such
|
||||||
|
* that x = arr[i].transform(x) is valid input for convertor arr[i+1].transform(), e.g.
|
||||||
|
* arr[i+1].transform(arr[i].transform( ... )) is a valid conversion procedure.
|
||||||
|
*
|
||||||
|
* Note: if a function is returned, it is a callback called once the data is ready.
|
||||||
*/
|
*/
|
||||||
getConversionPath(from, ...to) {
|
getConversionPath(from, to) {
|
||||||
$.console.assert(to.length > 0, "[getConversionPath] conversion 'to' type must be defined.");
|
let bestConvertorPath, selectedType;
|
||||||
|
let knownFrom = this._known[from];
|
||||||
|
if (!knownFrom) {
|
||||||
|
this._known[from] = knownFrom = {};
|
||||||
|
}
|
||||||
|
|
||||||
let bestConvertorPath;
|
if (Array.isArray(to)) {
|
||||||
const knownFrom = this._known[from];
|
$.console.assert(typeof to === "string" || to.length > 0, "[getConversionPath] conversion 'to' type must be defined.");
|
||||||
if (knownFrom) {
|
|
||||||
let bestCost = Infinity;
|
let bestCost = Infinity;
|
||||||
|
|
||||||
|
//FIXME: pre-compute all paths in 'to' array? could be efficient for multiple
|
||||||
|
// type system, but overhead for simple use cases... now we just use the first type if costs unknown
|
||||||
|
selectedType = to[0];
|
||||||
|
|
||||||
for (const outType of to) {
|
for (const outType of to) {
|
||||||
const conversion = knownFrom[outType];
|
const conversion = knownFrom[outType];
|
||||||
if (conversion && bestCost > conversion.cost) {
|
if (conversion && bestCost > conversion.cost) {
|
||||||
bestConvertorPath = conversion;
|
bestConvertorPath = conversion;
|
||||||
bestCost = conversion.cost;
|
bestCost = conversion.cost;
|
||||||
|
selectedType = outType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._known[from] = {};
|
$.console.assert(typeof to === "string", "[getConversionPath] conversion 'to' type must be defined.");
|
||||||
|
bestConvertorPath = knownFrom[to];
|
||||||
|
selectedType = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bestConvertorPath) {
|
if (!bestConvertorPath) {
|
||||||
//FIXME: pre-compute all paths? could be efficient for multiple
|
bestConvertorPath = this.graph.dijkstra(from, selectedType);
|
||||||
// type system, but overhead for simple use cases...
|
this._known[from][selectedType] = bestConvertorPath;
|
||||||
bestConvertorPath = this.graph.dijkstra(from, to[0]);
|
|
||||||
this._known[from][to[0]] = bestConvertorPath;
|
|
||||||
}
|
}
|
||||||
return bestConvertorPath ? bestConvertorPath.path : undefined;
|
return bestConvertorPath ? bestConvertorPath.path : undefined;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static convertor available throughout OpenSeadragon
|
* Static convertor available throughout OpenSeadragon.
|
||||||
|
*
|
||||||
|
* Built-in conversions include types:
|
||||||
|
* - context2d canvas 2d context
|
||||||
|
* - image HTMLImage element
|
||||||
|
* - rasterUrl url string carrying or pointing to 2D raster data
|
||||||
|
* - canvas HTMLCanvas element
|
||||||
|
*
|
||||||
|
* @type OpenSeadragon.DataTypeConvertor
|
||||||
* @memberOf OpenSeadragon
|
* @memberOf OpenSeadragon
|
||||||
*/
|
*/
|
||||||
$.convertor = new DataTypeConvertor();
|
$.convertor = new $.DataTypeConvertor();
|
||||||
|
|
||||||
}(OpenSeadragon));
|
}(OpenSeadragon));
|
||||||
|
@ -70,10 +70,10 @@ $.EventSource.prototype = {
|
|||||||
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
|
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
|
||||||
*/
|
*/
|
||||||
addOnceHandler: function(eventName, handler, userData, times, priority) {
|
addOnceHandler: function(eventName, handler, userData, times, priority) {
|
||||||
var self = this;
|
const self = this;
|
||||||
times = times || 1;
|
times = times || 1;
|
||||||
var count = 0;
|
let count = 0;
|
||||||
var onceHandler = function(event) {
|
const onceHandler = function(event) {
|
||||||
count++;
|
count++;
|
||||||
if (count === times) {
|
if (count === times) {
|
||||||
self.removeHandler(eventName, onceHandler);
|
self.removeHandler(eventName, onceHandler);
|
||||||
@ -92,12 +92,12 @@ $.EventSource.prototype = {
|
|||||||
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
|
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
|
||||||
*/
|
*/
|
||||||
addHandler: function ( eventName, handler, userData, priority ) {
|
addHandler: function ( eventName, handler, userData, priority ) {
|
||||||
var events = this.events[ eventName ];
|
let events = this.events[ eventName ];
|
||||||
if ( !events ) {
|
if ( !events ) {
|
||||||
this.events[ eventName ] = events = [];
|
this.events[ eventName ] = events = [];
|
||||||
}
|
}
|
||||||
if ( handler && $.isFunction( handler ) ) {
|
if ( handler && $.isFunction( handler ) ) {
|
||||||
var index = events.length,
|
let index = events.length,
|
||||||
event = { handler: handler, userData: userData || null, priority: priority || 0 };
|
event = { handler: handler, userData: userData || null, priority: priority || 0 };
|
||||||
events[ index ] = event;
|
events[ index ] = event;
|
||||||
while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
|
while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
|
||||||
@ -115,14 +115,13 @@ $.EventSource.prototype = {
|
|||||||
* @param {OpenSeadragon.EventHandler} handler - Function to be removed.
|
* @param {OpenSeadragon.EventHandler} handler - Function to be removed.
|
||||||
*/
|
*/
|
||||||
removeHandler: function ( eventName, handler ) {
|
removeHandler: function ( eventName, handler ) {
|
||||||
var events = this.events[ eventName ],
|
const events = this.events[ eventName ],
|
||||||
handlers = [],
|
handlers = [];
|
||||||
i;
|
|
||||||
if ( !events ) {
|
if ( !events ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ( $.isArray( events ) ) {
|
if ( $.isArray( events ) ) {
|
||||||
for ( i = 0; i < events.length; i++ ) {
|
for ( let i = 0; i < events.length; i++ ) {
|
||||||
if ( events[i].handler !== handler ) {
|
if ( events[i].handler !== handler ) {
|
||||||
handlers.push( events[ i ] );
|
handlers.push( events[ i ] );
|
||||||
}
|
}
|
||||||
@ -137,7 +136,7 @@ $.EventSource.prototype = {
|
|||||||
* @returns {number} amount of events
|
* @returns {number} amount of events
|
||||||
*/
|
*/
|
||||||
numberOfHandlers: function (eventName) {
|
numberOfHandlers: function (eventName) {
|
||||||
var events = this.events[ eventName ];
|
const events = this.events[ eventName ];
|
||||||
if ( !events ) {
|
if ( !events ) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -154,7 +153,7 @@ $.EventSource.prototype = {
|
|||||||
if ( eventName ){
|
if ( eventName ){
|
||||||
this.events[ eventName ] = [];
|
this.events[ eventName ] = [];
|
||||||
} else{
|
} else{
|
||||||
for ( var eventType in this.events ) {
|
for ( let eventType in this.events ) {
|
||||||
this.events[ eventType ] = [];
|
this.events[ eventType ] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +165,7 @@ $.EventSource.prototype = {
|
|||||||
* @param {String} eventName - Name of event to get handlers for.
|
* @param {String} eventName - Name of event to get handlers for.
|
||||||
*/
|
*/
|
||||||
getHandler: function ( eventName) {
|
getHandler: function ( eventName) {
|
||||||
var events = this.events[ eventName ];
|
let events = this.events[ eventName ];
|
||||||
if ( !events || !events.length ) {
|
if ( !events || !events.length ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -174,9 +173,8 @@ $.EventSource.prototype = {
|
|||||||
[ events[ 0 ] ] :
|
[ events[ 0 ] ] :
|
||||||
Array.apply( null, events );
|
Array.apply( null, events );
|
||||||
return function ( source, args ) {
|
return function ( source, args ) {
|
||||||
var i,
|
let length = events.length;
|
||||||
length = events.length;
|
for ( let i = 0; i < length; i++ ) {
|
||||||
for ( i = 0; i < length; i++ ) {
|
|
||||||
if ( events[ i ] ) {
|
if ( events[ i ] ) {
|
||||||
args.eventSource = source;
|
args.eventSource = source;
|
||||||
args.userData = events[ i ].userData;
|
args.userData = events[ i ].userData;
|
||||||
@ -186,6 +184,43 @@ $.EventSource.prototype = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a function which iterates the list of all handlers registered for a given event,
|
||||||
|
* calling the handler for each and awaiting async ones.
|
||||||
|
* @function
|
||||||
|
* @param {String} eventName - Name of event to get handlers for.
|
||||||
|
*/
|
||||||
|
getAwaitingHandler: function ( eventName) {
|
||||||
|
let events = this.events[ eventName ];
|
||||||
|
if ( !events || !events.length ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
events = events.length === 1 ?
|
||||||
|
[ events[ 0 ] ] :
|
||||||
|
Array.apply( null, events );
|
||||||
|
|
||||||
|
return function ( source, args ) {
|
||||||
|
// We return a promise that gets resolved after all the events finish.
|
||||||
|
// Returning loop result is not correct, loop promises chain dynamically
|
||||||
|
// and outer code could process finishing logics in the middle of event loop.
|
||||||
|
return new $.Promise(resolve => {
|
||||||
|
const length = events.length;
|
||||||
|
function loop(index) {
|
||||||
|
if ( index >= length || !events[ index ] ) {
|
||||||
|
resolve("Resolved!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
args.eventSource = source;
|
||||||
|
args.userData = events[ index ].userData;
|
||||||
|
let result = events[ index ].handler( args );
|
||||||
|
result = (!result || $.type(result) !== "promise") ? $.Promise.resolve() : result;
|
||||||
|
return result.then(() => loop(index + 1));
|
||||||
|
}
|
||||||
|
loop(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger an event, optionally passing additional information.
|
* Trigger an event, optionally passing additional information.
|
||||||
* @function
|
* @function
|
||||||
@ -194,13 +229,31 @@ $.EventSource.prototype = {
|
|||||||
*/
|
*/
|
||||||
raiseEvent: function( eventName, eventArgs ) {
|
raiseEvent: function( eventName, eventArgs ) {
|
||||||
//uncomment if you want to get a log of all events
|
//uncomment if you want to get a log of all events
|
||||||
//$.console.log( eventName );
|
//$.console.log( "Event fired:", eventName );
|
||||||
|
|
||||||
var handler = this.getHandler( eventName );
|
const handler = this.getHandler( eventName );
|
||||||
if ( handler ) {
|
if ( handler ) {
|
||||||
return handler( this, eventArgs || {} );
|
return handler( this, eventArgs || {} );
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger an event, optionally passing additional information.
|
||||||
|
* This events awaits every asynchronous or promise-returning function.
|
||||||
|
* @param {String} eventName - Name of event to register.
|
||||||
|
* @param {Object} eventArgs - Event-specific data.
|
||||||
|
* @return {OpenSeadragon.Promise|undefined} - Promise resolved upon the event completion.
|
||||||
|
*/
|
||||||
|
raiseEventAwaiting: function ( eventName, eventArgs ) {
|
||||||
|
//uncomment if you want to get a log of all events
|
||||||
|
//$.console.log( "Awaiting event fired:", eventName );
|
||||||
|
|
||||||
|
const awaitingHandler = this.getAwaitingHandler( eventName );
|
||||||
|
if ( awaitingHandler ) {
|
||||||
|
return awaitingHandler( this, eventArgs || {} );
|
||||||
|
}
|
||||||
|
return $.Promise.resolve("No handler for this event registered.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2886,6 +2886,30 @@ function OpenSeadragon( options ){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise proxy in OpenSeadragon, can be removed once IE11 support is dropped
|
||||||
|
* @type {PromiseConstructor}
|
||||||
|
*/
|
||||||
|
$.Promise = (function () {
|
||||||
|
if (window.Promise) {
|
||||||
|
return window.Promise;
|
||||||
|
}
|
||||||
|
const promise = function () {};
|
||||||
|
//TODO consider supplying promise API via callbacks/polyfill
|
||||||
|
promise.prototype.then = function () {
|
||||||
|
throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
|
||||||
|
};
|
||||||
|
promise.prototype.resolve = function () {
|
||||||
|
throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
|
||||||
|
};
|
||||||
|
promise.prototype.reject = function () {
|
||||||
|
throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
|
||||||
|
};
|
||||||
|
promise.prototype.finally = function () {
|
||||||
|
throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
|
||||||
|
};
|
||||||
|
return promise;
|
||||||
|
})();
|
||||||
}(OpenSeadragon));
|
}(OpenSeadragon));
|
||||||
|
|
||||||
|
|
||||||
|
27
src/tile.js
27
src/tile.js
@ -144,6 +144,15 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
|||||||
* @memberof OpenSeadragon.Tile#
|
* @memberof OpenSeadragon.Tile#
|
||||||
*/
|
*/
|
||||||
this.cacheKey = cacheKey;
|
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'.
|
||||||
|
* @member {String} originalCacheKey
|
||||||
|
* @memberof OpenSeadragon.Tile#
|
||||||
|
*/
|
||||||
|
this.originalCacheKey = this.cacheKey;
|
||||||
/**
|
/**
|
||||||
* Is this tile loaded?
|
* Is this tile loaded?
|
||||||
* @member {Boolean} loaded
|
* @member {Boolean} loaded
|
||||||
@ -441,14 +450,19 @@ $.Tile.prototype = {
|
|||||||
if (!cache) {
|
if (!cache) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return cache.getData(type);
|
cache.getData(type); //returns a promise
|
||||||
|
//we return the data synchronously immediatelly (undefined if conversion happens)
|
||||||
|
return cache.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate the tile so that viewport gets updated.
|
* Invalidate the tile so that viewport gets updated.
|
||||||
*/
|
*/
|
||||||
save() {
|
save() {
|
||||||
this._needsDraw = true;
|
const parent = this.tiledImage;
|
||||||
|
if (parent) {
|
||||||
|
parent._needsDraw = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -500,6 +514,15 @@ $.Tile.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME:refactor
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
dataReady() {
|
||||||
|
return this.getCache(this.cacheKey).loaded;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the tile in a canvas-based context.
|
* Renders the tile in a canvas-based context.
|
||||||
* @function
|
* @function
|
||||||
|
352
src/tilecache.js
352
src/tilecache.js
@ -37,21 +37,50 @@
|
|||||||
/**
|
/**
|
||||||
* Cached Data Record, the cache object.
|
* Cached Data Record, the cache object.
|
||||||
* Keeps only latest object type required.
|
* Keeps only latest object type required.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Furthermore, it has a 'getData' function that returns a promise resolving
|
||||||
|
* with the value on the desired type passed to the function.
|
||||||
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* getImage: function,
|
* destroy: function,
|
||||||
|
* save: function,
|
||||||
* getData: function,
|
* getData: function,
|
||||||
* getRenderedContext: function
|
* data: ?,
|
||||||
|
* loaded: boolean
|
||||||
* }} OpenSeadragon.CacheRecord
|
* }} OpenSeadragon.CacheRecord
|
||||||
*/
|
*/
|
||||||
$.CacheRecord = class {
|
$.CacheRecord = class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._tiles = [];
|
this._tiles = [];
|
||||||
|
this._data = null;
|
||||||
|
this.loaded = false;
|
||||||
|
this._promise = $.Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this._tiles = null;
|
this._tiles = null;
|
||||||
this._data = null;
|
this._data = null;
|
||||||
this._type = null;
|
this._type = null;
|
||||||
|
this.loaded = false;
|
||||||
|
//make sure this gets destroyed even if loaded=false
|
||||||
|
if (this.loaded) {
|
||||||
|
$.convertor.destroy(this._type, this._data);
|
||||||
|
} else {
|
||||||
|
this._promise.then(x => $.convertor.destroy(this._type, x));
|
||||||
|
}
|
||||||
|
this._promise = $.Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this._type;
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
@ -60,48 +89,46 @@ $.CacheRecord = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get data() {
|
|
||||||
$.console.warn("[CacheRecord.data] is deprecated property. Use getData(...) instead!");
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
set data(value) {
|
|
||||||
//FIXME: addTile bit bad name, related to the issue mentioned elsewhere
|
|
||||||
$.console.warn("[CacheRecord.data] is deprecated property. Use addTile(...) instead!");
|
|
||||||
this._data = value;
|
|
||||||
this._type = $.convertor.guessType(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getImage() {
|
|
||||||
return this.getData("image");
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderedContext() {
|
|
||||||
return this.getData("context2d");
|
|
||||||
}
|
|
||||||
|
|
||||||
getData(type = this._type) {
|
getData(type = this._type) {
|
||||||
if (type !== this._type) {
|
if (type !== this._type) {
|
||||||
this._data = $.convertor.convert(this._data, this._type, type);
|
if (!this.loaded) {
|
||||||
this._type = type;
|
$.console.warn("Attempt to call getData with desired type %s, the tile data type is %s and the tile is not loaded!", type, this._type);
|
||||||
|
return this._promise;
|
||||||
}
|
}
|
||||||
return this._data;
|
this._convert(this._type, type);
|
||||||
|
}
|
||||||
|
return this._promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tile dependency on this record
|
||||||
|
* @param tile
|
||||||
|
* @param data
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
addTile(tile, data, type) {
|
addTile(tile, data, type) {
|
||||||
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||||
|
|
||||||
//allow overriding the cache - existing tile or different type
|
//allow overriding the cache - existing tile or different type
|
||||||
if (this._tiles.includes(tile)) {
|
if (this._tiles.includes(tile)) {
|
||||||
this.removeTile(tile);
|
this.removeTile(tile);
|
||||||
} else if (!this._type !== type) {
|
|
||||||
|
} else if (!this.loaded) {
|
||||||
this._type = type;
|
this._type = type;
|
||||||
|
this._promise = $.Promise.resolve(data);
|
||||||
this._data = data;
|
this._data = data;
|
||||||
|
this.loaded = true;
|
||||||
|
} else if (this._type !== type) {
|
||||||
|
$.console.warn("[CacheRecord.addTile] Tile %s was added to an existing cache, but the tile is supposed to carry incompatible data type %s!", tile, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tiles.push(tile);
|
this._tiles.push(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove tile dependency on this record.
|
||||||
|
* @param tile
|
||||||
|
*/
|
||||||
removeTile(tile) {
|
removeTile(tile) {
|
||||||
for (let i = 0; i < this._tiles.length; i++) {
|
for (let i = 0; i < this._tiles.length; i++) {
|
||||||
if (this._tiles[i] === tile) {
|
if (this._tiles[i] === tile) {
|
||||||
@ -113,123 +140,178 @@ $.CacheRecord = class {
|
|||||||
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amount of tiles sharing this record.
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
getTileCount() {
|
getTileCount() {
|
||||||
return this._tiles.length;
|
return this._tiles.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private conversion that makes sure the cache knows its data is ready
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_convert(from, to) {
|
||||||
|
const convertor = $.convertor,
|
||||||
|
conversionPath = convertor.getConversionPath(from, to);
|
||||||
|
if (!conversionPath) {
|
||||||
|
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
|
||||||
|
return; //no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalData = this._data,
|
||||||
|
stepCount = conversionPath.length,
|
||||||
|
_this = this,
|
||||||
|
convert = (x, i) => {
|
||||||
|
if (i >= stepCount) {
|
||||||
|
_this._data = x;
|
||||||
|
_this.loaded = true;
|
||||||
|
return $.Promise.resolve(x);
|
||||||
|
}
|
||||||
|
let edge = conversionPath[i];
|
||||||
|
return $.Promise.resolve(edge.transform(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;
|
||||||
|
}
|
||||||
|
//node.value holds the type string
|
||||||
|
convertor.destroy(edge.origin.value, x);
|
||||||
|
return convert(y, i + 1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loaded = false;
|
||||||
|
this._data = undefined;
|
||||||
|
this._type = to;
|
||||||
|
this._promise = convert(originalData, 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//FIXME: really implement or throw away? new parameter would allow users to
|
//FIXME: really implement or throw away? new parameter would allow users to
|
||||||
// use this implementation isntead of the above to allow caching for old data
|
// use this implementation instead of the above to allow caching for old data
|
||||||
// (for example in the default use, the data is downloaded as an image, and
|
// (for example in the default use, the data is downloaded as an image, and
|
||||||
// converted to a canvas -> the image record gets thrown away)
|
// converted to a canvas -> the image record gets thrown away)
|
||||||
$.MemoryCacheRecord = class extends $.CacheRecord {
|
//
|
||||||
constructor(memorySize) {
|
//FIXME: Note that this can be also achieved somewhat by caching the midresults
|
||||||
super();
|
// as a single cache object instead. Also, there is the problem of lifecycle-oriented
|
||||||
this.length = memorySize;
|
// data types such as WebGL textures we want to unload manually: this looks like
|
||||||
this.index = 0;
|
// we really want to cache midresuls and have their custom destructors
|
||||||
this.content = [];
|
// $.MemoryCacheRecord = class extends $.CacheRecord {
|
||||||
this.types = [];
|
// constructor(memorySize) {
|
||||||
this.defaultType = "image";
|
// super();
|
||||||
}
|
// this.length = memorySize;
|
||||||
|
// this.index = 0;
|
||||||
// overrides:
|
// this.content = [];
|
||||||
|
// this.types = [];
|
||||||
destroy() {
|
// this.defaultType = "image";
|
||||||
super.destroy();
|
// }
|
||||||
this.types = null;
|
//
|
||||||
this.content = null;
|
// // overrides:
|
||||||
this.types = null;
|
//
|
||||||
this.defaultType = null;
|
// destroy() {
|
||||||
}
|
// super.destroy();
|
||||||
|
// this.types = null;
|
||||||
getData(type = this.defaultType) {
|
// this.content = null;
|
||||||
let item = this.add(type, undefined);
|
// this.types = null;
|
||||||
if (item === undefined) {
|
// this.defaultType = null;
|
||||||
//no such type available, get if possible
|
// }
|
||||||
//todo: possible unomptimal use, we could cache costs and re-use known paths, though it adds overhead...
|
//
|
||||||
item = $.convertor.convert(this.current(), this.currentType(), type);
|
// getData(type = this.defaultType) {
|
||||||
this.add(type, item);
|
// let item = this.add(type, undefined);
|
||||||
}
|
// if (item === undefined) {
|
||||||
return item;
|
// //no such type available, get if possible
|
||||||
}
|
// //todo: possible unomptimal use, we could cache costs and re-use known paths, though it adds overhead...
|
||||||
|
// item = $.convertor.convert(this.current(), this.currentType(), type);
|
||||||
/**
|
// this.add(type, item);
|
||||||
* @deprecated
|
// }
|
||||||
*/
|
// return item;
|
||||||
get data() {
|
// }
|
||||||
$.console.warn("[MemoryCacheRecord.data] is deprecated property. Use getData(...) instead!");
|
//
|
||||||
return this.current();
|
// /**
|
||||||
}
|
// * @deprecated
|
||||||
|
// */
|
||||||
/**
|
// get data() {
|
||||||
* @deprecated
|
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use getData(...) instead!");
|
||||||
* @param value
|
// return this.current();
|
||||||
*/
|
// }
|
||||||
set data(value) {
|
//
|
||||||
//FIXME: addTile bit bad name, related to the issue mentioned elsewhere
|
// /**
|
||||||
$.console.warn("[MemoryCacheRecord.data] is deprecated property. Use addTile(...) instead!");
|
// * @deprecated
|
||||||
this.defaultType = $.convertor.guessType(value);
|
// * @param value
|
||||||
this.add(this.defaultType, value);
|
// */
|
||||||
}
|
// set data(value) {
|
||||||
|
// //FIXME: addTile bit bad name, related to the issue mentioned elsewhere
|
||||||
addTile(tile, data, type) {
|
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use addTile(...) instead!");
|
||||||
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
// this.defaultType = $.convertor.guessType(value);
|
||||||
|
// this.add(this.defaultType, value);
|
||||||
//allow overriding the cache - existing tile or different type
|
// }
|
||||||
if (this._tiles.includes(tile)) {
|
//
|
||||||
this.removeTile(tile);
|
// addTile(tile, data, type) {
|
||||||
} else if (!this.defaultType !== type) {
|
// $.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||||
this.defaultType = type;
|
//
|
||||||
this.add(type, data);
|
// //allow overriding the cache - existing tile or different type
|
||||||
}
|
// if (this._tiles.includes(tile)) {
|
||||||
|
// this.removeTile(tile);
|
||||||
this._tiles.push(tile);
|
// } else if (!this.defaultType !== type) {
|
||||||
}
|
// this.defaultType = type;
|
||||||
|
// this.add(type, data);
|
||||||
// extends:
|
// }
|
||||||
|
//
|
||||||
add(type, item) {
|
// this._tiles.push(tile);
|
||||||
const index = this.hasIndex(type);
|
// }
|
||||||
if (index > -1) {
|
//
|
||||||
//no index change, swap (optimally, move all by one - too expensive...)
|
// // extends:
|
||||||
item = this.content[index];
|
//
|
||||||
this.content[index] = this.content[this.index];
|
// add(type, item) {
|
||||||
} else {
|
// const index = this.hasIndex(type);
|
||||||
this.index = (this.index + 1) % this.length;
|
// if (index > -1) {
|
||||||
}
|
// //no index change, swap (optimally, move all by one - too expensive...)
|
||||||
this.content[this.index] = item;
|
// item = this.content[index];
|
||||||
this.types[this.index] = type;
|
// this.content[index] = this.content[this.index];
|
||||||
return item;
|
// } else {
|
||||||
}
|
// this.index = (this.index + 1) % this.length;
|
||||||
|
// }
|
||||||
has(type) {
|
// this.content[this.index] = item;
|
||||||
for (let i = 0; i < this.types.length; i++) {
|
// this.types[this.index] = type;
|
||||||
const t = this.types[i];
|
// return item;
|
||||||
if (t === type) {
|
// }
|
||||||
return this.content[i];
|
//
|
||||||
}
|
// has(type) {
|
||||||
}
|
// for (let i = 0; i < this.types.length; i++) {
|
||||||
return undefined;
|
// const t = this.types[i];
|
||||||
}
|
// if (t === type) {
|
||||||
|
// return this.content[i];
|
||||||
hasIndex(type) {
|
// }
|
||||||
for (let i = 0; i < this.types.length; i++) {
|
// }
|
||||||
const t = this.types[i];
|
// return undefined;
|
||||||
if (t === type) {
|
// }
|
||||||
return i;
|
//
|
||||||
}
|
// hasIndex(type) {
|
||||||
}
|
// for (let i = 0; i < this.types.length; i++) {
|
||||||
return -1;
|
// const t = this.types[i];
|
||||||
}
|
// if (t === type) {
|
||||||
|
// return i;
|
||||||
current() {
|
// }
|
||||||
return this.content[this.index];
|
// }
|
||||||
}
|
// return -1;
|
||||||
|
// }
|
||||||
currentType() {
|
//
|
||||||
return this.types[this.index];
|
// current() {
|
||||||
}
|
// return this.content[this.index];
|
||||||
};
|
// }
|
||||||
|
//
|
||||||
|
// currentType() {
|
||||||
|
// return this.types[this.index];
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class TileCache
|
* @class TileCache
|
||||||
|
@ -1530,15 +1530,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
levelVisibility
|
levelVisibility
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!tile.loaded) {
|
if (!tile.loaded && !tile.loading) {
|
||||||
// Tile was created or its data removed: check whether cache has the data before downloading.
|
// Tile was created or its data removed: check whether cache has the data before downloading.
|
||||||
if (!tile.cacheKey) {
|
if (!tile.cacheKey) {
|
||||||
tile.cacheKey = "";
|
tile.cacheKey = "";
|
||||||
this._setTileLoaded(tile, null);
|
tile.originalCacheKey = "";
|
||||||
|
}
|
||||||
|
const similarCacheRecord =
|
||||||
|
this._tileCache.getCacheRecord(tile.originalCacheKey) ||
|
||||||
|
this._tileCache.getCacheRecord(tile.cacheKey);
|
||||||
|
|
||||||
|
if (similarCacheRecord) {
|
||||||
|
const cutoff = this.source.getClosestLevel();
|
||||||
|
if (similarCacheRecord.loaded) {
|
||||||
|
this._setTileLoaded(tile, similarCacheRecord.data, cutoff, null, similarCacheRecord.type);
|
||||||
} else {
|
} else {
|
||||||
const imageRecord = this._tileCache.getCacheRecord(tile.cacheKey);
|
similarCacheRecord.getData().then(data =>
|
||||||
if (imageRecord) {
|
this._setTileLoaded(tile, data, cutoff, null, similarCacheRecord.type));
|
||||||
this._setTileLoaded(tile, imageRecord.getData());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1762,80 +1770,94 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
* @private
|
* @private
|
||||||
* @inner
|
* @inner
|
||||||
* @param {OpenSeadragon.Tile} tile
|
* @param {OpenSeadragon.Tile} tile
|
||||||
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
|
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object,
|
||||||
|
* can be null: in that case, cache is assigned to a tile without further processing
|
||||||
* @param {?Number} cutoff
|
* @param {?Number} cutoff
|
||||||
* @param {?XMLHttpRequest} tileRequest
|
* @param {?XMLHttpRequest} tileRequest
|
||||||
* @param {?String} [dataType=undefined] data type, derived automatically if not set
|
* @param {?String} [dataType=undefined] data type, derived automatically if not set
|
||||||
*/
|
*/
|
||||||
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) {
|
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) {
|
||||||
var increment = 0,
|
|
||||||
eventFinished = false,
|
|
||||||
_this = this;
|
|
||||||
|
|
||||||
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
|
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
|
||||||
// -> reason why it is not in the constructor
|
// -> reason why it is not in the constructor
|
||||||
tile.setCache(tile.cacheKey, data, dataType, false, cutoff);
|
tile.setCache(tile.cacheKey, data, dataType, false, cutoff);
|
||||||
|
|
||||||
function getCompletionCallback() {
|
let resolver = null;
|
||||||
if (eventFinished) {
|
const _this = this,
|
||||||
$.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " +
|
finishPromise = new $.Promise(r => {
|
||||||
"Its return value should be called asynchronously.");
|
resolver = r;
|
||||||
}
|
});
|
||||||
increment++;
|
|
||||||
return completionCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function completionCallback() {
|
function completionCallback() {
|
||||||
increment--;
|
|
||||||
if (increment === 0) {
|
|
||||||
tile.loading = false;
|
|
||||||
tile.loaded = true;
|
|
||||||
//do not override true if set (false is default)
|
//do not override true if set (false is default)
|
||||||
tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency(
|
tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency(
|
||||||
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData
|
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData
|
||||||
);
|
);
|
||||||
|
//make sure cache data is ready for drawing, if not, request the desired format
|
||||||
|
const cache = tile.getCache(tile.cacheKey),
|
||||||
|
// TODO: dynamic type declaration from the drawer base class interface from v5.0 onwards
|
||||||
|
requiredType = _this._drawer.useCanvas ? "context2d" : "image";
|
||||||
|
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);
|
||||||
|
resolver(tile);
|
||||||
|
} else if (cache.type !== requiredType) {
|
||||||
|
//initiate conversion as soon as possible if incompatible with the drawer
|
||||||
|
cache.getData(requiredType).then(_ => {
|
||||||
|
tile.loading = false;
|
||||||
|
tile.loaded = true;
|
||||||
|
resolver(tile);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tile.loading = false;
|
||||||
|
tile.loaded = true;
|
||||||
|
resolver(tile);
|
||||||
|
}
|
||||||
|
|
||||||
//FIXME: design choice: cache tile now set automatically so users can do
|
//FIXME: design choice: cache tile now set automatically so users can do
|
||||||
// tile.getCache(...) inside this event, but maybe we would like to have users
|
// tile.getCache(...) inside this event, but maybe we would like to have users
|
||||||
// freedom to decide on the cache creation (note, tiles now MUST have cache, e.g.
|
// freedom to decide on the cache creation (note, tiles now MUST have cache, e.g.
|
||||||
// it is no longer possible to store all tiles in the memory as it was with context2D prop)
|
// it is no longer possible to store all tiles in the memory as it was with context2D prop)
|
||||||
tile.save();
|
tile.save();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackCompletion = getCompletionCallback();
|
|
||||||
/**
|
/**
|
||||||
* Triggered when a tile has just been loaded in memory. That means that the
|
* Triggered when a tile has just been loaded in memory. That means that the
|
||||||
* image has been downloaded and can be modified before being drawn to the canvas.
|
* image has been downloaded and can be modified before being drawn to the canvas.
|
||||||
|
* This event awaits its handlers - they can return promises, or be async functions.
|
||||||
*
|
*
|
||||||
* @event tile-loaded
|
* @event tile-loaded awaiting event
|
||||||
* @memberof OpenSeadragon.Viewer
|
* @memberof OpenSeadragon.Viewer
|
||||||
* @type {object}
|
* @type {object}
|
||||||
* @property {Image|*} image - The image (data) of the tile. Deprecated.
|
* @property {Image|*} image - The image (data) of the tile. Deprecated.
|
||||||
* @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
|
* @property {*} data image data, the data sent to ImageJob.prototype.finish(),
|
||||||
|
* by default an Image object. Deprecated
|
||||||
* @property {String} dataType type of the data
|
* @property {String} dataType type of the data
|
||||||
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
|
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
|
||||||
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
|
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
|
||||||
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
|
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
|
||||||
* @property {function} getCompletionCallback - A function giving a callback to call
|
* @property {OpenSeadragon.Promise} - Promise resolved when the tile gets fully loaded.
|
||||||
* when the asynchronous processing of the image is done. The image will be
|
* @property {function} getCompletionCallback - deprecated
|
||||||
* marked as entirely loaded when the callback has been called once for each
|
|
||||||
* call to getCompletionCallback.
|
|
||||||
*/
|
*/
|
||||||
this.viewer.raiseEvent("tile-loaded", {
|
const promise = this.viewer.raiseEventAwaiting("tile-loaded", {
|
||||||
tile: tile,
|
tile: tile,
|
||||||
tiledImage: this,
|
tiledImage: this,
|
||||||
tileRequest: tileRequest,
|
tileRequest: tileRequest,
|
||||||
|
promise: finishPromise,
|
||||||
get image() {
|
get image() {
|
||||||
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
|
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile.getData()' instead.");
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
data: data,
|
get data() {
|
||||||
dataType: dataType,
|
$.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile.getData()' instead.");
|
||||||
getCompletionCallback: getCompletionCallback
|
return data;
|
||||||
|
},
|
||||||
|
getCompletionCallback: function () {
|
||||||
|
$.console.error("[tile-loaded] getCompletionCallback is not supported: it is compulsory to handle the event with async functions if applicable.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
promise.then(completionCallback).catch(() => {
|
||||||
|
$.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.");
|
||||||
|
completionCallback();
|
||||||
});
|
});
|
||||||
eventFinished = true;
|
|
||||||
// In case the completion callback is never called, we at least force it once.
|
|
||||||
fallbackCompletion();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,10 +33,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runTest(e) {
|
function runTest(e, async=false) {
|
||||||
context.raiseEvent(eName, e);
|
context.raiseEvent(eName, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runTestAwaiting(e, async=false) {
|
||||||
|
context.raiseEventAwaiting(eName, e);
|
||||||
|
}
|
||||||
|
|
||||||
QUnit.module( 'EventSource', {
|
QUnit.module( 'EventSource', {
|
||||||
beforeEach: function () {
|
beforeEach: function () {
|
||||||
context = new OpenSeadragon.EventSource();
|
context = new OpenSeadragon.EventSource();
|
||||||
@ -82,4 +86,58 @@
|
|||||||
message: 'Prioritized callback order should follow [2,1,4,5,3].'
|
message: 'Prioritized callback order should follow [2,1,4,5,3].'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('EventSource: async non-synchronized order', function(assert) {
|
||||||
|
context.addHandler(eName, executor(1, 5));
|
||||||
|
context.addHandler(eName, executor(2, 50));
|
||||||
|
context.addHandler(eName, executor(3));
|
||||||
|
context.addHandler(eName, executor(4));
|
||||||
|
runTest({
|
||||||
|
assert: assert,
|
||||||
|
done: assert.async(),
|
||||||
|
expected: [3, 4, 1, 2],
|
||||||
|
message: 'Async callback order should follow [3,4,1,2].'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('EventSource: async non-synchronized priority order', function(assert) {
|
||||||
|
context.addHandler(eName, executor(1, 5));
|
||||||
|
context.addHandler(eName, executor(2, 50), undefined, -100);
|
||||||
|
context.addHandler(eName, executor(3), undefined, -500);
|
||||||
|
context.addHandler(eName, executor(4), undefined, 675);
|
||||||
|
runTest({
|
||||||
|
assert: assert,
|
||||||
|
done: assert.async(),
|
||||||
|
expected: [4, 3, 1, 2],
|
||||||
|
message: 'Async callback order with priority should follow [4,3,1,2]. Async functions do not respect priority.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('EventSource: async synchronized order', function(assert) {
|
||||||
|
context.addHandler(eName, executor(1, 5));
|
||||||
|
context.addHandler(eName, executor(2, 50));
|
||||||
|
context.addHandler(eName, executor(3));
|
||||||
|
context.addHandler(eName, executor(4));
|
||||||
|
runTestAwaiting({
|
||||||
|
waitForPromiseHandlers: true,
|
||||||
|
assert: assert,
|
||||||
|
done: assert.async(),
|
||||||
|
expected: [1, 2, 3, 4],
|
||||||
|
message: 'Async callback order should follow [1,2,3,4], since it is synchronized.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('EventSource: async synchronized priority order', function(assert) {
|
||||||
|
context.addHandler(eName, executor(1, 5));
|
||||||
|
context.addHandler(eName, executor(2), undefined, -500);
|
||||||
|
context.addHandler(eName, executor(3, 50), undefined, -200);
|
||||||
|
context.addHandler(eName, executor(4), undefined, 675);
|
||||||
|
runTestAwaiting({
|
||||||
|
waitForPromiseHandlers: true,
|
||||||
|
assert: assert,
|
||||||
|
done: assert.async(),
|
||||||
|
expected: [4, 1, 3, 2],
|
||||||
|
message: 'Async callback order with priority should follow [4,1,3,2], since priority is respected when synchronized.'
|
||||||
|
});
|
||||||
|
});
|
||||||
} )();
|
} )();
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var viewer;
|
var viewer;
|
||||||
|
var sleep = time => new Promise(res => setTimeout(res, time));
|
||||||
|
|
||||||
QUnit.module( 'Events', {
|
QUnit.module( 'Events', {
|
||||||
beforeEach: function () {
|
beforeEach: function () {
|
||||||
@ -1210,11 +1211,12 @@
|
|||||||
var tile = event.tile;
|
var tile = event.tile;
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
assert.ok( tile.loading, "The tile should be marked as loading.");
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
||||||
setTimeout(function() {
|
//make sure we require tile loaded status once the data is ready
|
||||||
|
event.promise.then(function() {
|
||||||
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
||||||
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
||||||
done();
|
done();
|
||||||
}, 0);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
viewer.addHandler( 'tile-loaded', tileLoaded);
|
viewer.addHandler( 'tile-loaded', tileLoaded);
|
||||||
@ -1226,51 +1228,61 @@
|
|||||||
function tileLoaded ( event ) {
|
function tileLoaded ( event ) {
|
||||||
viewer.removeHandler( 'tile-loaded', tileLoaded);
|
viewer.removeHandler( 'tile-loaded', tileLoaded);
|
||||||
var tile = event.tile;
|
var tile = event.tile;
|
||||||
var callback = event.getCompletionCallback();
|
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
assert.ok( tile.loading, "The tile should be marked as loading.");
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
||||||
assert.ok( callback, "The event should have a callback.");
|
event.promise.then( _ => {
|
||||||
setTimeout(function() {
|
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
|
||||||
callback();
|
|
||||||
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
||||||
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
||||||
done();
|
done();
|
||||||
}, 0);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
viewer.addHandler( 'tile-loaded', tileLoaded);
|
viewer.addHandler( 'tile-loaded', tileLoaded);
|
||||||
viewer.open( '/test/data/testpattern.dzi' );
|
viewer.open( '/test/data/testpattern.dzi' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
QUnit.test( 'Viewer: tile-loaded event with 2 callbacks.', function (assert) {
|
QUnit.test( 'Viewer: asynchronous tile processing.', function (assert) {
|
||||||
var done = assert.async();
|
var done = assert.async(),
|
||||||
function tileLoaded ( event ) {
|
handledOnce = false;
|
||||||
viewer.removeHandler( 'tile-loaded', tileLoaded);
|
|
||||||
var tile = event.tile;
|
const tileLoaded1 = async (event) => {
|
||||||
var callback1 = event.getCompletionCallback();
|
assert.ok( handledOnce, "tileLoaded1 with priority 5 should be called second.");
|
||||||
var callback2 = event.getCompletionCallback();
|
const tile = event.tile;
|
||||||
|
handledOnce = true;
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
assert.ok( tile.loading, "The tile should be marked as loading.");
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
||||||
setTimeout(function() {
|
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
event.promise.then(() => {
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
|
||||||
callback1();
|
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
|
||||||
setTimeout(function() {
|
|
||||||
assert.ok( tile.loading, "The tile should be marked as loading.");
|
|
||||||
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
|
||||||
callback2();
|
|
||||||
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
||||||
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
||||||
done();
|
done();
|
||||||
}, 0);
|
done = null;
|
||||||
}, 0);
|
});
|
||||||
}
|
await sleep(10);
|
||||||
|
};
|
||||||
|
const tileLoaded2 = async (event) => {
|
||||||
|
assert.notOk( handledOnce, "TileLoaded2 with priority 10 should be called first.");
|
||||||
|
const tile = event.tile;
|
||||||
|
|
||||||
viewer.addHandler( 'tile-loaded', tileLoaded);
|
//remove handlers immediatelly, processing is async -> removing in the second function could
|
||||||
|
//get after a different tile gets processed
|
||||||
|
viewer.removeHandler( 'tile-loaded', tileLoaded1);
|
||||||
|
viewer.removeHandler( 'tile-loaded', tileLoaded2);
|
||||||
|
|
||||||
|
handledOnce = true;
|
||||||
|
assert.ok( tile.loading, "The tile should be marked as loading.");
|
||||||
|
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
|
||||||
|
|
||||||
|
event.promise.then(() => {
|
||||||
|
assert.notOk( tile.loading, "The tile should not be marked as loading.");
|
||||||
|
assert.ok( tile.loaded, "The tile should be marked as loaded.");
|
||||||
|
});
|
||||||
|
await sleep(30);
|
||||||
|
};
|
||||||
|
|
||||||
|
//first will get called tileLoaded2 although registered later
|
||||||
|
viewer.addHandler( 'tile-loaded', tileLoaded1, null, 5);
|
||||||
|
viewer.addHandler( 'tile-loaded', tileLoaded2, null, 10);
|
||||||
viewer.open( '/test/data/testpattern.dzi' );
|
viewer.open( '/test/data/testpattern.dzi' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>OpenSeadragon QUnit</title>
|
<title>OpenSeadragon QUnit</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.QUnit = {
|
||||||
|
config: {
|
||||||
|
//one minute per test timeout
|
||||||
|
testTimeout: 60000
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<link rel="stylesheet" href="/node_modules/qunit/qunit/qunit.css">
|
<link rel="stylesheet" href="/node_modules/qunit/qunit/qunit.css">
|
||||||
<link rel="stylesheet" href="/test/lib/jquery-ui-1.10.2/css/smoothness/jquery-ui-1.10.2.min.css">
|
<link rel="stylesheet" href="/test/lib/jquery-ui-1.10.2/css/smoothness/jquery-ui-1.10.2.min.css">
|
||||||
<link rel="stylesheet" href="/test/helpers/test.css">
|
<link rel="stylesheet" href="/test/helpers/test.css">
|
||||||
|
Loading…
Reference in New Issue
Block a user