mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-02-20 08:43:13 +03:00
Merge 750d45be81c09f4651cdd60fd2cd25e65a07ea19 into 89ae9c1376ebb86142283472bd4be8bcc102fd11
This commit is contained in:
commit
42ac1426d0
@ -48,6 +48,8 @@ module.exports = function(grunt) {
|
|||||||
"src/legacytilesource.js",
|
"src/legacytilesource.js",
|
||||||
"src/imagetilesource.js",
|
"src/imagetilesource.js",
|
||||||
"src/tilesourcecollection.js",
|
"src/tilesourcecollection.js",
|
||||||
|
"src/priorityqueue.js",
|
||||||
|
"src/datatypeconvertor.js",
|
||||||
"src/button.js",
|
"src/button.js",
|
||||||
"src/buttongroup.js",
|
"src/buttongroup.js",
|
||||||
"src/rectangle.js",
|
"src/rectangle.js",
|
||||||
|
387
src/datatypeconvertor.js
Normal file
387
src/datatypeconvertor.js
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
/*
|
||||||
|
* OpenSeadragon.convertor (static property)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2009 CodePlex Foundation
|
||||||
|
* Copyright (C) 2010-2023 OpenSeadragon contributors
|
||||||
|
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are
|
||||||
|
* met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* - Neither the name of CodePlex Foundation nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($){
|
||||||
|
|
||||||
|
/**
|
||||||
|
* modified from https://gist.github.com/Prottoy2938/66849e04b0bac459606059f5f9f3aa1a
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class WeightedGraph {
|
||||||
|
constructor() {
|
||||||
|
this.adjacencyList = {};
|
||||||
|
this.vertices = {};
|
||||||
|
}
|
||||||
|
addVertex(vertex) {
|
||||||
|
if (!this.vertices[vertex]) {
|
||||||
|
this.vertices[vertex] = new $.PriorityQueue.Node(0, vertex);
|
||||||
|
this.adjacencyList[vertex] = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
addEdge(vertex1, vertex2, weight, transform) {
|
||||||
|
this.adjacencyList[vertex1].push({ target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {{path: *[], cost: number}|undefined} cheapest path for
|
||||||
|
*/
|
||||||
|
dijkstra(start, finish) {
|
||||||
|
let path = []; //to return at end
|
||||||
|
if (start === finish) {
|
||||||
|
return {path: path, cost: 0};
|
||||||
|
}
|
||||||
|
const nodes = new OpenSeadragon.PriorityQueue();
|
||||||
|
let smallestNode;
|
||||||
|
//build up initial state
|
||||||
|
for (let vertex in this.vertices) {
|
||||||
|
vertex = this.vertices[vertex];
|
||||||
|
if (vertex.value === start) {
|
||||||
|
vertex.key = 0; //keys are known distances
|
||||||
|
nodes.insertNode(vertex);
|
||||||
|
} else {
|
||||||
|
vertex.key = Infinity;
|
||||||
|
delete vertex.index;
|
||||||
|
}
|
||||||
|
vertex._previous = null;
|
||||||
|
}
|
||||||
|
// as long as there is something to visit
|
||||||
|
while (nodes.getCount() > 0) {
|
||||||
|
smallestNode = nodes.remove();
|
||||||
|
if (smallestNode.value === finish) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const neighbors = this.adjacencyList[smallestNode.value];
|
||||||
|
for (let neighborKey in neighbors) {
|
||||||
|
let edge = neighbors[neighborKey];
|
||||||
|
//relax node
|
||||||
|
let newCost = smallestNode.key + edge.weight;
|
||||||
|
let nextNeighbor = edge.target;
|
||||||
|
if (newCost < nextNeighbor.key) {
|
||||||
|
nextNeighbor._previous = smallestNode;
|
||||||
|
//key change
|
||||||
|
nodes.decreaseKey(nextNeighbor, newCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smallestNode || !smallestNode._previous) {
|
||||||
|
return undefined; //no path
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalCost = smallestNode.key; //final weight last node
|
||||||
|
|
||||||
|
// done, build the shortest path
|
||||||
|
while (smallestNode._previous) {
|
||||||
|
//backtrack
|
||||||
|
const to = smallestNode.value,
|
||||||
|
parent = smallestNode._previous,
|
||||||
|
from = parent.value;
|
||||||
|
|
||||||
|
path.push(this.adjacencyList[from].find(x => x.target.value === to));
|
||||||
|
smallestNode = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: path.reverse(),
|
||||||
|
cost: finalCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
this.graph = new WeightedGraph();
|
||||||
|
this.destructors = {};
|
||||||
|
|
||||||
|
// Teaching OpenSeadragon built-in conversions:
|
||||||
|
|
||||||
|
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("context2d", "canvas", (context2D) => context2D.canvas);
|
||||||
|
this.learn("image", "canvas", (image) => {
|
||||||
|
const canvas = document.createElement( 'canvas' );
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.drawImage( image, 0, 0 );
|
||||||
|
return canvas;
|
||||||
|
}, 1, 1);
|
||||||
|
this.learn("rasterUrl", "image", (url) => {
|
||||||
|
return new $.Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onerror = img.onabort = reject;
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: types are sensitive thing. Same data type might have different data semantics.
|
||||||
|
* - 'string' can be anything, for images, dataUrl or some URI, or incompatible stuff: vector data (JSON)
|
||||||
|
* - using $.type makes explicit requirements on its extensibility, and makes mess in naming
|
||||||
|
* - most types are [object X]
|
||||||
|
* - selected types are 'nice' -> string, canvas...
|
||||||
|
* - hard to debug
|
||||||
|
*
|
||||||
|
* Unique identifier (unlike toString.call(x)) to be guessed
|
||||||
|
* from the data value
|
||||||
|
*
|
||||||
|
* @function uniqueType
|
||||||
|
* @param x object to get unique identifier for
|
||||||
|
* - can be array, in that case, alphabetically-ordered list of inner unique types
|
||||||
|
* is returned (null, undefined are ignored)
|
||||||
|
* - if $.isPlainObject(x) is true, then the object can define
|
||||||
|
* getType function to specify its type
|
||||||
|
* - otherwise, toString.call(x) is applied to get the parameter description
|
||||||
|
* @return {string} unique variable descriptor
|
||||||
|
*/
|
||||||
|
guessType( x ) {
|
||||||
|
if (Array.isArray(x)) {
|
||||||
|
const types = [];
|
||||||
|
for (let item of x) {
|
||||||
|
if (item === undefined || item === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.guessType(item);
|
||||||
|
if (!types.includes(type)) {
|
||||||
|
types.push(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
types.sort();
|
||||||
|
return `Array [${types.join(",")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessType = $.type(x);
|
||||||
|
if (guessType === "dom-node") {
|
||||||
|
//distinguish nodes
|
||||||
|
return guessType.nodeName.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guessType === "object") {
|
||||||
|
if ($.isFunction(x.getType)) {
|
||||||
|
return x.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return guessType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teach the system to convert data type 'from' -> 'to'
|
||||||
|
* @param {string} from unique ID of the data item 'from'
|
||||||
|
* @param {string} to unique ID of the data item '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.
|
||||||
|
* Should reflect the actual cost of the conversion:
|
||||||
|
* - if nothing must be done and only reference is retrieved (or a constant operation done),
|
||||||
|
* return 0 (default)
|
||||||
|
* - if a linear amount of work is necessary,
|
||||||
|
* return 1
|
||||||
|
* ... and so on, basically the number in O() complexity power exponent (for simplification)
|
||||||
|
* @param {Number} [costMultiplier=1] multiplier of the cost class, e.g. O(3n^2) would
|
||||||
|
* use costPower=2, costMultiplier=3; can be between 1 and 10^5
|
||||||
|
*/
|
||||||
|
learn(from, to, callback, costPower = 0, costMultiplier = 1) {
|
||||||
|
$.console.assert(costPower >= 0 && costPower <= 7, "[DataTypeConvertor] Conversion costPower must be between <0, 7>.");
|
||||||
|
$.console.assert($.isFunction(callback), "[DataTypeConvertor:learn] Callback must be a valid function!");
|
||||||
|
|
||||||
|
//we won't know if somebody added multiple edges, though it will choose some edge anyway
|
||||||
|
costPower++;
|
||||||
|
costMultiplier = Math.min(Math.max(costMultiplier, 1), 10 ^ 5);
|
||||||
|
this.graph.addVertex(from);
|
||||||
|
this.graph.addVertex(to);
|
||||||
|
this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
|
||||||
|
this._known = {}; //invalidate precomputed paths :/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teach the system to destroy data type 'type'
|
||||||
|
* for example, textures loaded to GPU have to be also manually removed when not needed anymore.
|
||||||
|
* Needs to be defined only when the created object has extra deletion process.
|
||||||
|
* @param {string} type
|
||||||
|
* @param {OpenSeadragon.TypeConvertor} callback destructor, receives the object created,
|
||||||
|
* 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 {string} from data item type
|
||||||
|
* @param {string} to desired type(s)
|
||||||
|
* @return {OpenSeadragon.Promise<?>} promise resolution with type 'to' or undefined if the conversion failed
|
||||||
|
*/
|
||||||
|
convert(x, from, ...to) {
|
||||||
|
const conversionPath = this.getConversionPath(from, to);
|
||||||
|
if (!conversionPath) {
|
||||||
|
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
|
||||||
|
return $.Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepCount = conversionPath.length,
|
||||||
|
_this = this;
|
||||||
|
const step = (x, i) => {
|
||||||
|
if (i >= stepCount) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get possible system type conversions and cache result.
|
||||||
|
* @param {string} from data item type
|
||||||
|
* @param {string|string[]} to array of accepted types
|
||||||
|
* @return {[ConversionStep]|undefined} array of required conversions (returns empty array
|
||||||
|
* 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) {
|
||||||
|
let bestConvertorPath, selectedType;
|
||||||
|
let knownFrom = this._known[from];
|
||||||
|
if (!knownFrom) {
|
||||||
|
this._known[from] = knownFrom = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(to)) {
|
||||||
|
$.console.assert(typeof to === "string" || to.length > 0, "[getConversionPath] conversion 'to' type must be defined.");
|
||||||
|
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) {
|
||||||
|
const conversion = knownFrom[outType];
|
||||||
|
if (conversion && bestCost > conversion.cost) {
|
||||||
|
bestConvertorPath = conversion;
|
||||||
|
bestCost = conversion.cost;
|
||||||
|
selectedType = outType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.console.assert(typeof to === "string", "[getConversionPath] conversion 'to' type must be defined.");
|
||||||
|
bestConvertorPath = knownFrom[to];
|
||||||
|
selectedType = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestConvertorPath) {
|
||||||
|
bestConvertorPath = this.graph.dijkstra(from, selectedType);
|
||||||
|
this._known[from][selectedType] = bestConvertorPath;
|
||||||
|
}
|
||||||
|
return bestConvertorPath ? bestConvertorPath.path : undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
$.convertor = new $.DataTypeConvertor();
|
||||||
|
|
||||||
|
}(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.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,12 +112,38 @@ $.ImageJob.prototype = {
|
|||||||
* Finish this job.
|
* Finish this job.
|
||||||
* @param {*} data data that has been downloaded
|
* @param {*} data data that has been downloaded
|
||||||
* @param {XMLHttpRequest} request reference to the request if used
|
* @param {XMLHttpRequest} request reference to the request if used
|
||||||
* @param {string} errorMessage description upon failure
|
* @param {string} dataType data type identifier
|
||||||
|
* old behavior: dataType treated as errorMessage if data is falsey value
|
||||||
*/
|
*/
|
||||||
finish: function(data, request, errorMessage ) {
|
finish: function(data, request, dataType) {
|
||||||
|
// old behavior, no deprecation due to possible finish calls with invalid data item (e.g. different error)
|
||||||
|
if (data === null || data === undefined || data === false) {
|
||||||
|
this.fail(dataType || "[downloadTileStart->finish()] Retrieved data is invalid!", request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.request = request;
|
this.request = request;
|
||||||
|
this.errorMsg = null;
|
||||||
|
this.dataType = dataType;
|
||||||
|
|
||||||
|
if (this.jobId) {
|
||||||
|
window.clearTimeout(this.jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callback(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish this job as a failure.
|
||||||
|
* @param {string} errorMessage description upon failure
|
||||||
|
* @param {XMLHttpRequest} request reference to the request if used
|
||||||
|
*/
|
||||||
|
fail: function(errorMessage, request) {
|
||||||
|
this.data = null;
|
||||||
|
this.request = request;
|
||||||
this.errorMsg = errorMessage;
|
this.errorMsg = errorMessage;
|
||||||
|
this.dataType = null;
|
||||||
|
|
||||||
if (this.jobId) {
|
if (this.jobId) {
|
||||||
window.clearTimeout(this.jobId);
|
window.clearTimeout(this.jobId);
|
||||||
@ -180,10 +206,7 @@ $.ImageLoader.prototype = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var _this = this,
|
const _this = this,
|
||||||
complete = function(job) {
|
|
||||||
completeJob(_this, job, options.callback);
|
|
||||||
},
|
|
||||||
jobOptions = {
|
jobOptions = {
|
||||||
src: options.src,
|
src: options.src,
|
||||||
tile: options.tile || {},
|
tile: options.tile || {},
|
||||||
@ -193,7 +216,7 @@ $.ImageLoader.prototype = {
|
|||||||
crossOriginPolicy: options.crossOriginPolicy,
|
crossOriginPolicy: options.crossOriginPolicy,
|
||||||
ajaxWithCredentials: options.ajaxWithCredentials,
|
ajaxWithCredentials: options.ajaxWithCredentials,
|
||||||
postData: options.postData,
|
postData: options.postData,
|
||||||
callback: complete,
|
callback: (job) => completeJob(_this, job, options.callback),
|
||||||
abort: options.abort,
|
abort: options.abort,
|
||||||
timeout: this.timeout
|
timeout: this.timeout
|
||||||
},
|
},
|
||||||
@ -234,10 +257,10 @@ $.ImageLoader.prototype = {
|
|||||||
* @param callback - Called once cleanup is finished.
|
* @param callback - Called once cleanup is finished.
|
||||||
*/
|
*/
|
||||||
function completeJob(loader, job, callback) {
|
function completeJob(loader, job, callback) {
|
||||||
if (job.errorMsg !== '' && (job.data === null || job.data === undefined) && job.tries < 1 + loader.tileRetryMax) {
|
if (job.errorMsg && job.data === null && job.tries < 1 + loader.tileRetryMax) {
|
||||||
loader.failedTiles.push(job);
|
loader.failedTiles.push(job);
|
||||||
}
|
}
|
||||||
var nextJob;
|
let nextJob;
|
||||||
|
|
||||||
loader.jobsInProgress--;
|
loader.jobsInProgress--;
|
||||||
|
|
||||||
@ -249,15 +272,15 @@ function completeJob(loader, job, callback) {
|
|||||||
|
|
||||||
if (loader.tileRetryMax > 0 && loader.jobQueue.length === 0) {
|
if (loader.tileRetryMax > 0 && loader.jobQueue.length === 0) {
|
||||||
if ((!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.failedTiles.length > 0) {
|
if ((!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.failedTiles.length > 0) {
|
||||||
nextJob = loader.failedTiles.shift();
|
nextJob = loader.failedTiles.shift();
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
nextJob.start();
|
nextJob.start();
|
||||||
}, loader.tileRetryDelay);
|
}, loader.tileRetryDelay);
|
||||||
loader.jobsInProgress++;
|
loader.jobsInProgress++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(job.data, job.errorMsg, job.request);
|
callback(job.data, job.errorMsg, job.request, job.dataType);
|
||||||
}
|
}
|
||||||
|
|
||||||
}(OpenSeadragon));
|
}(OpenSeadragon));
|
||||||
|
@ -34,250 +34,243 @@
|
|||||||
|
|
||||||
(function ($) {
|
(function ($) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class ImageTileSource
|
* @class ImageTileSource
|
||||||
* @classdesc The ImageTileSource allows a simple image to be loaded
|
* @classdesc The ImageTileSource allows a simple image to be loaded
|
||||||
* into an OpenSeadragon Viewer.
|
* into an OpenSeadragon Viewer.
|
||||||
* There are 2 ways to open an ImageTileSource:
|
* There are 2 ways to open an ImageTileSource:
|
||||||
* 1. viewer.open({type: 'image', url: fooUrl});
|
* 1. viewer.open({type: 'image', url: fooUrl});
|
||||||
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
|
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
|
||||||
*
|
*
|
||||||
* With the first syntax, the crossOriginPolicy, ajaxWithCredentials and
|
* With the first syntax, the crossOriginPolicy, ajaxWithCredentials and
|
||||||
* useCanvas options are inherited from the viewer if they are not
|
* useCanvas options are inherited from the viewer if they are not
|
||||||
* specified directly in the options object.
|
* specified directly in the options object.
|
||||||
*
|
*
|
||||||
* @memberof OpenSeadragon
|
* @memberof OpenSeadragon
|
||||||
* @extends OpenSeadragon.TileSource
|
* @extends OpenSeadragon.TileSource
|
||||||
* @param {Object} options Options object.
|
* @param {Object} options Options object.
|
||||||
* @param {String} options.url URL of the image
|
* @param {String} options.url URL of the image
|
||||||
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
|
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
|
||||||
* pyramid will be built internally to provide a better downsampling.
|
* pyramid will be built internally to provide a better downsampling.
|
||||||
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
|
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
|
||||||
* 'Anonymous', 'use-credentials', and false. If false, image requests will
|
* 'Anonymous', 'use-credentials', and false. If false, image requests will
|
||||||
* not use CORS preventing internal pyramid building for images from other
|
* not use CORS preventing internal pyramid building for images from other
|
||||||
* domains.
|
* domains.
|
||||||
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
|
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
|
||||||
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
|
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
|
||||||
* @param {Boolean} [options.useCanvas=true] Set to false to prevent any use
|
* @param {Boolean} [options.useCanvas=true] Set to false to prevent any use
|
||||||
* of the canvas API.
|
* of the canvas API.
|
||||||
*/
|
*/
|
||||||
$.ImageTileSource = function (options) {
|
$.ImageTileSource = class extends $.TileSource {
|
||||||
|
|
||||||
options = $.extend({
|
constructor(props) {
|
||||||
|
super($.extend({
|
||||||
buildPyramid: true,
|
buildPyramid: true,
|
||||||
crossOriginPolicy: false,
|
crossOriginPolicy: false,
|
||||||
ajaxWithCredentials: false,
|
ajaxWithCredentials: false,
|
||||||
useCanvas: true
|
useCanvas: true
|
||||||
}, options);
|
}, props));
|
||||||
$.TileSource.apply(this, [options]);
|
}
|
||||||
|
|
||||||
};
|
/**
|
||||||
|
* Determine if the data and/or url imply the image service is supported by
|
||||||
|
* this tile source.
|
||||||
|
* @function
|
||||||
|
* @param {Object|Array} data
|
||||||
|
* @param {String} url - optional
|
||||||
|
*/
|
||||||
|
supports(data, url) {
|
||||||
|
return data.type && data.type === "image";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @param {Object} options - the options
|
||||||
|
* @param {String} dataUrl - the url the image was retrieved from, if any.
|
||||||
|
* @param {String} postData - HTTP POST data in k=v&k2=v2... form or null
|
||||||
|
* @returns {Object} options - A dictionary of keyword arguments sufficient
|
||||||
|
* to configure this tile sources constructor.
|
||||||
|
*/
|
||||||
|
configure(options, dataUrl, postData) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Responsible for retrieving, and caching the
|
||||||
|
* image metadata pertinent to this TileSources implementation.
|
||||||
|
* @function
|
||||||
|
* @param {String} url
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
getImageInfo(url) {
|
||||||
|
const image = new Image(),
|
||||||
|
_this = this;
|
||||||
|
|
||||||
$.extend($.ImageTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ImageTileSource.prototype */{
|
if (this.crossOriginPolicy) {
|
||||||
/**
|
image.crossOrigin = this.crossOriginPolicy;
|
||||||
* Determine if the data and/or url imply the image service is supported by
|
}
|
||||||
* this tile source.
|
if (this.ajaxWithCredentials) {
|
||||||
* @function
|
image.useCredentials = this.ajaxWithCredentials;
|
||||||
* @param {Object|Array} data
|
}
|
||||||
* @param {String} optional - url
|
|
||||||
*/
|
|
||||||
supports: function (data, url) {
|
|
||||||
return data.type && data.type === "image";
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @function
|
|
||||||
* @param {Object} options - the options
|
|
||||||
* @param {String} dataUrl - the url the image was retrieved from, if any.
|
|
||||||
* @param {String} postData - HTTP POST data in k=v&k2=v2... form or null
|
|
||||||
* @returns {Object} options - A dictionary of keyword arguments sufficient
|
|
||||||
* to configure this tile sources constructor.
|
|
||||||
*/
|
|
||||||
configure: function (options, dataUrl, postData) {
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Responsible for retrieving, and caching the
|
|
||||||
* image metadata pertinent to this TileSources implementation.
|
|
||||||
* @function
|
|
||||||
* @param {String} url
|
|
||||||
* @throws {Error}
|
|
||||||
*/
|
|
||||||
getImageInfo: function (url) {
|
|
||||||
var image = this._image = new Image();
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
if (this.crossOriginPolicy) {
|
$.addEvent(image, 'load', function () {
|
||||||
image.crossOrigin = this.crossOriginPolicy;
|
_this.width = image.naturalWidth;
|
||||||
}
|
_this.height = image.naturalHeight;
|
||||||
if (this.ajaxWithCredentials) {
|
_this.aspectRatio = _this.width / _this.height;
|
||||||
image.useCredentials = this.ajaxWithCredentials;
|
_this.dimensions = new $.Point(_this.width, _this.height);
|
||||||
}
|
_this._tileWidth = _this.width;
|
||||||
|
_this._tileHeight = _this.height;
|
||||||
|
_this.tileOverlap = 0;
|
||||||
|
_this.minLevel = 0;
|
||||||
|
_this.image = image;
|
||||||
|
_this.levels = _this._buildLevels(image);
|
||||||
|
_this.maxLevel = _this.levels.length - 1;
|
||||||
|
|
||||||
$.addEvent(image, 'load', function () {
|
_this.ready = true;
|
||||||
_this.width = image.naturalWidth;
|
|
||||||
_this.height = image.naturalHeight;
|
|
||||||
_this.aspectRatio = _this.width / _this.height;
|
|
||||||
_this.dimensions = new $.Point(_this.width, _this.height);
|
|
||||||
_this._tileWidth = _this.width;
|
|
||||||
_this._tileHeight = _this.height;
|
|
||||||
_this.tileOverlap = 0;
|
|
||||||
_this.minLevel = 0;
|
|
||||||
_this.levels = _this._buildLevels();
|
|
||||||
_this.maxLevel = _this.levels.length - 1;
|
|
||||||
|
|
||||||
_this.ready = true;
|
// Note: this event is documented elsewhere, in TileSource
|
||||||
|
_this.raiseEvent('ready', {tileSource: _this});
|
||||||
|
});
|
||||||
|
|
||||||
// Note: this event is documented elsewhere, in TileSource
|
$.addEvent(image, 'error', function () {
|
||||||
_this.raiseEvent('ready', {tileSource: _this});
|
_this.image = null;
|
||||||
|
// Note: this event is documented elsewhere, in TileSource
|
||||||
|
_this.raiseEvent('open-failed', {
|
||||||
|
message: "Error loading image at " + url,
|
||||||
|
source: url
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$.addEvent(image, 'error', function () {
|
image.src = url;
|
||||||
// Note: this event is documented elsewhere, in TileSource
|
}
|
||||||
_this.raiseEvent('open-failed', {
|
/**
|
||||||
message: "Error loading image at " + url,
|
* @function
|
||||||
source: url
|
* @param {Number} level
|
||||||
});
|
*/
|
||||||
});
|
getLevelScale(level) {
|
||||||
|
let levelScale = NaN;
|
||||||
|
if (level >= this.minLevel && level <= this.maxLevel) {
|
||||||
|
levelScale =
|
||||||
|
this.levels[level].width /
|
||||||
|
this.levels[this.maxLevel].width;
|
||||||
|
}
|
||||||
|
return levelScale;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {Number} level
|
||||||
|
*/
|
||||||
|
getNumTiles(level) {
|
||||||
|
if (this.getLevelScale(level)) {
|
||||||
|
return new $.Point(1, 1);
|
||||||
|
}
|
||||||
|
return new $.Point(0, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retrieves a tile url
|
||||||
|
* @function
|
||||||
|
* @param {Number} level Level of the tile
|
||||||
|
* @param {Number} x x coordinate of the tile
|
||||||
|
* @param {Number} y y coordinate of the tile
|
||||||
|
*/
|
||||||
|
getTileUrl(level, x, y) {
|
||||||
|
if (level === this.maxLevel) {
|
||||||
|
return this.url; //for original image, preserve url
|
||||||
|
}
|
||||||
|
//make up url by positional args
|
||||||
|
return `${this.url}?l=${level}&x=${x}&y=${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
image.src = url;
|
getTilePostData(level, x, y) {
|
||||||
},
|
return {level: level, x: x, y: y};
|
||||||
/**
|
}
|
||||||
* @function
|
|
||||||
* @param {Number} level
|
|
||||||
*/
|
|
||||||
getLevelScale: function (level) {
|
|
||||||
var levelScale = NaN;
|
|
||||||
if (level >= this.minLevel && level <= this.maxLevel) {
|
|
||||||
levelScale =
|
|
||||||
this.levels[level].width /
|
|
||||||
this.levels[this.maxLevel].width;
|
|
||||||
}
|
|
||||||
return levelScale;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @function
|
|
||||||
* @param {Number} level
|
|
||||||
*/
|
|
||||||
getNumTiles: function (level) {
|
|
||||||
var scale = this.getLevelScale(level);
|
|
||||||
if (scale) {
|
|
||||||
return new $.Point(1, 1);
|
|
||||||
} else {
|
|
||||||
return new $.Point(0, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Retrieves a tile url
|
|
||||||
* @function
|
|
||||||
* @param {Number} level Level of the tile
|
|
||||||
* @param {Number} x x coordinate of the tile
|
|
||||||
* @param {Number} y y coordinate of the tile
|
|
||||||
*/
|
|
||||||
getTileUrl: function (level, x, y) {
|
|
||||||
var url = null;
|
|
||||||
if (level >= this.minLevel && level <= this.maxLevel) {
|
|
||||||
url = this.levels[level].url;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Retrieves a tile context 2D
|
|
||||||
* @function
|
|
||||||
* @param {Number} level Level of the tile
|
|
||||||
* @param {Number} x x coordinate of the tile
|
|
||||||
* @param {Number} y y coordinate of the tile
|
|
||||||
*/
|
|
||||||
getContext2D: function (level, x, y) {
|
|
||||||
var context = null;
|
|
||||||
if (level >= this.minLevel && level <= this.maxLevel) {
|
|
||||||
context = this.levels[level].context2D;
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Destroys ImageTileSource
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
destroy: function () {
|
|
||||||
this._freeupCanvasMemory();
|
|
||||||
},
|
|
||||||
|
|
||||||
// private
|
/**
|
||||||
//
|
* Retrieves a tile context 2D
|
||||||
// Builds the different levels of the pyramid if possible
|
* @deprecated
|
||||||
// (i.e. if canvas API enabled and no canvas tainting issue).
|
*/
|
||||||
_buildLevels: function () {
|
getContext2D(level, x, y) {
|
||||||
var levels = [{
|
$.console.warn('Using [TiledImage.getContext2D] (for plain images only) is deprecated. ' +
|
||||||
url: this._image.src,
|
'Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.');
|
||||||
width: this._image.naturalWidth,
|
var context = null;
|
||||||
height: this._image.naturalHeight
|
if (level >= this.minLevel && level <= this.maxLevel) {
|
||||||
}];
|
context = this.levels[level].context2D;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) {
|
downloadTileStart(job) {
|
||||||
// We don't need the image anymore. Allows it to be GC.
|
const tileData = job.postData;
|
||||||
delete this._image;
|
if (tileData.level === this.maxLevel) {
|
||||||
return levels;
|
job.finish(this.image, null, "image");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var currentWidth = this._image.naturalWidth;
|
if (tileData.level >= this.minLevel && tileData.level <= this.maxLevel) {
|
||||||
var currentHeight = this._image.naturalHeight;
|
const levelData = this.levels[tileData.level];
|
||||||
|
const context = this._createContext2D(this.image, levelData.width, levelData.height);
|
||||||
|
job.finish(context, null, "context2d");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
job.fail(`Invalid level ${tileData.level} for plain image source. Did you forget to set buildPyramid=true?`);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTileAbort(job) {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
var bigCanvas = document.createElement("canvas");
|
// private
|
||||||
var bigContext = bigCanvas.getContext("2d");
|
//
|
||||||
|
// Builds the different levels of the pyramid if possible
|
||||||
|
// (i.e. if canvas API enabled and no canvas tainting issue).
|
||||||
|
_buildLevels(image) {
|
||||||
|
const levels = [{
|
||||||
|
url: image.src,
|
||||||
|
width: image.naturalWidth,
|
||||||
|
height: image.naturalHeight
|
||||||
|
}];
|
||||||
|
|
||||||
bigCanvas.width = currentWidth;
|
if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) {
|
||||||
bigCanvas.height = currentHeight;
|
|
||||||
bigContext.drawImage(this._image, 0, 0, currentWidth, currentHeight);
|
|
||||||
// We cache the context of the highest level because the browser
|
|
||||||
// is a lot faster at downsampling something it already has
|
|
||||||
// downsampled before.
|
|
||||||
levels[0].context2D = bigContext;
|
|
||||||
// We don't need the image anymore. Allows it to be GC.
|
|
||||||
delete this._image;
|
|
||||||
|
|
||||||
if ($.isCanvasTainted(bigCanvas)) {
|
|
||||||
// If the canvas is tainted, we can't compute the pyramid.
|
|
||||||
return levels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We build smaller levels until either width or height becomes
|
|
||||||
// 1 pixel wide.
|
|
||||||
while (currentWidth >= 2 && currentHeight >= 2) {
|
|
||||||
currentWidth = Math.floor(currentWidth / 2);
|
|
||||||
currentHeight = Math.floor(currentHeight / 2);
|
|
||||||
var smallCanvas = document.createElement("canvas");
|
|
||||||
var smallContext = smallCanvas.getContext("2d");
|
|
||||||
smallCanvas.width = currentWidth;
|
|
||||||
smallCanvas.height = currentHeight;
|
|
||||||
smallContext.drawImage(bigCanvas, 0, 0, currentWidth, currentHeight);
|
|
||||||
|
|
||||||
levels.splice(0, 0, {
|
|
||||||
context2D: smallContext,
|
|
||||||
width: currentWidth,
|
|
||||||
height: currentHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
bigCanvas = smallCanvas;
|
|
||||||
bigContext = smallContext;
|
|
||||||
}
|
|
||||||
return levels;
|
return levels;
|
||||||
},
|
}
|
||||||
/**
|
|
||||||
* Free up canvas memory
|
let currentWidth = image.naturalWidth,
|
||||||
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
|
currentHeight = image.naturalHeight;
|
||||||
* and Safari keeps canvas until its height and width will be set to 0).
|
|
||||||
* @function
|
// We cache the context of the highest level because the browser
|
||||||
*/
|
// is a lot faster at downsampling something it already has
|
||||||
_freeupCanvasMemory: function () {
|
// downsampled before.
|
||||||
for (var i = 0; i < this.levels.length; i++) {
|
levels[0].context2D = this._createContext2D(image, currentWidth, currentHeight);
|
||||||
if(this.levels[i].context2D){
|
// We don't need the image anymore. Allows it to be GC.
|
||||||
this.levels[i].context2D.canvas.height = 0;
|
|
||||||
this.levels[i].context2D.canvas.width = 0;
|
if ($.isCanvasTainted(levels[0].context2D)) {
|
||||||
}
|
// If the canvas is tainted, we can't compute the pyramid.
|
||||||
}
|
this.buildPyramid = false;
|
||||||
},
|
return levels;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// We build smaller levels until either width or height becomes
|
||||||
|
// 1 pixel wide.
|
||||||
|
while (currentWidth >= 2 && currentHeight >= 2) {
|
||||||
|
currentWidth = Math.floor(currentWidth / 2);
|
||||||
|
currentHeight = Math.floor(currentHeight / 2);
|
||||||
|
|
||||||
|
levels.push({
|
||||||
|
width: currentWidth,
|
||||||
|
height: currentHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return levels.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createContext2D(data, w, h) {
|
||||||
|
const canvas = document.createElement("canvas"),
|
||||||
|
context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
context.drawImage(data, 0, 0, w, h);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}(OpenSeadragon));
|
}(OpenSeadragon));
|
||||||
|
@ -838,16 +838,20 @@ function OpenSeadragon( options ){
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
var class2type = {
|
var class2type = {
|
||||||
'[object Boolean]': 'boolean',
|
'[object Boolean]': 'boolean',
|
||||||
'[object Number]': 'number',
|
'[object Number]': 'number',
|
||||||
'[object String]': 'string',
|
'[object String]': 'string',
|
||||||
'[object Function]': 'function',
|
'[object Function]': 'function',
|
||||||
'[object AsyncFunction]': 'function',
|
'[object AsyncFunction]': 'function',
|
||||||
'[object Promise]': 'promise',
|
'[object Promise]': 'promise',
|
||||||
'[object Array]': 'array',
|
'[object Array]': 'array',
|
||||||
'[object Date]': 'date',
|
'[object Date]': 'date',
|
||||||
'[object RegExp]': 'regexp',
|
'[object RegExp]': 'regexp',
|
||||||
'[object Object]': 'object'
|
'[object Object]': 'object',
|
||||||
|
'[object HTMLUnknownElement]': 'dom-node',
|
||||||
|
'[object HTMLImageElement]': 'image',
|
||||||
|
'[object HTMLCanvasElement]': 'canvas',
|
||||||
|
'[object CanvasRenderingContext2D]': 'context2d'
|
||||||
},
|
},
|
||||||
// Save a reference to some core methods
|
// Save a reference to some core methods
|
||||||
toString = Object.prototype.toString,
|
toString = Object.prototype.toString,
|
||||||
@ -2375,6 +2379,7 @@ function OpenSeadragon( options ){
|
|||||||
|
|
||||||
// Note that our preferred API is that you pass in a single object; the named
|
// Note that our preferred API is that you pass in a single object; the named
|
||||||
// arguments are for legacy support.
|
// arguments are for legacy support.
|
||||||
|
// FIXME ^ are we ready to drop legacy support? since we abandoned old ES...
|
||||||
if( $.isPlainObject( url ) ){
|
if( $.isPlainObject( url ) ){
|
||||||
onSuccess = url.success;
|
onSuccess = url.success;
|
||||||
onError = url.error;
|
onError = url.error;
|
||||||
@ -2881,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));
|
||||||
|
|
||||||
|
|
||||||
|
360
src/priorityqueue.js
Normal file
360
src/priorityqueue.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
/*
|
||||||
|
* OpenSeadragon - Queue
|
||||||
|
*
|
||||||
|
* Copyright (C) 2023 OpenSeadragon contributors (modified)
|
||||||
|
* Copyright (C) Google Inc., The Closure Library Authors.
|
||||||
|
* https://github.com/google/closure-library
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class PriorityQueue
|
||||||
|
* @classdesc Fast priority queue. Implemented as a Heap.
|
||||||
|
*/
|
||||||
|
$.PriorityQueue = class {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?OpenSeadragon.PriorityQueue} optHeap Optional Heap or
|
||||||
|
* Object to initialize heap with.
|
||||||
|
*/
|
||||||
|
constructor(optHeap = undefined) {
|
||||||
|
/**
|
||||||
|
* The nodes of the heap.
|
||||||
|
*
|
||||||
|
* This is a densely packed array containing all nodes of the heap, using
|
||||||
|
* the standard flat representation of a tree as an array (i.e. element [0]
|
||||||
|
* at the top, with [1] and [2] as the second row, [3] through [6] as the
|
||||||
|
* third, etc). Thus, the children of element `i` are `2i+1` and `2i+2`, and
|
||||||
|
* the parent of element `i` is `⌊(i-1)/2⌋`.
|
||||||
|
*
|
||||||
|
* The only invariant is that children's keys must be greater than parents'.
|
||||||
|
*
|
||||||
|
* @private @const {!Array<!Node>}
|
||||||
|
*/
|
||||||
|
this.nodes_ = [];
|
||||||
|
|
||||||
|
if (optHeap) {
|
||||||
|
this.insertAll(optHeap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert the given value into the heap with the given key.
|
||||||
|
* @param {K} key The key.
|
||||||
|
* @param {V} value The value.
|
||||||
|
*/
|
||||||
|
insert(key, value) {
|
||||||
|
this.insertNode(new Node(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert node item.
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
|
insertNode(node) {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
node.index = nodes.length;
|
||||||
|
nodes.push(node);
|
||||||
|
this.moveUp_(node.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple key-value pairs from another Heap or Object
|
||||||
|
* @param {?OpenSeadragon.PriorityQueue} heap Object containing the data to add.
|
||||||
|
*/
|
||||||
|
insertAll(heap) {
|
||||||
|
let keys, values;
|
||||||
|
if (heap instanceof $.PriorityQueue) {
|
||||||
|
keys = heap.getKeys();
|
||||||
|
values = heap.getValues();
|
||||||
|
|
||||||
|
// If it is a heap and the current heap is empty, I can rely on the fact
|
||||||
|
// that the keys/values are in the correct order to put in the underlying
|
||||||
|
// structure.
|
||||||
|
if (this.getCount() <= 0) {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const node = new Node(keys[i], values[i]);
|
||||||
|
node.index = nodes.length;
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "insertAll supports only OpenSeadragon.PriorityQueue object!";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
this.insert(keys[i], values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and removes the root value of this heap.
|
||||||
|
* @return {Node} The root node item removed from the root of the heap. Returns
|
||||||
|
* undefined if the heap is empty.
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
const count = nodes.length;
|
||||||
|
const rootNode = nodes[0];
|
||||||
|
if (count <= 0) {
|
||||||
|
return undefined;
|
||||||
|
} else if (count == 1) { // eslint-disable-line
|
||||||
|
nodes.length = 0;
|
||||||
|
} else {
|
||||||
|
nodes[0] = nodes.pop();
|
||||||
|
if (nodes[0]) {
|
||||||
|
nodes[0].index = 0;
|
||||||
|
}
|
||||||
|
this.moveDown_(0);
|
||||||
|
}
|
||||||
|
if (rootNode) {
|
||||||
|
delete rootNode.index;
|
||||||
|
}
|
||||||
|
return rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves but does not remove the root value of this heap.
|
||||||
|
* @return {V} The value at the root of the heap. Returns
|
||||||
|
* undefined if the heap is empty.
|
||||||
|
*/
|
||||||
|
peek() {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
if (nodes.length == 0) { // eslint-disable-line
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return nodes[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves but does not remove the key of the root node of this heap.
|
||||||
|
* @return {string} The key at the root of the heap. Returns undefined if the
|
||||||
|
* heap is empty.
|
||||||
|
*/
|
||||||
|
peekKey() {
|
||||||
|
return this.nodes_[0] && this.nodes_[0].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the node up in hierarchy
|
||||||
|
* @param {Node} node the node
|
||||||
|
* @param {K} key new ley, must be smaller than current key
|
||||||
|
*/
|
||||||
|
decreaseKey(node, key) {
|
||||||
|
if (node.index === undefined) {
|
||||||
|
node.key = key;
|
||||||
|
this.insertNode(node);
|
||||||
|
} else {
|
||||||
|
node.key = key;
|
||||||
|
this.moveUp_(node.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the node at the given index down to its proper place in the heap.
|
||||||
|
* @param {number} index The index of the node to move down.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
moveDown_(index) {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
const count = nodes.length;
|
||||||
|
|
||||||
|
// Save the node being moved down.
|
||||||
|
const node = nodes[index];
|
||||||
|
// While the current node has a child.
|
||||||
|
while (index < (count >> 1)) {
|
||||||
|
const leftChildIndex = this.getLeftChildIndex_(index);
|
||||||
|
const rightChildIndex = this.getRightChildIndex_(index);
|
||||||
|
|
||||||
|
// Determine the index of the smaller child.
|
||||||
|
const smallerChildIndex = rightChildIndex < count &&
|
||||||
|
nodes[rightChildIndex].key < nodes[leftChildIndex].key ?
|
||||||
|
rightChildIndex :
|
||||||
|
leftChildIndex;
|
||||||
|
|
||||||
|
// If the node being moved down is smaller than its children, the node
|
||||||
|
// has found the correct index it should be at.
|
||||||
|
if (nodes[smallerChildIndex].key > node.key) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not, then take the smaller child as the current node.
|
||||||
|
nodes[index] = nodes[smallerChildIndex];
|
||||||
|
nodes[index].index = index;
|
||||||
|
index = smallerChildIndex;
|
||||||
|
}
|
||||||
|
nodes[index] = node;
|
||||||
|
if (node) {
|
||||||
|
node.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the node at the given index up to its proper place in the heap.
|
||||||
|
* @param {number} index The index of the node to move up.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
moveUp_(index) {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
const node = nodes[index];
|
||||||
|
|
||||||
|
// While the node being moved up is not at the root.
|
||||||
|
while (index > 0) {
|
||||||
|
// If the parent is greater than the node being moved up, move the parent
|
||||||
|
// down.
|
||||||
|
const parentIndex = this.getParentIndex_(index);
|
||||||
|
if (nodes[parentIndex].key > node.key) {
|
||||||
|
nodes[index] = nodes[parentIndex];
|
||||||
|
nodes[index].index = index;
|
||||||
|
index = parentIndex;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes[index] = node;
|
||||||
|
if (node) {
|
||||||
|
node.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the left child of the node at the given index.
|
||||||
|
* @param {number} index The index of the node to get the left child for.
|
||||||
|
* @return {number} The index of the left child.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getLeftChildIndex_(index) {
|
||||||
|
return index * 2 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the right child of the node at the given index.
|
||||||
|
* @param {number} index The index of the node to get the right child for.
|
||||||
|
* @return {number} The index of the right child.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getRightChildIndex_(index) {
|
||||||
|
return index * 2 + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the parent of the node at the given index.
|
||||||
|
* @param {number} index The index of the node to get the parent for.
|
||||||
|
* @return {number} The index of the parent.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getParentIndex_(index) {
|
||||||
|
return (index - 1) >> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the values of the heap.
|
||||||
|
* @return {!Array<*>} The values in the heap.
|
||||||
|
*/
|
||||||
|
getValues() {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
const rv = [];
|
||||||
|
const l = nodes.length;
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
rv.push(nodes[i].value);
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the keys of the heap.
|
||||||
|
* @return {!Array<string>} The keys in the heap.
|
||||||
|
*/
|
||||||
|
getKeys() {
|
||||||
|
const nodes = this.nodes_;
|
||||||
|
const rv = [];
|
||||||
|
const l = nodes.length;
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
rv.push(nodes[i].key);
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the heap contains the given value.
|
||||||
|
* @param {V} val The value to check for.
|
||||||
|
* @return {boolean} Whether the heap contains the value.
|
||||||
|
*/
|
||||||
|
containsValue(val) {
|
||||||
|
return this.nodes_.some((node) => node.value == val); // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the heap contains the given key.
|
||||||
|
* @param {string} key The key to check for.
|
||||||
|
* @return {boolean} Whether the heap contains the key.
|
||||||
|
*/
|
||||||
|
containsKey(key) {
|
||||||
|
return this.nodes_.some((node) => node.value == key); // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones a heap and returns a new heap
|
||||||
|
* @return {!OpenSeadragon.PriorityQueue} A new Heap with the same key-value pairs.
|
||||||
|
*/
|
||||||
|
clone() {
|
||||||
|
return new $.PriorityQueue(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of key-value pairs in the map
|
||||||
|
* @return {number} The number of pairs.
|
||||||
|
*/
|
||||||
|
getCount() {
|
||||||
|
return this.nodes_.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this heap contains no elements.
|
||||||
|
* @return {boolean} Whether this heap contains no elements.
|
||||||
|
*/
|
||||||
|
isEmpty() {
|
||||||
|
return this.nodes_.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from the heap.
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.nodes_.length = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.PriorityQueue.Node = class {
|
||||||
|
constructor(key, value) {
|
||||||
|
/**
|
||||||
|
* The key.
|
||||||
|
* @private {K}
|
||||||
|
*/
|
||||||
|
this.key = key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value.
|
||||||
|
* @private {V}
|
||||||
|
*/
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node index value. Updated in the heap.
|
||||||
|
* @type {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new Node(this.key, this.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}(OpenSeadragon));
|
209
src/tile.js
209
src/tile.js
@ -45,8 +45,8 @@
|
|||||||
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
|
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
|
||||||
* this tile failed to load? )
|
* this tile failed to load? )
|
||||||
* @param {String|Function} url The URL of this tile's image or a function that returns a url.
|
* @param {String|Function} url The URL of this tile's image or a function that returns a url.
|
||||||
* @param {CanvasRenderingContext2D} context2D The context2D of this tile if it
|
* @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it
|
||||||
* is provided directly by the tile source.
|
* * is provided directly by the tile source. Deprecated: use Tile::setCache(...) instead.
|
||||||
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
|
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
|
||||||
* @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable).
|
* @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable).
|
||||||
* @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the
|
* @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the
|
||||||
@ -115,7 +115,9 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
|||||||
* @member {CanvasRenderingContext2D} context2D
|
* @member {CanvasRenderingContext2D} context2D
|
||||||
* @memberOf OpenSeadragon.Tile#
|
* @memberOf OpenSeadragon.Tile#
|
||||||
*/
|
*/
|
||||||
this.context2D = context2D;
|
if (context2D) {
|
||||||
|
this.context2D = context2D;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Whether to load this tile's image with an AJAX request.
|
* Whether to load this tile's image with an AJAX request.
|
||||||
* @member {Boolean} loadWithAjax
|
* @member {Boolean} loadWithAjax
|
||||||
@ -136,11 +138,21 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
|||||||
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
|
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The unique cache key for this tile.
|
* The unique main cache key for this tile. Created automatically
|
||||||
|
* from the given tiledImage.source.getTileHashKey(...) implementation.
|
||||||
* @member {String} cacheKey
|
* @member {String} cacheKey
|
||||||
* @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
|
||||||
@ -252,6 +264,24 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
|
|||||||
* @memberof OpenSeadragon.Tile#
|
* @memberof OpenSeadragon.Tile#
|
||||||
*/
|
*/
|
||||||
this.isBottomMost = false;
|
this.isBottomMost = false;
|
||||||
|
/**
|
||||||
|
* FIXME: I would like to remove this reference but there is no way
|
||||||
|
* to remove it since tile-unloaded event requires the tiledImage reference.
|
||||||
|
* And, unloadTilesFor(tiledImage) in cache uses it too. Storing the
|
||||||
|
* reference on a tile level rather than cache level is more efficient.
|
||||||
|
*
|
||||||
|
* Owner of this tile.
|
||||||
|
* @member {OpenSeadragon.TiledImage}
|
||||||
|
* @memberof OpenSeadragon.Tile#
|
||||||
|
*/
|
||||||
|
this.tiledImage = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of cached tile data associated with the tile.
|
||||||
|
* @member {Object} _caches
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._caches = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @lends OpenSeadragon.Tile.prototype */
|
/** @lends OpenSeadragon.Tile.prototype */
|
||||||
@ -267,26 +297,12 @@ $.Tile.prototype = {
|
|||||||
return this.level + "/" + this.x + "_" + this.y;
|
return this.level + "/" + this.x + "_" + this.y;
|
||||||
},
|
},
|
||||||
|
|
||||||
// private
|
|
||||||
_hasTransparencyChannel: function() {
|
|
||||||
console.warn("Tile.prototype._hasTransparencyChannel() has been " +
|
|
||||||
"deprecated and will be removed in the future. Use TileSource.prototype.hasTransparency() instead.");
|
|
||||||
return !!this.context2D || this.getUrl().match('.png');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the tile in an html container.
|
* Renders the tile in an html container.
|
||||||
* @function
|
* @function
|
||||||
* @param {Element} container
|
* @param {Element} container
|
||||||
*/
|
*/
|
||||||
drawHTML: function( container ) {
|
drawHTML: function( container ) {
|
||||||
if (!this.cacheImageRecord) {
|
|
||||||
$.console.warn(
|
|
||||||
'[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
|
|
||||||
this.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !this.loaded ) {
|
if ( !this.loaded ) {
|
||||||
$.console.warn(
|
$.console.warn(
|
||||||
"Attempting to draw tile %s when it's not yet loaded.",
|
"Attempting to draw tile %s when it's not yet loaded.",
|
||||||
@ -297,10 +313,12 @@ $.Tile.prototype = {
|
|||||||
|
|
||||||
//EXPERIMENTAL - trying to figure out how to scale the container
|
//EXPERIMENTAL - trying to figure out how to scale the container
|
||||||
// content during animation of the container size.
|
// content during animation of the container size.
|
||||||
|
|
||||||
if ( !this.element ) {
|
if ( !this.element ) {
|
||||||
var image = this.getImage();
|
const image = this.getImage();
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
$.console.warn(
|
||||||
|
'[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
|
||||||
|
this.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,10 +376,10 @@ $.Tile.prototype = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Image object for this tile.
|
* Get the Image object for this tile.
|
||||||
* @returns {Image}
|
* @returns {?Image}
|
||||||
*/
|
*/
|
||||||
getImage: function() {
|
getImage: function() {
|
||||||
return this.cacheImageRecord.getImage();
|
return this.getData("image");
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,41 +397,158 @@ $.Tile.prototype = {
|
|||||||
/**
|
/**
|
||||||
* Get the CanvasRenderingContext2D instance for tile image data drawn
|
* Get the CanvasRenderingContext2D instance for tile image data drawn
|
||||||
* onto Canvas if enabled and available
|
* onto Canvas if enabled and available
|
||||||
* @returns {CanvasRenderingContext2D}
|
* @returns {?CanvasRenderingContext2D}
|
||||||
*/
|
*/
|
||||||
getCanvasContext: function() {
|
getCanvasContext: function() {
|
||||||
return this.context2D || this.cacheImageRecord.getRenderedContext();
|
return this.getData("context2d");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context2D of this tile if it is provided directly by the tile source.
|
||||||
|
* @deprecated
|
||||||
|
* @type {CanvasRenderingContext2D} context2D
|
||||||
|
*/
|
||||||
|
get context2D() {
|
||||||
|
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::getCache().");
|
||||||
|
return this.getData("context2d");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context2D of this tile if it is provided directly by the tile source.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
set context2D(value) {
|
||||||
|
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::setCache().");
|
||||||
|
this.setData(value, "context2d");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default cache for this tile.
|
||||||
|
* @deprecated
|
||||||
|
* @type OpenSeadragon.CacheRecord
|
||||||
|
*/
|
||||||
|
get cacheImageRecord() {
|
||||||
|
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::getCache.");
|
||||||
|
return this.getCache(this.cacheKey);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default cache for this tile.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
set cacheImageRecord(value) {
|
||||||
|
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::setCache.");
|
||||||
|
this._caches[this.cacheKey] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default data for this tile
|
||||||
|
* @param {?string} [type=undefined] data type to require
|
||||||
|
*/
|
||||||
|
getData(type = undefined) {
|
||||||
|
const cache = this.getCache(this.cacheKey);
|
||||||
|
if (!cache) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
const parent = this.tiledImage;
|
||||||
|
if (parent) {
|
||||||
|
parent._needsDraw = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache data
|
||||||
|
* @param {*} value
|
||||||
|
* @param {?string} [type=undefined] data type to require
|
||||||
|
*/
|
||||||
|
setData(value, type = undefined) {
|
||||||
|
this.setCache(this.cacheKey, value, type);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read tile cache data object (CacheRecord)
|
||||||
|
* @param {string} key cache key to read that belongs to this tile
|
||||||
|
* @return {OpenSeadragon.CacheRecord}
|
||||||
|
*/
|
||||||
|
getCache: function(key) {
|
||||||
|
return this._caches[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set tile cache, possibly multiple with custom key
|
||||||
|
* @param {string} key cache key, must be unique (we recommend re-using this.cacheTile
|
||||||
|
* value and extend it with some another unique content, by default overrides the existing
|
||||||
|
* main cache used for drawing, if not existing.
|
||||||
|
* @param {*} data data to cache - this data will be sent to the TileSource API for refinement.
|
||||||
|
* @param {?string} type data type, will be guessed if not provided
|
||||||
|
* @param [_safely=true] private
|
||||||
|
* @param [_cutoff=0] private
|
||||||
|
*/
|
||||||
|
setCache: function(key, data, type = undefined, _safely = true, _cutoff = 0) {
|
||||||
|
type = type || $.convertor.guessType(data);
|
||||||
|
|
||||||
|
if (_safely && key === this.cacheKey) {
|
||||||
|
//todo later, we could have drawers register their supported rendering type
|
||||||
|
// and OpenSeadragon would check compatibility automatically, now we render
|
||||||
|
// using two main types so we check their ability
|
||||||
|
const conversion = $.convertor.getConversionPath(type, "canvas", "image");
|
||||||
|
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" +
|
||||||
|
"to render. Make sure OpenSeadragon.convertor was taught to convert type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tiledImage._tileCache.cacheTile({
|
||||||
|
data: data,
|
||||||
|
dataType: type,
|
||||||
|
tile: this,
|
||||||
|
cacheKey: key,
|
||||||
|
cutoff: _cutoff
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
* @param {Canvas} context
|
* @param {CanvasRenderingContext2D} context
|
||||||
* @param {Function} drawingHandler - Method for firing the drawing event.
|
* @param {Function} drawingHandler - Method for firing the drawing event.
|
||||||
* drawingHandler({context, tile, rendered})
|
* drawingHandler({context, tile, rendered})
|
||||||
* where <code>rendered</code> is the context with the pre-drawn image.
|
* where <code>rendered</code> is the context with the pre-drawn image.
|
||||||
* @param {Number} [scale=1] - Apply a scale to position and size
|
* @param {Number} [scale=1] - Apply a scale to position and size
|
||||||
* @param {OpenSeadragon.Point} [translate] - A translation vector
|
* @param {OpenSeadragon.Point} [translate] - A translation vector
|
||||||
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
|
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
|
||||||
* position and size of tiles supporting alpha channel in non-transparency
|
* position and size of tiles supporting alpha channel in non-transparency context.
|
||||||
* context.
|
|
||||||
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
|
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
|
||||||
*/
|
*/
|
||||||
drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
|
drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
|
||||||
|
|
||||||
var position = this.position.times($.pixelDensityRatio),
|
var position = this.position.times($.pixelDensityRatio),
|
||||||
size = this.size.times($.pixelDensityRatio),
|
size = this.size.times($.pixelDensityRatio),
|
||||||
rendered;
|
rendered = this.getCanvasContext();
|
||||||
|
|
||||||
if (!this.context2D && !this.cacheImageRecord) {
|
if (!rendered) {
|
||||||
$.console.warn(
|
$.console.warn(
|
||||||
'[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
|
'[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
|
||||||
this.toString());
|
this.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rendered = this.getCanvasContext();
|
|
||||||
|
|
||||||
if ( !this.loaded || !rendered ){
|
if ( !this.loaded || !rendered ){
|
||||||
$.console.warn(
|
$.console.warn(
|
||||||
"Attempting to draw tile %s when it's not yet loaded.",
|
"Attempting to draw tile %s when it's not yet loaded.",
|
||||||
@ -495,15 +630,11 @@ $.Tile.prototype = {
|
|||||||
/**
|
/**
|
||||||
* Get the ratio between current and original size.
|
* Get the ratio between current and original size.
|
||||||
* @function
|
* @function
|
||||||
* @returns {Float}
|
* @returns {Number}
|
||||||
*/
|
*/
|
||||||
getScaleForEdgeSmoothing: function() {
|
getScaleForEdgeSmoothing: function() {
|
||||||
var context;
|
const context = this.getCanvasContext();
|
||||||
if (this.cacheImageRecord) {
|
if (!context) {
|
||||||
context = this.cacheImageRecord.getRenderedContext();
|
|
||||||
} else if (this.context2D) {
|
|
||||||
context = this.context2D;
|
|
||||||
} else {
|
|
||||||
$.console.warn(
|
$.console.warn(
|
||||||
'[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached',
|
'[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached',
|
||||||
this.toString());
|
this.toString());
|
||||||
@ -548,6 +679,8 @@ $.Tile.prototype = {
|
|||||||
this.element.parentNode.removeChild( this.element );
|
this.element.parentNode.removeChild( this.element );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tiledImage = null;
|
||||||
|
this._caches = [];
|
||||||
this.element = null;
|
this.element = null;
|
||||||
this.imgElement = null;
|
this.imgElement = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
496
src/tilecache.js
496
src/tilecache.js
@ -34,55 +34,285 @@
|
|||||||
|
|
||||||
(function( $ ){
|
(function( $ ){
|
||||||
|
|
||||||
// private class
|
/**
|
||||||
var TileRecord = function( options ) {
|
* Cached Data Record, the cache object.
|
||||||
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
* Keeps only latest object type required.
|
||||||
$.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
|
*
|
||||||
$.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" );
|
* This class acts like the Maybe type:
|
||||||
this.tile = options.tile;
|
* - it has 'loaded' flag indicating whether the tile data is ready
|
||||||
this.tiledImage = options.tiledImage;
|
* - 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 {{
|
||||||
|
* destroy: function,
|
||||||
|
* save: function,
|
||||||
|
* getData: function,
|
||||||
|
* data: ?,
|
||||||
|
* loaded: boolean
|
||||||
|
* }} OpenSeadragon.CacheRecord
|
||||||
|
*/
|
||||||
|
$.CacheRecord = class {
|
||||||
|
constructor() {
|
||||||
|
this._tiles = [];
|
||||||
|
this._data = null;
|
||||||
|
this.loaded = false;
|
||||||
|
this._promise = $.Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// private class
|
destroy() {
|
||||||
var ImageRecord = function(options) {
|
|
||||||
$.console.assert( options, "[ImageRecord] options is required" );
|
|
||||||
$.console.assert( options.data, "[ImageRecord] options.data is required" );
|
|
||||||
this._tiles = [];
|
|
||||||
|
|
||||||
options.create.apply(null, [this, options.data, options.ownerTile]);
|
|
||||||
this._destroyImplementation = options.destroy.bind(null, this);
|
|
||||||
this.getImage = options.getImage.bind(null, this);
|
|
||||||
this.getData = options.getData.bind(null, this);
|
|
||||||
this.getRenderedContext = options.getRenderedContext.bind(null, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
ImageRecord.prototype = {
|
|
||||||
destroy: function() {
|
|
||||||
this._destroyImplementation();
|
|
||||||
this._tiles = null;
|
this._tiles = null;
|
||||||
},
|
this._data = 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() {
|
||||||
|
for (let tile of this._tiles) {
|
||||||
|
tile._needsDraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(type = this._type) {
|
||||||
|
if (type !== this._type) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
$.console.warn("Attempt to call getData with desired type %s, the tile data type is %s and the tile is not loaded!", type, this._type);
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
this._convert(this._type, type);
|
||||||
|
}
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tile dependency on this record
|
||||||
|
* @param tile
|
||||||
|
* @param data
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
addTile(tile, data, type) {
|
||||||
|
$.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||||
|
|
||||||
|
//allow overriding the cache - existing tile or different type
|
||||||
|
if (this._tiles.includes(tile)) {
|
||||||
|
this.removeTile(tile);
|
||||||
|
|
||||||
|
} else if (!this.loaded) {
|
||||||
|
this._type = type;
|
||||||
|
this._promise = $.Promise.resolve(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);
|
||||||
|
}
|
||||||
|
|
||||||
addTile: function(tile) {
|
|
||||||
$.console.assert(tile, '[ImageRecord.addTile] tile is required');
|
|
||||||
this._tiles.push(tile);
|
this._tiles.push(tile);
|
||||||
},
|
}
|
||||||
|
|
||||||
removeTile: function(tile) {
|
/**
|
||||||
for (var i = 0; i < this._tiles.length; i++) {
|
* Remove tile dependency on this record.
|
||||||
|
* @param tile
|
||||||
|
*/
|
||||||
|
removeTile(tile) {
|
||||||
|
for (let i = 0; i < this._tiles.length; i++) {
|
||||||
if (this._tiles[i] === tile) {
|
if (this._tiles[i] === tile) {
|
||||||
this._tiles.splice(i, 1);
|
this._tiles.splice(i, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.console.warn('[ImageRecord.removeTile] trying to remove unknown tile', tile);
|
$.console.warn('[CacheRecord.removeTile] trying to remove unknown tile', tile);
|
||||||
},
|
}
|
||||||
|
|
||||||
getTileCount: function() {
|
/**
|
||||||
|
* Get the amount of tiles sharing this record.
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
// converted to a canvas -> the image record gets thrown away)
|
||||||
|
//
|
||||||
|
//FIXME: Note that this can be also achieved somewhat by caching the midresults
|
||||||
|
// as a single cache object instead. Also, there is the problem of lifecycle-oriented
|
||||||
|
// data types such as WebGL textures we want to unload manually: this looks like
|
||||||
|
// we really want to cache midresuls and have their custom destructors
|
||||||
|
// $.MemoryCacheRecord = class extends $.CacheRecord {
|
||||||
|
// constructor(memorySize) {
|
||||||
|
// super();
|
||||||
|
// this.length = memorySize;
|
||||||
|
// this.index = 0;
|
||||||
|
// this.content = [];
|
||||||
|
// this.types = [];
|
||||||
|
// this.defaultType = "image";
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // overrides:
|
||||||
|
//
|
||||||
|
// destroy() {
|
||||||
|
// super.destroy();
|
||||||
|
// this.types = null;
|
||||||
|
// this.content = null;
|
||||||
|
// this.types = null;
|
||||||
|
// this.defaultType = null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// getData(type = this.defaultType) {
|
||||||
|
// let item = this.add(type, undefined);
|
||||||
|
// if (item === undefined) {
|
||||||
|
// //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);
|
||||||
|
// }
|
||||||
|
// return item;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * @deprecated
|
||||||
|
// */
|
||||||
|
// get data() {
|
||||||
|
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use getData(...) instead!");
|
||||||
|
// return this.current();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * @deprecated
|
||||||
|
// * @param value
|
||||||
|
// */
|
||||||
|
// set data(value) {
|
||||||
|
// //FIXME: addTile bit bad name, related to the issue mentioned elsewhere
|
||||||
|
// $.console.warn("[MemoryCacheRecord.data] is deprecated property. Use addTile(...) instead!");
|
||||||
|
// this.defaultType = $.convertor.guessType(value);
|
||||||
|
// this.add(this.defaultType, value);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// addTile(tile, data, type) {
|
||||||
|
// $.console.assert(tile, '[CacheRecord.addTile] tile is required');
|
||||||
|
//
|
||||||
|
// //allow overriding the cache - existing tile or different type
|
||||||
|
// if (this._tiles.includes(tile)) {
|
||||||
|
// this.removeTile(tile);
|
||||||
|
// } else if (!this.defaultType !== type) {
|
||||||
|
// this.defaultType = type;
|
||||||
|
// this.add(type, data);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// this._tiles.push(tile);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // extends:
|
||||||
|
//
|
||||||
|
// add(type, item) {
|
||||||
|
// const index = this.hasIndex(type);
|
||||||
|
// if (index > -1) {
|
||||||
|
// //no index change, swap (optimally, move all by one - too expensive...)
|
||||||
|
// item = this.content[index];
|
||||||
|
// this.content[index] = this.content[this.index];
|
||||||
|
// } else {
|
||||||
|
// this.index = (this.index + 1) % this.length;
|
||||||
|
// }
|
||||||
|
// this.content[this.index] = item;
|
||||||
|
// this.types[this.index] = type;
|
||||||
|
// return item;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// has(type) {
|
||||||
|
// for (let i = 0; i < this.types.length; i++) {
|
||||||
|
// const t = this.types[i];
|
||||||
|
// if (t === type) {
|
||||||
|
// return this.content[i];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// hasIndex(type) {
|
||||||
|
// for (let i = 0; i < this.types.length; i++) {
|
||||||
|
// const t = this.types[i];
|
||||||
|
// if (t === type) {
|
||||||
|
// return i;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return -1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// current() {
|
||||||
|
// return this.content[this.index];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// currentType() {
|
||||||
|
// return this.types[this.index];
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class TileCache
|
* @class TileCache
|
||||||
* @memberof OpenSeadragon
|
* @memberof OpenSeadragon
|
||||||
@ -92,24 +322,24 @@ ImageRecord.prototype = {
|
|||||||
* @param {Number} [options.maxImageCacheCount] - See maxImageCacheCount in
|
* @param {Number} [options.maxImageCacheCount] - See maxImageCacheCount in
|
||||||
* {@link OpenSeadragon.Options} for details.
|
* {@link OpenSeadragon.Options} for details.
|
||||||
*/
|
*/
|
||||||
$.TileCache = function( options ) {
|
$.TileCache = class {
|
||||||
options = options || {};
|
constructor( options ) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
this._maxImageCacheCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount;
|
this._maxCacheItemCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount;
|
||||||
this._tilesLoaded = [];
|
this._tilesLoaded = [];
|
||||||
this._imagesLoaded = [];
|
this._cachesLoaded = [];
|
||||||
this._imagesLoadedCount = 0;
|
this._cachesLoadedCount = 0;
|
||||||
};
|
}
|
||||||
|
|
||||||
/** @lends OpenSeadragon.TileCache.prototype */
|
|
||||||
$.TileCache.prototype = {
|
|
||||||
/**
|
/**
|
||||||
* @returns {Number} The total number of tiles that have been loaded by
|
* @returns {Number} The total number of tiles that have been loaded by
|
||||||
* this TileCache.
|
* this TileCache. Note that the tile might be recorded here mutliple times,
|
||||||
|
* once for each cache it uses.
|
||||||
*/
|
*/
|
||||||
numTilesLoaded: function() {
|
numTilesLoaded() {
|
||||||
return this._tilesLoaded.length;
|
return this._tilesLoaded.length;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches the specified tile, removing an old tile if necessary to stay under the
|
* Caches the specified tile, removing an old tile if necessary to stay under the
|
||||||
@ -119,24 +349,27 @@ $.TileCache.prototype = {
|
|||||||
* may temporarily surpass that number, but should eventually come back down to the max specified.
|
* may temporarily surpass that number, but should eventually come back down to the max specified.
|
||||||
* @param {Object} options - Tile info.
|
* @param {Object} options - Tile info.
|
||||||
* @param {OpenSeadragon.Tile} options.tile - The tile to cache.
|
* @param {OpenSeadragon.Tile} options.tile - The tile to cache.
|
||||||
|
* @param {String} [options.cacheKey=undefined] - Cache Key to use. Defaults to options.tile.cacheKey
|
||||||
* @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache.
|
* @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache.
|
||||||
* @param {Image} options.image - The image of the tile to cache.
|
* Used if cacheKey not set.
|
||||||
* @param {OpenSeadragon.TiledImage} options.tiledImage - The TiledImage that owns that tile.
|
* @param {Image} options.image - The image of the tile to cache. Deprecated.
|
||||||
|
* @param {*} options.data - The data of the tile to cache.
|
||||||
|
* @param {string} [options.dataType] - The data type of the tile to cache. Required.
|
||||||
* @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this
|
* @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this
|
||||||
* function will release an old tile. The cutoff option specifies a tile level at or below which
|
* function will release an old tile. The cutoff option specifies a tile level at or below which
|
||||||
* tiles will not be released.
|
* tiles will not be released.
|
||||||
*/
|
*/
|
||||||
cacheTile: function( options ) {
|
cacheTile( options ) {
|
||||||
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
$.console.assert( options, "[TileCache.cacheTile] options is required" );
|
||||||
$.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
|
$.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" );
|
||||||
$.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
|
$.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" );
|
||||||
$.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" );
|
|
||||||
|
|
||||||
var cutoff = options.cutoff || 0;
|
let cutoff = options.cutoff || 0,
|
||||||
var insertionIndex = this._tilesLoaded.length;
|
insertionIndex = this._tilesLoaded.length,
|
||||||
|
cacheKey = options.cacheKey || options.tile.cacheKey;
|
||||||
|
|
||||||
var imageRecord = this._imagesLoaded[options.tile.cacheKey];
|
let cacheRecord = this._cachesLoaded[options.tile.cacheKey];
|
||||||
if (!imageRecord) {
|
if (!cacheRecord) {
|
||||||
|
|
||||||
if (!options.data) {
|
if (!options.data) {
|
||||||
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
|
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
|
||||||
@ -144,41 +377,54 @@ $.TileCache.prototype = {
|
|||||||
options.data = options.image;
|
options.data = options.image;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an ImageRecord" );
|
$.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an CacheRecord" );
|
||||||
imageRecord = this._imagesLoaded[options.tile.cacheKey] = new ImageRecord({
|
cacheRecord = this._cachesLoaded[options.tile.cacheKey] = new $.CacheRecord();
|
||||||
data: options.data,
|
this._cachesLoadedCount++;
|
||||||
ownerTile: options.tile,
|
} else if (cacheRecord.__zombie__) {
|
||||||
create: options.tiledImage.source.createTileCache,
|
delete cacheRecord.__zombie__;
|
||||||
destroy: options.tiledImage.source.destroyTileCache,
|
//revive cache, replace from _tilesLoaded so it won't get unloaded
|
||||||
getImage: options.tiledImage.source.getTileCacheDataAsImage,
|
this._tilesLoaded.splice( cacheRecord.__index__, 1 );
|
||||||
getData: options.tiledImage.source.getTileCacheData,
|
delete cacheRecord.__index__;
|
||||||
getRenderedContext: options.tiledImage.source.getTileCacheDataAsContext2D,
|
insertionIndex--;
|
||||||
});
|
|
||||||
|
|
||||||
this._imagesLoadedCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageRecord.addTile(options.tile);
|
if (!options.dataType) {
|
||||||
options.tile.cacheImageRecord = imageRecord;
|
$.console.error("[TileCache.cacheTile] options.dataType is newly required. " +
|
||||||
|
"For easier use of the cache system, use the tile instance API.");
|
||||||
|
options.dataType = $.convertor.guessType(options.data);
|
||||||
|
}
|
||||||
|
cacheRecord.addTile(options.tile, options.data, options.dataType);
|
||||||
|
options.tile._caches[ cacheKey ] = cacheRecord;
|
||||||
|
|
||||||
// Note that just because we're unloading a tile doesn't necessarily mean
|
// Note that just because we're unloading a tile doesn't necessarily mean
|
||||||
// we're unloading an image. With repeated calls it should sort itself out, though.
|
// we're unloading its cache records. With repeated calls it should sort itself out, though.
|
||||||
if ( this._imagesLoadedCount > this._maxImageCacheCount ) {
|
if ( this._cachesLoadedCount > this._maxCacheItemCount ) {
|
||||||
var worstTile = null;
|
let worstTile = null;
|
||||||
var worstTileIndex = -1;
|
let worstTileIndex = -1;
|
||||||
var worstTileRecord = null;
|
let prevTile, worstTime, worstLevel, prevTime, prevLevel;
|
||||||
var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord;
|
|
||||||
|
|
||||||
for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||||
prevTileRecord = this._tilesLoaded[ i ];
|
prevTile = this._tilesLoaded[ i ];
|
||||||
prevTile = prevTileRecord.tile;
|
|
||||||
|
//todo try different approach? the only ugly part, keep tilesLoaded array empty of unloaded tiles
|
||||||
|
if (!prevTile.loaded) {
|
||||||
|
//iterates from the array end, safe to remove
|
||||||
|
this._tilesLoaded.splice( i, 1 );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( prevTile.__zombie__ !== undefined ) {
|
||||||
|
//remove without hesitation, CacheRecord instance
|
||||||
|
worstTile = prevTile.__zombie__;
|
||||||
|
worstTileIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ( prevTile.level <= cutoff || prevTile.beingDrawn ) {
|
if ( prevTile.level <= cutoff || prevTile.beingDrawn ) {
|
||||||
continue;
|
continue;
|
||||||
} else if ( !worstTile ) {
|
} else if ( !worstTile ) {
|
||||||
worstTile = prevTile;
|
worstTile = prevTile;
|
||||||
worstTileIndex = i;
|
worstTileIndex = i;
|
||||||
worstTileRecord = prevTileRecord;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,64 +434,91 @@ $.TileCache.prototype = {
|
|||||||
worstLevel = worstTile.level;
|
worstLevel = worstTile.level;
|
||||||
|
|
||||||
if ( prevTime < worstTime ||
|
if ( prevTime < worstTime ||
|
||||||
( prevTime === worstTime && prevLevel > worstLevel ) ) {
|
( prevTime === worstTime && prevLevel > worstLevel )) {
|
||||||
worstTile = prevTile;
|
worstTile = prevTile;
|
||||||
worstTileIndex = i;
|
worstTileIndex = i;
|
||||||
worstTileRecord = prevTileRecord;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( worstTile && worstTileIndex >= 0 ) {
|
if ( worstTile && worstTileIndex >= 0 ) {
|
||||||
this._unloadTile(worstTileRecord);
|
this._unloadTile(worstTile, true);
|
||||||
insertionIndex = worstTileIndex;
|
insertionIndex = worstTileIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tilesLoaded[ insertionIndex ] = new TileRecord({
|
this._tilesLoaded[ insertionIndex ] = options.tile;
|
||||||
tile: options.tile,
|
}
|
||||||
tiledImage: options.tiledImage
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all tiles associated with the specified tiledImage.
|
* Clears all tiles associated with the specified tiledImage.
|
||||||
* @param {OpenSeadragon.TiledImage} tiledImage
|
* @param {OpenSeadragon.TiledImage} tiledImage
|
||||||
*/
|
*/
|
||||||
clearTilesFor: function( tiledImage ) {
|
clearTilesFor( tiledImage ) {
|
||||||
$.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required');
|
$.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required');
|
||||||
var tileRecord;
|
let tile;
|
||||||
for ( var i = 0; i < this._tilesLoaded.length; ++i ) {
|
for ( let i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
|
||||||
tileRecord = this._tilesLoaded[ i ];
|
tile = this._tilesLoaded[ i ];
|
||||||
if ( tileRecord.tiledImage === tiledImage ) {
|
|
||||||
this._unloadTile(tileRecord);
|
//todo try different approach? the only ugly part, keep tilesLoaded array empty of unloaded tiles
|
||||||
|
if (!tile.loaded) {
|
||||||
|
//iterates from the array end, safe to remove
|
||||||
this._tilesLoaded.splice( i, 1 );
|
this._tilesLoaded.splice( i, 1 );
|
||||||
i--;
|
i--;
|
||||||
|
} else if ( tile.tiledImage === tiledImage ) {
|
||||||
|
this._unloadTile(tile, !tiledImage._zombieCache ||
|
||||||
|
this._cachesLoadedCount > this._maxCacheItemCount, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
getImageRecord: function(cacheKey) {
|
getCacheRecord(cacheKey) {
|
||||||
$.console.assert(cacheKey, '[TileCache.getImageRecord] cacheKey is required');
|
$.console.assert(cacheKey, '[TileCache.getCacheRecord] cacheKey is required');
|
||||||
return this._imagesLoaded[cacheKey];
|
return this._cachesLoaded[cacheKey];
|
||||||
},
|
}
|
||||||
|
|
||||||
// private
|
/**
|
||||||
_unloadTile: function(tileRecord) {
|
* @param tile tile to unload
|
||||||
$.console.assert(tileRecord, '[TileCache._unloadTile] tileRecord is required');
|
* @param destroy destroy tile cache if the cache tile counts falls to zero
|
||||||
var tile = tileRecord.tile;
|
* @param deleteAtIndex index to remove the tile record at, will not remove from _tiledLoaded if not set
|
||||||
var tiledImage = tileRecord.tiledImage;
|
* @private
|
||||||
|
*/
|
||||||
|
_unloadTile(tile, destroy, deleteAtIndex) {
|
||||||
|
$.console.assert(tile, '[TileCache._unloadTile] tile is required');
|
||||||
|
|
||||||
tile.unload();
|
for (let key in tile._caches) {
|
||||||
tile.cacheImageRecord = null;
|
const cacheRecord = this._cachesLoaded[key];
|
||||||
|
if (cacheRecord) {
|
||||||
|
cacheRecord.removeTile(tile);
|
||||||
|
if (!cacheRecord.getTileCount()) {
|
||||||
|
if (destroy) {
|
||||||
|
// #1 tile marked as destroyed (e.g. too much cached tiles or not a zombie)
|
||||||
|
cacheRecord.destroy();
|
||||||
|
delete this._cachesLoaded[tile.cacheKey];
|
||||||
|
this._cachesLoadedCount--;
|
||||||
|
|
||||||
var imageRecord = this._imagesLoaded[tile.cacheKey];
|
//delete also the tile record
|
||||||
imageRecord.removeTile(tile);
|
if (deleteAtIndex !== undefined) {
|
||||||
if (!imageRecord.getTileCount()) {
|
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||||
imageRecord.destroy();
|
}
|
||||||
delete this._imagesLoaded[tile.cacheKey];
|
} else if (deleteAtIndex !== undefined) {
|
||||||
this._imagesLoadedCount--;
|
// #2 Tile is a zombie. Do not delete record, reuse.
|
||||||
|
// a bit dirty but performant... -> we can remove later, or revive
|
||||||
|
// we can do this, in array the tile is once for each its cache object
|
||||||
|
this._tilesLoaded[ deleteAtIndex ] = cacheRecord;
|
||||||
|
cacheRecord.__zombie__ = tile;
|
||||||
|
cacheRecord.__index__ = deleteAtIndex;
|
||||||
|
}
|
||||||
|
} else if (deleteAtIndex !== undefined) {
|
||||||
|
// #3 Cache stays. Tile record needs to be removed anyway, since the tile is removed.
|
||||||
|
this._tilesLoaded.splice( deleteAtIndex, 1 );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.console.warn("[TileCache._unloadTile] Attempting to delete missing cache!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const tiledImage = tile.tiledImage;
|
||||||
|
tile.unload();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when a tile has just been unloaded from memory.
|
* Triggered when a tile has just been unloaded from memory.
|
||||||
@ -263,4 +536,5 @@ $.TileCache.prototype = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}( OpenSeadragon ));
|
}( OpenSeadragon ));
|
||||||
|
@ -161,8 +161,9 @@ $.TiledImage = function( options ) {
|
|||||||
lastResetTime: 0, // Last time for which the tiledImage was reset.
|
lastResetTime: 0, // Last time for which the tiledImage was reset.
|
||||||
_midDraw: false, // Is the tiledImage currently updating the viewport?
|
_midDraw: false, // Is the tiledImage currently updating the viewport?
|
||||||
_needsDraw: true, // Does the tiledImage need to update the viewport again?
|
_needsDraw: true, // Does the tiledImage need to update the viewport again?
|
||||||
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
|
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
|
||||||
_tilesLoading: 0, // The number of pending tile requests.
|
_tilesLoading: 0, // The number of pending tile requests.
|
||||||
|
_zombieCache: false, // Allow cache to stay in memory upon deletion.
|
||||||
//configurable settings
|
//configurable settings
|
||||||
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
|
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
|
||||||
animationTime: $.DEFAULT_SETTINGS.animationTime,
|
animationTime: $.DEFAULT_SETTINGS.animationTime,
|
||||||
@ -337,6 +338,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
if (this.source.destroy) {
|
if (this.source.destroy) {
|
||||||
|
$.console.warn("[TileSource.destroy] is deprecated. Use advanced data model API.");
|
||||||
this.source.destroy();
|
this.source.destroy();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1094,6 +1096,18 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable cache preservation even without this tile image,
|
||||||
|
* by default disabled. It means that upon removing,
|
||||||
|
* the tile cache does not get immediately erased but
|
||||||
|
* stays in the memory to be potentially re-used by other
|
||||||
|
* TiledImages.
|
||||||
|
* @param {boolean} allow
|
||||||
|
*/
|
||||||
|
allowZombieCache: function(allow) {
|
||||||
|
this._zombieCache = allow;
|
||||||
|
},
|
||||||
|
|
||||||
// private
|
// private
|
||||||
_setScale: function(scale, immediately) {
|
_setScale: function(scale, immediately) {
|
||||||
var sameTarget = (this._scaleSpring.target.value === scale);
|
var sameTarget = (this._scaleSpring.target.value === scale);
|
||||||
@ -1277,11 +1291,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
|
|
||||||
// Load the new 'best' n tiles
|
// Load the new 'best' n tiles
|
||||||
if (bestTiles && bestTiles.length > 0) {
|
if (bestTiles && bestTiles.length > 0) {
|
||||||
bestTiles.forEach(function (tile) {
|
for (let tile of bestTiles) {
|
||||||
if (tile && !tile.context2D) {
|
if (tile) {
|
||||||
this._loadTile(tile, currentTime);
|
this._loadTile(tile, currentTime);
|
||||||
}
|
}
|
||||||
}, this);
|
}
|
||||||
this._needsDraw = true;
|
this._needsDraw = true;
|
||||||
this._setFullyLoaded(false);
|
this._setFullyLoaded(false);
|
||||||
} else {
|
} else {
|
||||||
@ -1460,12 +1474,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
|
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
|
||||||
|
|
||||||
var tile = this._getTile(
|
var tile = this._getTile(
|
||||||
x, y,
|
x, y,
|
||||||
level,
|
level,
|
||||||
currentTime,
|
currentTime,
|
||||||
numberOfTiles,
|
numberOfTiles,
|
||||||
this._worldWidthCurrent,
|
this._worldWidthCurrent,
|
||||||
this._worldHeightCurrent
|
this._worldHeightCurrent
|
||||||
),
|
),
|
||||||
drawTile = drawLevel;
|
drawTile = drawLevel;
|
||||||
|
|
||||||
@ -1516,13 +1530,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
levelVisibility
|
levelVisibility
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!tile.loaded) {
|
if (!tile.loaded && !tile.loading) {
|
||||||
if (tile.context2D) {
|
// Tile was created or its data removed: check whether cache has the data before downloading.
|
||||||
this._setTileLoaded(tile);
|
if (!tile.cacheKey) {
|
||||||
} else {
|
tile.cacheKey = "";
|
||||||
var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
|
tile.originalCacheKey = "";
|
||||||
if (imageRecord) {
|
}
|
||||||
this._setTileLoaded(tile, imageRecord.getData());
|
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 {
|
||||||
|
similarCacheRecord.getData().then(data =>
|
||||||
|
this._setTileLoaded(tile, data, cutoff, null, similarCacheRecord.type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1578,7 +1602,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
urlOrGetter,
|
urlOrGetter,
|
||||||
post,
|
post,
|
||||||
ajaxHeaders,
|
ajaxHeaders,
|
||||||
context2D,
|
|
||||||
tile,
|
tile,
|
||||||
tilesMatrix = this.tilesMatrix,
|
tilesMatrix = this.tilesMatrix,
|
||||||
tileSource = this.source;
|
tileSource = this.source;
|
||||||
@ -1610,9 +1633,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
ajaxHeaders = null;
|
ajaxHeaders = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
context2D = tileSource.getContext2D ?
|
|
||||||
tileSource.getContext2D(level, xMod, yMod) : undefined;
|
|
||||||
|
|
||||||
tile = new $.Tile(
|
tile = new $.Tile(
|
||||||
level,
|
level,
|
||||||
x,
|
x,
|
||||||
@ -1620,12 +1640,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
bounds,
|
bounds,
|
||||||
exists,
|
exists,
|
||||||
urlOrGetter,
|
urlOrGetter,
|
||||||
context2D,
|
undefined,
|
||||||
this.loadTilesWithAjax,
|
this.loadTilesWithAjax,
|
||||||
ajaxHeaders,
|
ajaxHeaders,
|
||||||
sourceBounds,
|
sourceBounds,
|
||||||
post,
|
post,
|
||||||
tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post)
|
tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post),
|
||||||
|
this
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.getFlip()) {
|
if (this.getFlip()) {
|
||||||
@ -1672,8 +1693,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
ajaxHeaders: tile.ajaxHeaders,
|
ajaxHeaders: tile.ajaxHeaders,
|
||||||
crossOriginPolicy: this.crossOriginPolicy,
|
crossOriginPolicy: this.crossOriginPolicy,
|
||||||
ajaxWithCredentials: this.ajaxWithCredentials,
|
ajaxWithCredentials: this.ajaxWithCredentials,
|
||||||
callback: function( data, errorMsg, tileRequest ){
|
callback: function( data, errorMsg, tileRequest, dataType ){
|
||||||
_this._onTileLoad( tile, time, data, errorMsg, tileRequest );
|
_this._onTileLoad( tile, time, data, errorMsg, tileRequest, dataType );
|
||||||
},
|
},
|
||||||
abort: function() {
|
abort: function() {
|
||||||
tile.loading = false;
|
tile.loading = false;
|
||||||
@ -1690,9 +1711,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
* @param {*} data image data
|
* @param {*} data image data
|
||||||
* @param {String} errorMsg
|
* @param {String} errorMsg
|
||||||
* @param {XMLHttpRequest} tileRequest
|
* @param {XMLHttpRequest} tileRequest
|
||||||
|
* @param {String} [dataType=undefined] data type, derived automatically if not set
|
||||||
*/
|
*/
|
||||||
_onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
|
_onTileLoad: function( tile, time, data, errorMsg, tileRequest, dataType ) {
|
||||||
if ( !data ) {
|
//data is set to null on error by image loader, allow custom falsey values (e.g. 0)
|
||||||
|
if ( data === null ) {
|
||||||
$.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
|
$.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
|
||||||
/**
|
/**
|
||||||
* Triggered when a tile fails to load.
|
* Triggered when a tile fails to load.
|
||||||
@ -1730,8 +1753,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
finish = function() {
|
finish = function() {
|
||||||
var ccc = _this.source;
|
var ccc = _this.source;
|
||||||
var cutoff = ccc.getClosestLevel();
|
var cutoff = ccc.getClosestLevel();
|
||||||
_this._setTileLoaded(tile, data, cutoff, tileRequest);
|
_this._setTileLoaded(tile, data, cutoff, tileRequest, dataType);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we're mid-update; this can happen on IE8 because image load events for
|
// Check if we're mid-update; this can happen on IE8 because image load events for
|
||||||
// cached images happen immediately there
|
// cached images happen immediately there
|
||||||
@ -1739,7 +1762,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
// Wait until after the update, in case caching unloads any tiles
|
// Wait until after the update, in case caching unloads any tiles
|
||||||
window.setTimeout( finish, 1);
|
window.setTimeout(finish, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1747,77 +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,
|
||||||
* @param {Number|undefined} cutoff
|
* can be null: in that case, cache is assigned to a tile without further processing
|
||||||
* @param {XMLHttpRequest|undefined} tileRequest
|
* @param {?Number} cutoff
|
||||||
|
* @param {?XMLHttpRequest} tileRequest
|
||||||
|
* @param {?String} [dataType=undefined] data type, derived automatically if not set
|
||||||
*/
|
*/
|
||||||
_setTileLoaded: function(tile, data, cutoff, tileRequest) {
|
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) {
|
||||||
var increment = 0,
|
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
|
||||||
eventFinished = false,
|
// -> reason why it is not in the constructor
|
||||||
_this = this;
|
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--;
|
//do not override true if set (false is default)
|
||||||
if (increment === 0) {
|
tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency(
|
||||||
|
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.loading = false;
|
||||||
tile.loaded = true;
|
tile.loaded = true;
|
||||||
tile.hasTransparency = _this.source.hasTransparency(
|
resolver(tile);
|
||||||
tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
|
|
||||||
);
|
|
||||||
if (!tile.context2D) {
|
|
||||||
_this._tileCache.cacheTile({
|
|
||||||
data: data,
|
|
||||||
tile: tile,
|
|
||||||
cutoff: cutoff,
|
|
||||||
tiledImage: _this
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_this._needsDraw = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME: design choice: cache tile now set automatically so users can do
|
||||||
|
// tile.getCache(...) inside this event, but maybe we would like to have users
|
||||||
|
// freedom to decide on the cache creation (note, tiles now MUST have cache, e.g.
|
||||||
|
// it is no longer possible to store all tiles in the memory as it was with context2D prop)
|
||||||
|
tile.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {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.
|
|
||||||
*/
|
*/
|
||||||
|
const promise = this.viewer.raiseEventAwaiting("tile-loaded", {
|
||||||
var fallbackCompletion = getCompletionCallback();
|
|
||||||
this.viewer.raiseEvent("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() {
|
||||||
getCompletionCallback: getCompletionCallback
|
$.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile.getData()' instead.");
|
||||||
|
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1978,7 +2018,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||||||
useSketch = this.opacity < 1 ||
|
useSketch = this.opacity < 1 ||
|
||||||
(this.compositeOperation && this.compositeOperation !== 'source-over') ||
|
(this.compositeOperation && this.compositeOperation !== 'source-over') ||
|
||||||
(!this._isBottomItem() &&
|
(!this._isBottomItem() &&
|
||||||
this.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
|
(tile.hasTransparency || this.source.hasTransparency(
|
||||||
|
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sketchScale;
|
var sketchScale;
|
||||||
|
@ -139,6 +139,12 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve context2D of this tile source
|
||||||
|
* @memberOf OpenSeadragon.TileSource
|
||||||
|
* @function getContext2D
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ratio of width to height
|
* Ratio of width to height
|
||||||
* @member {Number} aspectRatio
|
* @member {Number} aspectRatio
|
||||||
@ -682,9 +688,12 @@ $.TileSource.prototype = {
|
|||||||
* The tile cache object is uniquely determined by this key and used to lookup
|
* The tile cache object is uniquely determined by this key and used to lookup
|
||||||
* the image data in cache: keys should be different if images are different.
|
* the image data in cache: keys should be different if images are different.
|
||||||
*
|
*
|
||||||
* In case a tile has context2D property defined (TileSource.prototype.getContext2D)
|
* You can return falsey tile cache key, in which case the tile will
|
||||||
* or its context2D is set manually; the cache is not used and this function
|
* be created without invoking ImageJob --- but with data=null. Then,
|
||||||
* is irrelevant.
|
* you are responsible for manually creating the cache data. This is useful
|
||||||
|
* particularly if you want to use empty TiledImage with client-side derived data
|
||||||
|
* only. The default tile-cache key is then called "" - an empty string.
|
||||||
|
*
|
||||||
* Note: default behaviour does not take into account post data.
|
* Note: default behaviour does not take into account post data.
|
||||||
* @param {Number} level tile level it was fetched with
|
* @param {Number} level tile level it was fetched with
|
||||||
* @param {Number} x x-coordinate in the pyramid level
|
* @param {Number} x x-coordinate in the pyramid level
|
||||||
@ -692,6 +701,9 @@ $.TileSource.prototype = {
|
|||||||
* @param {String} url the tile was fetched with
|
* @param {String} url the tile was fetched with
|
||||||
* @param {Object} ajaxHeaders the tile was fetched with
|
* @param {Object} ajaxHeaders the tile was fetched with
|
||||||
* @param {*} postData data the tile was fetched with (type depends on getTilePostData(..) return type)
|
* @param {*} postData data the tile was fetched with (type depends on getTilePostData(..) return type)
|
||||||
|
* @return {?String} can return the cache key or null, in that case an empty cache is initialized
|
||||||
|
* without downloading any data for internal use: user has to define the cache contents manually, via
|
||||||
|
* the cache interface of this class.
|
||||||
*/
|
*/
|
||||||
getTileHashKey: function(level, x, y, url, ajaxHeaders, postData) {
|
getTileHashKey: function(level, x, y, url, ajaxHeaders, postData) {
|
||||||
function withHeaders(hash) {
|
function withHeaders(hash) {
|
||||||
@ -722,10 +734,15 @@ $.TileSource.prototype = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide whether tiles have transparency: this is crucial for correct images blending.
|
* Decide whether tiles have transparency: this is crucial for correct images blending.
|
||||||
|
* Overriden on a tile level by setting tile.hasTransparency = true;
|
||||||
|
* @param context2D unused, deprecated argument
|
||||||
|
* @param url tile.getUrl() value for given tile
|
||||||
|
* @param ajaxHeaders tile.ajaxHeaders value for given tile
|
||||||
|
* @param post tile.post value for given tile
|
||||||
* @returns {boolean} true if the image has transparency
|
* @returns {boolean} true if the image has transparency
|
||||||
*/
|
*/
|
||||||
hasTransparency: function(context2D, url, ajaxHeaders, post) {
|
hasTransparency: function(context2D, url, ajaxHeaders, post) {
|
||||||
return !!context2D || url.match('.png');
|
return url.match('.png');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -737,15 +754,18 @@ $.TileSource.prototype = {
|
|||||||
* @param {String} [context.ajaxHeaders] - Headers to add to the image request if using AJAX.
|
* @param {String} [context.ajaxHeaders] - Headers to add to the image request if using AJAX.
|
||||||
* @param {Boolean} [context.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
|
* @param {Boolean} [context.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
|
||||||
* @param {String} [context.crossOriginPolicy] - CORS policy to use for downloads
|
* @param {String} [context.crossOriginPolicy] - CORS policy to use for downloads
|
||||||
* @param {String} [context.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
|
* @param {?String|?Object} [context.postData] - HTTP POST data (usually but not necessarily
|
||||||
* see TileSource::getPostData) or null
|
* in k=v&k2=v2... form, see TileSource::getPostData) or null
|
||||||
* @param {*} [context.userData] - Empty object to attach your own data and helper variables to.
|
* @param {*} [context.userData] - Empty object to attach your own data and helper variables to.
|
||||||
* @param {Function} [context.finish] - Should be called unless abort() was executed, e.g. on all occasions,
|
* @param {Function} [context.finish] - Should be called unless abort() was executed upon successful
|
||||||
* be it successful or unsuccessful request.
|
* data retrieval.
|
||||||
* Usage: context.finish(data, request, errMessage). Pass the downloaded data object or null upon failure.
|
* Usage: context.finish(data, request, dataType=undefined). Pass the downloaded data object
|
||||||
* Add also reference to an ajax request if used. Provide error message in case of failure.
|
* add also reference to an ajax request if used. Optionally, specify what data type the data is.
|
||||||
|
* @param {Function} [context.fail] - Should be called unless abort() was executed upon unsuccessful request.
|
||||||
|
* Usage: context.fail(errMessage, request). Provide error message in case of failure,
|
||||||
|
* add also reference to an ajax request if used.
|
||||||
* @param {Function} [context.abort] - Called automatically when the job times out.
|
* @param {Function} [context.abort] - Called automatically when the job times out.
|
||||||
* Usage: context.abort().
|
* Usage: if you decide to abort the request (no fail/finish will be called), call context.abort().
|
||||||
* @param {Function} [context.callback] @private - Called automatically once image has been downloaded
|
* @param {Function} [context.callback] @private - Called automatically once image has been downloaded
|
||||||
* (triggered by finish).
|
* (triggered by finish).
|
||||||
* @param {Number} [context.timeout] @private - The max number of milliseconds that
|
* @param {Number} [context.timeout] @private - The max number of milliseconds that
|
||||||
@ -753,25 +773,26 @@ $.TileSource.prototype = {
|
|||||||
* @param {string} [context.errorMsg] @private - The final error message, default null (set by finish).
|
* @param {string} [context.errorMsg] @private - The final error message, default null (set by finish).
|
||||||
*/
|
*/
|
||||||
downloadTileStart: function (context) {
|
downloadTileStart: function (context) {
|
||||||
var dataStore = context.userData,
|
const dataStore = context.userData,
|
||||||
image = new Image();
|
image = new Image();
|
||||||
|
|
||||||
dataStore.image = image;
|
dataStore.image = image;
|
||||||
dataStore.request = null;
|
dataStore.request = null;
|
||||||
|
|
||||||
var finish = function(error) {
|
const finalize = function(error) {
|
||||||
if (!image) {
|
if (error || !image) {
|
||||||
context.finish(null, dataStore.request, "Image load failed: undefined Image instance.");
|
context.fail(error || "[downloadTileStart] Image load failed: undefined Image instance.",
|
||||||
|
dataStore.request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
image.onload = image.onerror = image.onabort = null;
|
image.onload = image.onerror = image.onabort = null;
|
||||||
context.finish(error ? null : image, dataStore.request, error);
|
context.finish(image, dataStore.request); //dataType="image" recognized automatically
|
||||||
};
|
};
|
||||||
image.onload = function () {
|
image.onload = function () {
|
||||||
finish();
|
finalize();
|
||||||
};
|
};
|
||||||
image.onabort = image.onerror = function() {
|
image.onabort = image.onerror = function() {
|
||||||
finish("Image load aborted.");
|
finalize("[downloadTileStart] Image load aborted.");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load the tile with an AJAX request if the loadWithAjax option is
|
// Load the tile with an AJAX request if the loadWithAjax option is
|
||||||
@ -791,21 +812,21 @@ $.TileSource.prototype = {
|
|||||||
try {
|
try {
|
||||||
blb = new window.Blob([request.response]);
|
blb = new window.Blob([request.response]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var BlobBuilder = (
|
const BlobBuilder = (
|
||||||
window.BlobBuilder ||
|
window.BlobBuilder ||
|
||||||
window.WebKitBlobBuilder ||
|
window.WebKitBlobBuilder ||
|
||||||
window.MozBlobBuilder ||
|
window.MozBlobBuilder ||
|
||||||
window.MSBlobBuilder
|
window.MSBlobBuilder
|
||||||
);
|
);
|
||||||
if (e.name === 'TypeError' && BlobBuilder) {
|
if (e.name === 'TypeError' && BlobBuilder) {
|
||||||
var bb = new BlobBuilder();
|
const bb = new BlobBuilder();
|
||||||
bb.append(request.response);
|
bb.append(request.response);
|
||||||
blb = bb.getBlob();
|
blb = bb.getBlob();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the blob is empty for some reason consider the image load a failure.
|
// If the blob is empty for some reason consider the image load a failure.
|
||||||
if (blb.size === 0) {
|
if (blb.size === 0) {
|
||||||
finish("Empty image response.");
|
finalize("[downloadTileStart] Empty image response.");
|
||||||
} else {
|
} else {
|
||||||
// Create a URL for the blob data and make it the source of the image object.
|
// Create a URL for the blob data and make it the source of the image object.
|
||||||
// This will still trigger Image.onload to indicate a successful tile load.
|
// This will still trigger Image.onload to indicate a successful tile load.
|
||||||
@ -813,7 +834,7 @@ $.TileSource.prototype = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(request) {
|
error: function(request) {
|
||||||
finish("Image load aborted - XHR error");
|
finalize("[downloadTileStart] Image load aborted - XHR error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -827,6 +848,8 @@ $.TileSource.prototype = {
|
|||||||
/**
|
/**
|
||||||
* Provide means of aborting the execution.
|
* Provide means of aborting the execution.
|
||||||
* Note that if you override this function, you should override also downloadTileStart().
|
* Note that if you override this function, you should override also downloadTileStart().
|
||||||
|
* Note that calling job.abort() would create an infinite loop!
|
||||||
|
*
|
||||||
* @param {ImageJob} context job, the same object as with downloadTileStart(..)
|
* @param {ImageJob} context job, the same object as with downloadTileStart(..)
|
||||||
* @param {*} [context.userData] - Empty object to attach (and mainly read) your own data.
|
* @param {*} [context.userData] - Empty object to attach (and mainly read) your own data.
|
||||||
*/
|
*/
|
||||||
@ -845,33 +868,44 @@ $.TileSource.prototype = {
|
|||||||
* cacheObject parameter should be used to attach the data to, there are no
|
* cacheObject parameter should be used to attach the data to, there are no
|
||||||
* conventions on how it should be stored - all the logic is implemented within *TileCache() functions.
|
* conventions on how it should be stored - all the logic is implemented within *TileCache() functions.
|
||||||
*
|
*
|
||||||
* Note that if you override any of *TileCache() functions, you should override all of them.
|
* Note that
|
||||||
* @param {object} cacheObject context cache object
|
* - data is cached automatically as cacheObject.data
|
||||||
|
* - if you override any of *TileCache() functions, you should override all of them.
|
||||||
|
* - these functions might be called over shared cache object managed by other TileSources simultaneously.
|
||||||
|
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||||
* @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
|
||||||
* @param {Tile} tile instance the cache was created with
|
* @param {OpenSeadragon.Tile} tile instance the cache was created with
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
createTileCache: function(cacheObject, data, tile) {
|
createTileCache: function(cacheObject, data, tile) {
|
||||||
cacheObject._data = data;
|
$.console.error("[TileSource.createTileCache] has been deprecated. Use cache API of a tile instead.");
|
||||||
|
//no-op, we create the cache automatically
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache object destructor, unset all properties you created to allow GC collection.
|
* Cache object destructor, unset all properties you created to allow GC collection.
|
||||||
* Note that if you override any of *TileCache() functions, you should override all of them.
|
* Note that if you override any of *TileCache() functions, you should override all of them.
|
||||||
* @param {object} cacheObject context cache object
|
* Note that these functions might be called over shared cache object managed by other TileSources simultaneously.
|
||||||
|
* Original cache data is cacheObject.data, but do not delete it manually! It is taken care for,
|
||||||
|
* you might break things.
|
||||||
|
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
destroyTileCache: function (cacheObject) {
|
destroyTileCache: function (cacheObject) {
|
||||||
cacheObject._data = null;
|
// FIXME: still allow custom desctruction? probably not - cache system should handle all
|
||||||
cacheObject._renderedContext = null;
|
$.console.error("[TileSource.destroyTileCache] has been deprecated. Use cache API of a tile instead.");
|
||||||
|
//no-op, handled internally
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw data getter
|
* Raw data getter, should return anything that is compatible with the system, or undefined
|
||||||
* Note that if you override any of *TileCache() functions, you should override all of them.
|
* if the system can handle it.
|
||||||
* @param {object} cacheObject context cache object
|
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||||
* @returns {*} cache data
|
* @returns {*} cache data
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
getTileCacheData: function(cacheObject) {
|
getTileCacheData: function(cacheObject) {
|
||||||
return cacheObject._data;
|
return cacheObject.getData();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -879,11 +913,14 @@ $.TileSource.prototype = {
|
|||||||
* - plugins might need image representation of the data
|
* - plugins might need image representation of the data
|
||||||
* - div HTML rendering relies on image element presence
|
* - div HTML rendering relies on image element presence
|
||||||
* Note that if you override any of *TileCache() functions, you should override all of them.
|
* Note that if you override any of *TileCache() functions, you should override all of them.
|
||||||
* @param {object} cacheObject context cache object
|
* Note that these functions might be called over shared cache object managed by other TileSources simultaneously.
|
||||||
|
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||||
* @returns {Image} cache data as an Image
|
* @returns {Image} cache data as an Image
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
getTileCacheDataAsImage: function(cacheObject) {
|
getTileCacheDataAsImage: function(cacheObject) {
|
||||||
return cacheObject._data; //the data itself by default is Image
|
$.console.error("[TileSource.getTileCacheDataAsImage] has been deprecated. Use cache API of a tile instead.");
|
||||||
|
return cacheObject.getData("image");
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -891,21 +928,13 @@ $.TileSource.prototype = {
|
|||||||
* - most heavily used rendering method is a canvas-based approach,
|
* - most heavily used rendering method is a canvas-based approach,
|
||||||
* convert the data to a canvas and return it's 2D context
|
* convert the data to a canvas and return it's 2D context
|
||||||
* Note that if you override any of *TileCache() functions, you should override all of them.
|
* Note that if you override any of *TileCache() functions, you should override all of them.
|
||||||
* @param {object} cacheObject context cache object
|
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
|
||||||
* @returns {CanvasRenderingContext2D} context of the canvas representation of the cache data
|
* @returns {CanvasRenderingContext2D} context of the canvas representation of the cache data
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
getTileCacheDataAsContext2D: function(cacheObject) {
|
getTileCacheDataAsContext2D: function(cacheObject) {
|
||||||
if (!cacheObject._renderedContext) {
|
$.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead.");
|
||||||
var canvas = document.createElement( 'canvas' );
|
return cacheObject.getData("context2d");
|
||||||
canvas.width = cacheObject._data.width;
|
|
||||||
canvas.height = cacheObject._data.height;
|
|
||||||
cacheObject._renderedContext = canvas.getContext('2d');
|
|
||||||
cacheObject._renderedContext.drawImage( cacheObject._data, 0, 0 );
|
|
||||||
//since we are caching the prerendered image on a canvas
|
|
||||||
//allow the image to not be held in memory
|
|
||||||
cacheObject._data = null;
|
|
||||||
}
|
|
||||||
return cacheObject._renderedContext;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -415,6 +415,7 @@ $.Viewer = function( options ) {
|
|||||||
|
|
||||||
// Create the tile cache
|
// Create the tile cache
|
||||||
this.tileCache = new $.TileCache({
|
this.tileCache = new $.TileCache({
|
||||||
|
viewer: this,
|
||||||
maxImageCacheCount: this.maxImageCacheCount
|
maxImageCacheCount: this.maxImageCacheCount
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1434,7 +1435,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||||||
* (portions of the image outside of this area will not be visible). Only works on
|
* (portions of the image outside of this area will not be visible). Only works on
|
||||||
* browsers that support the HTML5 canvas.
|
* browsers that support the HTML5 canvas.
|
||||||
* @param {Number} [options.opacity=1] Proportional opacity of the tiled images (1=opaque, 0=hidden)
|
* @param {Number} [options.opacity=1] Proportional opacity of the tiled images (1=opaque, 0=hidden)
|
||||||
* @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
|
* @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
|
||||||
|
* @param {Boolean} [options.zombieCache] In the case that this method removes any TiledImage instance,
|
||||||
|
* allow the item-referenced cache to remain in memory even without active tiles. Default false.
|
||||||
* @param {Number} [options.degrees=0] Initial rotation of the tiled image around
|
* @param {Number} [options.degrees=0] Initial rotation of the tiled image around
|
||||||
* its top left corner in degrees.
|
* its top left corner in degrees.
|
||||||
* @param {Boolean} [options.flipped=false] Whether to horizontally flip the image.
|
* @param {Boolean} [options.flipped=false] Whether to horizontally flip the image.
|
||||||
@ -1576,6 +1579,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||||||
if (newIndex !== -1) {
|
if (newIndex !== -1) {
|
||||||
queueItem.options.index = newIndex;
|
queueItem.options.index = newIndex;
|
||||||
}
|
}
|
||||||
|
queueItem.options.replaceItem.allowZombieCache(queueItem.options.zombieCache || false);
|
||||||
_this.world.removeItem(queueItem.options.replaceItem);
|
_this.world.removeItem(queueItem.options.replaceItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
|
|||||||
/**
|
/**
|
||||||
* Add the specified item.
|
* Add the specified item.
|
||||||
* @param {OpenSeadragon.TiledImage} item - The item to add.
|
* @param {OpenSeadragon.TiledImage} item - The item to add.
|
||||||
|
* @param {Object} options - Options affecting insertion.
|
||||||
* @param {Number} [options.index] - Index for the item. If not specified, goes at the top.
|
* @param {Number} [options.index] - Index for the item. If not specified, goes at the top.
|
||||||
* @fires OpenSeadragon.World.event:add-item
|
* @fires OpenSeadragon.World.event:add-item
|
||||||
* @fires OpenSeadragon.World.event:metrics-change
|
* @fires OpenSeadragon.World.event:metrics-change
|
||||||
|
@ -176,10 +176,20 @@
|
|||||||
|
|
||||||
for ( var i in testLog ) {
|
for ( var i in testLog ) {
|
||||||
if ( testLog.hasOwnProperty( i ) && testLog[i].push ) {
|
if ( testLog.hasOwnProperty( i ) && testLog[i].push ) {
|
||||||
|
//Tile.tiledImage creates circular reference, copy object to avoid and allow JSON serialization
|
||||||
|
const tileCircularStructureReplacer = function (key, value) {
|
||||||
|
if (value instanceof OpenSeadragon.Tile) {
|
||||||
|
var instance = {};
|
||||||
|
Object.assign(instance, value);
|
||||||
|
delete value.tiledImage;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
testConsole[i] = ( function ( arr ) {
|
testConsole[i] = ( function ( arr ) {
|
||||||
return function () {
|
return function () {
|
||||||
var args = Array.prototype.slice.call( arguments, 0 ); // Coerce to true Array
|
var args = Array.prototype.slice.call( arguments, 0 ); // Coerce to true Array
|
||||||
arr.push( JSON.stringify( args ) ); // Store as JSON to avoid tedious array-equality tests
|
arr.push( JSON.stringify( args, tileCircularStructureReplacer ) ); // Store as JSON to avoid tedious array-equality tests
|
||||||
};
|
};
|
||||||
} )( testLog[i] );
|
} )( testLog[i] );
|
||||||
|
|
||||||
|
@ -224,12 +224,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('FullScreen', function(assert) {
|
QUnit.test('FullScreen', function(assert) {
|
||||||
const done = assert.async();
|
|
||||||
if (!OpenSeadragon.supportsFullScreen) {
|
if (!OpenSeadragon.supportsFullScreen) {
|
||||||
|
const done = assert.async();
|
||||||
assert.expect(0);
|
assert.expect(0);
|
||||||
done();
|
done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var timeWatcher = Util.timeWatcher(assert, 7000);
|
||||||
|
|
||||||
viewer.addHandler('open', function () {
|
viewer.addHandler('open', function () {
|
||||||
assert.ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');
|
assert.ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');
|
||||||
@ -244,7 +245,7 @@
|
|||||||
viewer.removeHandler('full-screen', checkExitingFullScreen);
|
viewer.removeHandler('full-screen', checkExitingFullScreen);
|
||||||
assert.ok(!event.fullScreen, 'Disabling fullscreen');
|
assert.ok(!event.fullScreen, 'Disabling fullscreen');
|
||||||
assert.ok(!OpenSeadragon.isFullScreen(), 'Fullscreen disabled');
|
assert.ok(!OpenSeadragon.isFullScreen(), 'Fullscreen disabled');
|
||||||
done();
|
timeWatcher.done();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 'new' headless mode allows us to enter fullscreen, so verify
|
// The 'new' headless mode allows us to enter fullscreen, so verify
|
||||||
@ -254,11 +255,11 @@
|
|||||||
viewer.removeHandler('full-screen', checkAcquiredFullScreen);
|
viewer.removeHandler('full-screen', checkAcquiredFullScreen);
|
||||||
viewer.addHandler('full-screen', checkExitingFullScreen);
|
viewer.addHandler('full-screen', checkExitingFullScreen);
|
||||||
assert.ok(event.fullScreen, 'Acquired fullscreen');
|
assert.ok(event.fullScreen, 'Acquired fullscreen');
|
||||||
assert.ok(OpenSeadragon.isFullScreen(), 'Fullscreen enabled');
|
assert.ok(OpenSeadragon.isFullScreen(), 'Fullscreen enabled. Note: this test might fail ' +
|
||||||
|
'because fullscreen might be blocked by your browser - not a trusted event!');
|
||||||
viewer.setFullScreen(false);
|
viewer.setFullScreen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
viewer.addHandler('pre-full-screen', checkEnteringPreFullScreen);
|
viewer.addHandler('pre-full-screen', checkEnteringPreFullScreen);
|
||||||
viewer.addHandler('full-screen', checkAcquiredFullScreen);
|
viewer.addHandler('full-screen', checkAcquiredFullScreen);
|
||||||
viewer.setFullScreen(true);
|
viewer.setFullScreen(true);
|
||||||
@ -330,7 +331,7 @@
|
|||||||
height: 155
|
height: 155
|
||||||
} ]
|
} ]
|
||||||
} );
|
} );
|
||||||
viewer.addOnceHandler('tile-drawn', function() {
|
viewer.addOnceHandler('tile-drawn', function(e) {
|
||||||
assert.ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas),
|
assert.ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas),
|
||||||
"Canvas should be tainted.");
|
"Canvas should be tainted.");
|
||||||
done();
|
done();
|
||||||
|
@ -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.");
|
|
||||||
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.ok( tile.loaded, "The tile should be marked as loaded.");
|
|
||||||
done();
|
|
||||||
}, 0);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewer.addHandler( 'tile-loaded', tileLoaded);
|
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.");
|
||||||
|
done();
|
||||||
|
done = null;
|
||||||
|
});
|
||||||
|
await sleep(10);
|
||||||
|
};
|
||||||
|
const tileLoaded2 = async (event) => {
|
||||||
|
assert.notOk( handledOnce, "TileLoaded2 with priority 10 should be called first.");
|
||||||
|
const tile = event.tile;
|
||||||
|
|
||||||
|
//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' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -221,11 +221,12 @@
|
|||||||
viewer.addHandler('open', function() {
|
viewer.addHandler('open', function() {
|
||||||
var firstImage = viewer.world.getItemAt(0);
|
var firstImage = viewer.world.getItemAt(0);
|
||||||
firstImage.addHandler('fully-loaded-change', function() {
|
firstImage.addHandler('fully-loaded-change', function() {
|
||||||
var imageData = viewer.drawer.context.getImageData(0, 0,
|
var aX = Math.round(500 * density), aY = Math.round(500 * density);
|
||||||
500 * density, 500 * density);
|
var imageData = viewer.drawer.context.getImageData(0, 0, aX, aY);
|
||||||
|
|
||||||
// Pixel 250,250 will be in the hole of the A
|
// Pixel 250,250 will be in the hole of the A
|
||||||
var expectedVal = getPixelValue(imageData, 250 * density, 250 * density);
|
aX = Math.round(250 * density); aY = Math.round(250 * density);
|
||||||
|
var expectedVal = getPixelValue(imageData, aX, aY);
|
||||||
|
|
||||||
assert.notEqual(expectedVal.r, 0, 'Red channel should not be 0');
|
assert.notEqual(expectedVal.r, 0, 'Red channel should not be 0');
|
||||||
assert.notEqual(expectedVal.g, 0, 'Green channel should not be 0');
|
assert.notEqual(expectedVal.g, 0, 'Green channel should not be 0');
|
||||||
@ -237,8 +238,10 @@
|
|||||||
success: function() {
|
success: function() {
|
||||||
var secondImage = viewer.world.getItemAt(1);
|
var secondImage = viewer.world.getItemAt(1);
|
||||||
secondImage.addHandler('fully-loaded-change', function() {
|
secondImage.addHandler('fully-loaded-change', function() {
|
||||||
var imageData = viewer.drawer.context.getImageData(0, 0, 500 * density, 500 * density);
|
var aX = Math.round(500 * density), aY = Math.round(500 * density);
|
||||||
var actualVal = getPixelValue(imageData, 250 * density, 250 * density);
|
var imageData = viewer.drawer.context.getImageData(0, 0, aX, aY);
|
||||||
|
aX = Math.round(250 * density); aY = Math.round(250 * density);
|
||||||
|
var actualVal = getPixelValue(imageData, aX, aY);
|
||||||
|
|
||||||
assert.equal(actualVal.r, expectedVal.r,
|
assert.equal(actualVal.r, expectedVal.r,
|
||||||
'Red channel should not change in transparent part of the A');
|
'Red channel should not change in transparent part of the A');
|
||||||
@ -249,11 +252,12 @@
|
|||||||
assert.equal(actualVal.a, expectedVal.a,
|
assert.equal(actualVal.a, expectedVal.a,
|
||||||
'Alpha channel should not change in transparent part of the A');
|
'Alpha channel should not change in transparent part of the A');
|
||||||
|
|
||||||
var onAVal = getPixelValue(imageData, 333 * density, 250 * density);
|
aX = Math.round(333 * density); aY = Math.round(250 * density);
|
||||||
assert.equal(onAVal.r, 0, 'Red channel should be null on the A');
|
var onAVal = getPixelValue(imageData, aX, aY);
|
||||||
assert.equal(onAVal.g, 0, 'Green channel should be null on the A');
|
assert.equal(onAVal.r, 0, 'Red channel should be null on the A pixel (' + aX + ', ' + aY + ')');
|
||||||
assert.equal(onAVal.b, 0, 'Blue channel should be null on the A');
|
assert.equal(onAVal.g, 0, 'Green channel should be null on the A pixel (' + aX + ', ' + aY + ')');
|
||||||
assert.equal(onAVal.a, 255, 'Alpha channel should be 255 on the A');
|
assert.equal(onAVal.b, 0, 'Blue channel should be null on the A pixel (' + aX + ', ' + aY + ')');
|
||||||
|
assert.equal(onAVal.a, 255, 'Alpha channel should be 255 on the A pixel (' + aX + ', ' + aY + ')');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,9 @@
|
|||||||
url: 'foo.jpg',
|
url: 'foo.jpg',
|
||||||
cacheKey: 'foo.jpg',
|
cacheKey: 'foo.jpg',
|
||||||
image: {},
|
image: {},
|
||||||
|
loaded: true,
|
||||||
|
tiledImage: fakeTiledImage0,
|
||||||
|
_caches: [],
|
||||||
unload: function() {}
|
unload: function() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,6 +41,9 @@
|
|||||||
url: 'foo.jpg',
|
url: 'foo.jpg',
|
||||||
cacheKey: 'foo.jpg',
|
cacheKey: 'foo.jpg',
|
||||||
image: {},
|
image: {},
|
||||||
|
loaded: true,
|
||||||
|
tiledImage: fakeTiledImage1,
|
||||||
|
_caches: [],
|
||||||
unload: function() {}
|
unload: function() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,6 +90,9 @@
|
|||||||
url: 'different.jpg',
|
url: 'different.jpg',
|
||||||
cacheKey: 'different.jpg',
|
cacheKey: 'different.jpg',
|
||||||
image: {},
|
image: {},
|
||||||
|
loaded: true,
|
||||||
|
tiledImage: fakeTiledImage0,
|
||||||
|
_caches: [],
|
||||||
unload: function() {}
|
unload: function() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,6 +100,9 @@
|
|||||||
url: 'same.jpg',
|
url: 'same.jpg',
|
||||||
cacheKey: 'same.jpg',
|
cacheKey: 'same.jpg',
|
||||||
image: {},
|
image: {},
|
||||||
|
loaded: true,
|
||||||
|
tiledImage: fakeTiledImage0,
|
||||||
|
_caches: [],
|
||||||
unload: function() {}
|
unload: function() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,6 +110,9 @@
|
|||||||
url: 'same.jpg',
|
url: 'same.jpg',
|
||||||
cacheKey: 'same.jpg',
|
cacheKey: 'same.jpg',
|
||||||
image: {},
|
image: {},
|
||||||
|
loaded: true,
|
||||||
|
tiledImage: fakeTiledImage0,
|
||||||
|
_caches: [],
|
||||||
unload: function() {}
|
unload: function() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user