Merge pull request #2407 from RationAI/cache-overhaul-reviewed

Cache Overhaul for OpenSeadragon (reviewed).
This commit is contained in:
Ian Gilman 2024-12-16 09:26:13 -08:00 committed by GitHub
commit 64bb7e25c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 7798 additions and 1391 deletions

View File

@ -61,6 +61,10 @@ Our tests are based on [QUnit](https://qunitjs.com/) and [Puppeteer](https://git
grunt test
To test a specific module (`navigator` here) only:
grunt test --module="navigator"
If you wish to work interactively with the tests or test your changes:
grunt connect watch
@ -69,6 +73,12 @@ and open `http://localhost:8000/test/test.html` in your browser.
Another good page, if you want to interactively test out your changes, is `http://localhost:8000/test/demo/basic.html`.
> Note: corresponding npm commands for the above are:
> - npm run test
> - npm run test -- --module="navigator"
> - npm run dev
You can also get a report of the tests' code coverage:
grunt coverage

View File

@ -49,6 +49,8 @@ module.exports = function(grunt) {
"src/legacytilesource.js",
"src/imagetilesource.js",
"src/tilesourcecollection.js",
"src/priorityqueue.js",
"src/datatypeconvertor.js",
"src/button.js",
"src/buttongroup.js",
"src/rectangle.js",
@ -79,6 +81,11 @@ module.exports = function(grunt) {
grunt.config.set('gitInfo', rev);
});
let moduleFilter = '';
if (grunt.option('module')) {
moduleFilter = '?module=' + grunt.option('module')
}
// ----------
// Project configuration.
grunt.initConfig({
@ -164,7 +171,7 @@ module.exports = function(grunt) {
qunit: {
normal: {
options: {
urls: [ "http://localhost:8000/test/test.html" ],
urls: [ "http://localhost:8000/test/test.html" + moduleFilter ],
timeout: 10000,
puppeteer: {
headless: 'new'
@ -173,7 +180,7 @@ module.exports = function(grunt) {
},
coverage: {
options: {
urls: [ "http://localhost:8000/test/coverage.html" ],
urls: [ "http://localhost:8000/test/coverage.html" + moduleFilter ],
coverage: {
src: ['src/*.js'],
htmlReport: coverageDir + '/html/',
@ -194,7 +201,12 @@ module.exports = function(grunt) {
server: {
options: {
port: 8000,
base: "."
base: {
path: ".",
options: {
stylesheet: 'style.css'
}
}
}
}
},

View File

@ -27,3 +27,9 @@ OpenSeadragon is released under the New BSD license. For details, see the [LICEN
[github-releases]: https://github.com/openseadragon/openseadragon/releases
[github-contributing]: https://github.com/openseadragon/openseadragon/blob/master/CONTRIBUTING.md
[github-license]: https://github.com/openseadragon/openseadragon/blob/master/LICENSE.txt
## Sponsors
We are grateful for the (development or financial) contribution to the OpenSeadragon project.
<a href="https://www.bbmri-eric.eu"><img alt="Logo BBMRI ERIC" src="assets/logos/bbmri-logo.png" height="80"/></a>

BIN
assets/logos/bbmri-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -2,6 +2,28 @@ OPENSEADRAGON CHANGELOG
=======================
6.0.0: (in progress...)
* NEW BEHAVIOR: OpenSeadragon Data Pipeline Overhaul
* DEPRECATION: Properties on tile that manage drawer data, or store data to draw: Tile.[element|imgElement|style|context2D|getImage|getCanvasContext] and transitively Tile.getScaleForEdgeSmoothing
* DEPRECATION: TileSource data lifecycle handlers: system manages these automatically: TileSource.[createTileCache|destroyTileCache|getTileCacheData|getTileCacheDataAsImage|getTileCacheDataAsContext2D]
* Tiles data is driven by caches: tiles can have multiple caches and cache can reference multiple tiles.
* Data types & conversion pipeline: caches support automated conversion between types, and call optionally destructors. These are asynchronous.
* Data conversion reasoning: the system keeps costs of convertors and seeks the cheapest conversion to a target format (using Dijkstra).
* Async support: events can now await handlers. Added OpenSeadragon.Promise proxy object. This object supports also synchronous mode.
* Drawers define what data they are able to work with, and receive automatically data from one of the declared types.
* Drawers now store data only inside cache, and provide optional type convertors to move data into a format they can work with.
* TileSource equality operator. TileSource destructor support. TileSources now must output type of the data they download [context.finish].
* Zombies: data can outlive tiles, and be kept in the system to wait if they are not suddenly needed. Turned on automatically with TiledImage addition with `replace: true` and equality test success.
* ImagesLoadedPerFrame is boosted 10 times when system is fresh (reset / open) and then declines back to original value.
* CacheRecord supports 'internal cache' of 'SimpleCache' type. This cache can be used by drawers to hide complex types used for rendering. Such caches are stored internally on CacheRecord objects.
* CacheRecord drives asynchronous data management and ensures correct behavior through awaiting Promises.
* TileCache adds new methods for cache modification: renameCache, cloneCache, injectCache, replaceCache, restoreTilesThatShareOriginalCache, safeUnloadCache, unloadCacheForTile and more. Used internally within invalidation events
* Tiles have up to two 'originalCacheKey' and 'cacheKey' caches, which keep original data and target drawn data (if modified).
* Invalidation Pipeline: New event 'tile-invalidated' and requestInvalidate methods on World and TiledImage. Tiles get methods to modify data to draw, system prepares data for drawing and swaps them with the current main tile cache.
* New test suites for the new cache system, conversion pipeline and invalidation events.
* New testing/demo utilities (MockSeadragon, DrawerSwitcher for switching drawers in demos, getBuiltInDrawersForTest for testing all drawers), serialization guard in tests to remove circular references.
* New demos, demonstrating the new pipeline. New demos for older plugins to show how compatible new version is.
* Misc: updated CSS for dev server, new dev & test commands.
5.0.1:

View File

@ -46,6 +46,8 @@
},
"scripts": {
"test": "grunt test",
"prepare": "grunt build"
"prepare": "grunt build",
"build": "grunt build",
"dev": "grunt dev"
}
}

View File

@ -47,7 +47,7 @@
*/
class CanvasDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
constructor(options) {
super(options);
/**
@ -69,7 +69,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
* @memberof OpenSeadragon.CanvasDrawer#
* @private
*/
this.context = this.canvas.getContext( '2d' );
this.context = this.canvas.getContext('2d');
// Sketch canvas used to temporarily draw tiles which cannot be drawn directly
// to the main canvas due to opacity. Lazily initialized.
@ -97,6 +97,10 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
return 'canvas';
}
getSupportedDataFormats() {
return ["context2d"];
}
/**
* create the HTML element (e.g. canvas, div) that the image will be drawn into
* @returns {Element} the canvas to draw into
@ -267,26 +271,26 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
*
*/
_drawTiles( tiledImage ) {
var lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile);
const lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile);
if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) {
return;
}
var tile = lastDrawn[0];
var useSketch;
let tile = lastDrawn[0];
let useSketch;
if (tile) {
useSketch = tiledImage.opacity < 1 ||
(tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') ||
(!tiledImage._isBottomItem() &&
tiledImage.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
tiledImage.source.hasTransparency(null, tile.getUrl(), tile.ajaxHeaders, tile.postData));
}
var sketchScale;
var sketchTranslate;
let sketchScale;
let sketchTranslate;
var zoom = this.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom);
const zoom = this.viewport.getZoom(true);
const imageZoom = tiledImage.viewportToImageZoom(zoom);
if (lastDrawn.length > 1 &&
imageZoom > tiledImage.smoothTileEdgesMinZoom &&
@ -296,13 +300,19 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
// So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true;
sketchScale = tile.getScaleForEdgeSmoothing();
const context = tile.length && this.getDataToDraw(tile);
if (context) {
sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio);
} else {
sketchScale = 1;
}
sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
this._getCanvasSize(false),
this._getCanvasSize(true));
}
var bounds;
let bounds;
if (useSketch) {
if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the
@ -322,13 +332,13 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._setRotations(tiledImage, useSketch);
}
var usedClip = false;
let usedClip = false;
if ( tiledImage._clip ) {
this._saveContext(useSketch);
var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
let box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipRect = this.viewportToDrawerRectangle(box);
let clipRect = this.viewportToDrawerRectangle(box);
if (sketchScale) {
clipRect = clipRect.times(sketchScale);
}
@ -341,17 +351,17 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
}
if (tiledImage._croppingPolygons) {
var self = this;
const self = this;
if(!usedClip){
this._saveContext(useSketch);
}
try {
var polygons = tiledImage._croppingPolygons.map(function (polygon) {
const polygons = tiledImage._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
var point = tiledImage
const point = tiledImage
.imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipPoint = self.viewportCoordToDrawerCoord(point);
let clipPoint = self.viewportCoordToDrawerCoord(point);
if (sketchScale) {
clipPoint = clipPoint.times(sketchScale);
}
@ -388,19 +398,18 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._drawRectangle(placeholderRect, fillStyle, useSketch);
}
var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency);
const subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency);
var shouldRoundPositionAndSize = false;
let shouldRoundPositionAndSize = false;
if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
shouldRoundPositionAndSize = true;
} else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
var isAnimating = this.viewer && this.viewer.isAnimating();
shouldRoundPositionAndSize = !isAnimating;
shouldRoundPositionAndSize = !(this.viewer && this.viewer.isAnimating());
}
// Iterate over the tiles to draw, and draw them
for (var i = 0; i < lastDrawn.length; i++) {
for (let i = 0; i < lastDrawn.length; i++) {
tile = lastDrawn[ i ];
this._drawTile( tile, tiledImage, useSketch, sketchScale,
sketchTranslate, shouldRoundPositionAndSize, tiledImage.source );
@ -462,9 +471,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._drawDebugInfo( tiledImage, lastDrawn );
// Fire tiled-image-drawn event.
this._raiseTiledImageDrawnEvent(tiledImage, lastDrawn);
}
/**
@ -522,52 +529,25 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
$.console.assert(tile, '[Drawer._drawTile] tile is required');
$.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required');
var context = this._getContext(useSketch);
scale = scale || 1;
this._drawTileToCanvas(tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source);
}
/**
* Renders the tile in a canvas-based context.
* @private
* @function
* @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas
* @param {Canvas} context
* @param {OpenSeadragon.TiledImage} tiledImage - Method for firing the drawing event.
* drawingHandler({context, tile, rendered})
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Number} [scale=1] - Apply a scale to position and size
* @param {OpenSeadragon.Point} [translate] - A translation vector
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency
* context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
_drawTileToCanvas( tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source) {
var position = tile.position.times($.pixelDensityRatio),
size = tile.size.times($.pixelDensityRatio),
rendered;
if (!tile.context2D && !tile.cacheImageRecord) {
$.console.warn(
'[Drawer._drawTileToCanvas] attempting to draw tile %s when it\'s not cached',
tile.toString());
return;
}
rendered = tile.getCanvasContext();
if ( !tile.loaded || !rendered ){
if ( !tile.loaded ){
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
tile.toString()
);
return;
}
const rendered = this.getDataToDraw(tile);
if (!rendered) {
return;
}
const context = this._getContext(useSketch);
scale = scale || 1;
let position = tile.position.times($.pixelDensityRatio),
size = tile.size.times($.pixelDensityRatio);
context.save();
if (typeof scale === 'number' && scale !== 1) {
@ -606,7 +586,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._raiseTileDrawingEvent(tiledImage, context, tile, rendered);
var sourceWidth, sourceHeight;
let sourceWidth, sourceHeight;
if (tile.sourceBounds) {
sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width);
sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height);
@ -634,6 +614,8 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
context.restore();
}
/**
* Get the context of the main or sketch canvas
* @private

488
src/datatypeconvertor.js Normal file
View File

@ -0,0 +1,488 @@
/*
* OpenSeadragon.convertor (static property)
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 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 = {};
}
/**
* Add vertex to graph
* @param vertex unique vertex ID
* @return {boolean} true if inserted, false if exists (no-op)
*/
addVertex(vertex) {
if (!this.vertices[vertex]) {
this.vertices[vertex] = new $.PriorityQueue.Node(0, vertex);
this.adjacencyList[vertex] = [];
return true;
}
return false;
}
/**
* Add edge to graph
* @param vertex1 id, must exist by calling addVertex()
* @param vertex2 id, must exist by calling addVertex()
* @param weight
* @param transform function that transforms on path vertex1 -> vertex2
* @return {boolean} true if new edge, false if replaced existing
*/
addEdge(vertex1, vertex2, weight, transform) {
if (weight < 0) {
$.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");
}
const outgoingPaths = this.adjacencyList[vertex1],
replacedEdgeIndex = outgoingPaths.findIndex(edge => edge.target === this.vertices[vertex2]),
newEdge = { target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform };
if (replacedEdgeIndex < 0) {
this.adjacencyList[vertex1].push(newEdge);
return true;
}
this.adjacencyList[vertex1][replacedEdgeIndex] = newEdge;
return false;
}
/**
* @return {{path: ConversionStep[], cost: number}|undefined} cheapest path from start to finish
*/
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 || smallestNode.value !== finish) {
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
};
}
}
/**
* Edge.transform function 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 {OpenSeadragon.Tile} tile reference tile that owns the data
* @param {any} data data in the input format
* @returns {any} data in the output format
*/
/**
* Destructor called every time a data type is to be destroyed or converted to another type.
*
* @callback TypeDestructor
* @memberof OpenSeadragon
* @param {any} data data in the format the destructor is registered for
* @returns {any} can return any value that is carried over to the caller if desirable.
* Note: not used by the OSD cache system.
*/
/**
* 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 = {};
this.copyings = {};
// Teaching OpenSeadragon built-in conversions:
const imageCreator = (tile, url) => new $.Promise((resolve, reject) => {
if (!$.supportsAsync) {
throw "Not supported in sync mode!";
}
const img = new Image();
img.onerror = img.onabort = reject;
img.onload = () => resolve(img);
img.src = url;
});
const canvasContextCreator = (tile, imageData) => {
const canvas = document.createElement( 'canvas' );
canvas.width = imageData.width;
canvas.height = imageData.height;
const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage( imageData, 0, 0 );
return context;
};
this.learn("context2d", "webImageUrl", (tile, ctx) => ctx.canvas.toDataURL(), 1, 2);
this.learn("image", "webImageUrl", (tile, image) => image.url);
this.learn("image", "context2d", canvasContextCreator, 1, 1);
this.learn("url", "image", imageCreator, 1, 1);
//Copies
this.learn("image", "image", (tile, image) => imageCreator(tile, image.src), 1, 1);
this.learn("url", "url", (tile, url) => url, 0, 1); //strings are immutable, no need to copy
this.learn("context2d", "context2d", (tile, ctx) => canvasContextCreator(tile, ctx.canvas));
/**
* Free up canvas memory
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
* and Safari keeps canvas until its height and width will be set to 0).
*/
this.learnDestroy("context2d", ctx => {
ctx.canvas.width = 0;
ctx.canvas.height = 0;
});
}
/**
* Unique identifier (unlike toString.call(x)) to be guessed
* from the data value. This type guess is more strict than
* OpenSeadragon.type() implementation, but for most type recognition
* this test relies on the output of OpenSeadragon.type().
*
* Note: although we try to implement the type guessing, do
* not rely on this functionality! Prefer explicit type declaration.
*
* @function guessType
* @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 two arguments: a tile reference, and
* a data object of a type 'from'; and converts this data object to 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!");
if (from === to) {
this.copyings[to] = callback;
} else {
//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.TypeDestructor} 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().
* Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion).
* It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these
* manually if these should be destroyed.
* @param {OpenSeadragon.Tile} tile
* @param {any} data 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(tile, data, from, ...to) {
const conversionPath = this.getConversionPath(from, to);
if (!conversionPath) {
$.console.error(`[OpenSeadragon.convertor.convert] Conversion ${from} ---> ${to} cannot be done!`);
return $.Promise.resolve();
}
const stepCount = conversionPath.length,
_this = this;
const step = (x, i, destroy = true) => {
if (i >= stepCount) {
return $.Promise.resolve(x);
}
let edge = conversionPath[i];
let y = edge.transform(tile, x);
if (y === undefined) {
$.console.error(`[OpenSeadragon.convertor.convert] data mid result undefined value (while converting using %s)`, edge);
return $.Promise.resolve();
}
//node.value holds the type string
if (destroy) {
_this.destroy(x, edge.origin.value);
}
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
return result.then(res => step(res, i + 1));
};
//destroy only mid-results, but not the original value
return step(data, 0, false);
}
/**
* Copy the data item given.
* @param {OpenSeadragon.Tile} tile
* @param {any} data data item to convert
* @param {string} type data type
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor
*/
copy(tile, data, type) {
const copyTransform = this.copyings[type];
if (copyTransform) {
const y = copyTransform(tile, data);
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
}
$.console.warn(`[OpenSeadragon.convertor.copy] is not supported with type %s`, type);
return $.Promise.resolve(undefined);
}
/**
* Destroy the data item given.
* @param {string} type data type
* @param {any} data
* @return {OpenSeadragon.Promise<any>|undefined} promise resolution with data passed from constructor, or undefined
* if not such conversion exists
*/
destroy(data, type) {
const destructor = this.destructors[type];
if (destructor) {
const y = destructor(data);
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
}
return undefined;
}
/**
* 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(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;
}
/**
* Return a list of known conversion types
* @return {string[]}
*/
getKnownTypes() {
return Object.keys(this.graph.vertices);
}
/**
* Check whether given type is known to the convertor
* @param {string} type type to test
* @return {boolean}
*/
existsType(type) {
return !!this.graph.vertices[type];
}
};
/**
* Static convertor available throughout OpenSeadragon.
*
* Built-in conversions include types:
* - context2d canvas 2d context
* - image HTMLImage element
* - url url string carrying or pointing to 2D raster data
* - canvas HTMLCanvas element
*
* @type OpenSeadragon.DataTypeConvertor
* @memberOf OpenSeadragon
*/
$.convertor = new $.DataTypeConvertor();
}(OpenSeadragon));

View File

@ -34,7 +34,14 @@
(function( $ ){
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
/**
* @typedef BaseDrawerOptions
* @memberOf OpenSeadragon
* @property {boolean} [usePrivateCache=false] specify whether the drawer should use
* detached (=internal) cache object in case it has to perform type conversion
*/
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
/**
* @class OpenSeadragon.DrawerBase
* @classdesc Base class for Drawers that handle rendering of tiles for an {@link OpenSeadragon.Viewer}.
@ -51,10 +58,11 @@ OpenSeadragon.DrawerBase = class DrawerBase{
$.console.assert( options.viewport, "[Drawer] options.viewport is required" );
$.console.assert( options.element, "[Drawer] options.element is required" );
this._id = this.getType() + $.now();
this.viewer = options.viewer;
this.viewport = options.viewport;
this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
this.options = options.options || {};
this.options = $.extend({}, this.defaultOptions, options.options);
this.container = $.getElement( options.element );
@ -77,18 +85,40 @@ OpenSeadragon.DrawerBase = class DrawerBase{
this.container.style.textAlign = "left";
this.container.appendChild( this.canvas );
this._checkForAPIOverrides();
this._checkInterfaceImplementation();
}
/**
* Retrieve default options for the current drawer.
* The base implementation provides default shared options.
* Overrides should enumerate all defaults or extend from this implementation.
* return $.extend({}, super.options, { ... custom drawer instance options ... });
* @returns {BaseDrawerOptions} common options
*/
get defaultOptions() {
return {
usePrivateCache: false
};
}
// protect the canvas member with a getter
get canvas(){
return this._renderingTarget;
}
get element(){
$.console.error('Drawer.element is deprecated. Use Drawer.container instead.');
return this.container;
}
/**
* Get unique drawer ID
* @return {string}
*/
getId() {
return this._id;
}
/**
* @abstract
* @returns {String | undefined} What type of drawer this is. Must be overridden by extending classes.
@ -98,6 +128,43 @@ OpenSeadragon.DrawerBase = class DrawerBase{
return undefined;
}
/**
* Retrieve required data formats the data must be converted to.
* This list MUST BE A VALID SUBSET OF getSupportedDataFormats()
* @abstract
* @return {string[]}
*/
getRequiredDataFormats() {
return this.getSupportedDataFormats();
}
/**
* Retrieve data types
* @abstract
* @return {string[]}
*/
getSupportedDataFormats() {
throw "Drawer.getSupportedDataFormats must define its supported rendering data types!";
}
/**
* Check a particular cache record is compatible.
* This function _MUST_ be called: if it returns a falsey
* value, the rendering _MUST NOT_ proceed. It should
* await next animation frames and check again for availability.
* @param {OpenSeadragon.Tile} tile
* @return {any|undefined} undefined if cache not available, compatible data otherwise.
*/
getDataToDraw(tile) {
const cache = tile.getCache(tile.cacheKey);
if (!cache) {
$.console.warn("Attempt to draw tile %s when not cached!", tile);
return undefined;
}
const dataCache = cache.getDataForRendering(this, tile);
return dataCache && dataCache.data;
}
/**
* @abstract
* @returns {Boolean} Whether the drawer implementation is supported by the browser. Must be overridden by extending classes.
@ -149,7 +216,6 @@ OpenSeadragon.DrawerBase = class DrawerBase{
return false;
}
/**
* @abstract
* @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
@ -183,20 +249,20 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* @private
*
*/
_checkForAPIOverrides(){
if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){
_checkInterfaceImplementation(){
if (this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement) {
throw(new Error("[drawer]._createDrawingElement must be implemented by child class"));
}
if(this.draw === $.DrawerBase.prototype.draw){
if (this.draw === $.DrawerBase.prototype.draw) {
throw(new Error("[drawer].draw must be implemented by child class"));
}
if(this.canRotate === $.DrawerBase.prototype.canRotate){
if (this.canRotate === $.DrawerBase.prototype.canRotate) {
throw(new Error("[drawer].canRotate must be implemented by child class"));
}
if(this.destroy === $.DrawerBase.prototype.destroy){
if (this.destroy === $.DrawerBase.prototype.destroy) {
throw(new Error("[drawer].destroy must be implemented by child class"));
}
if(this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled){
if (this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled) {
throw(new Error("[drawer].setImageSmoothingEnabled must be implemented by child class"));
}
}
@ -302,7 +368,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* @property {OpenSeadragon.DrawerBase} drawer - The drawer that raised the error.
* @property {String} error - A message describing the error.
* @property {?Object} userData - Arbitrary subscriber-defined object.
* @private
* @protected
*/
this.viewer.raiseEvent( 'drawer-error', {
tiledImage: tiledImage,

View File

@ -167,6 +167,14 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead
},
/**
* Equality comparator
*/
equals: function(otherSource) {
return this.tilesUrl === otherSource.tilesUrl;
},
/**
* @function
* @param {Number} level

View File

@ -37,9 +37,19 @@
/**
* Event handler method signature used by all OpenSeadragon events.
*
* @callback EventHandler
* @typedef {function(OpenSeadragon.Event): void} OpenSeadragon.EventHandler
* @memberof OpenSeadragon
* @param {Object} event - See individual events for event-specific properties.
* @param {OpenSeadragon.Event} event - The event object containing event-specific properties.
* @returns {void} This handler does not return a value.
*/
/**
* Event handler method signature used by all OpenSeadragon events.
*
* @typedef {function(OpenSeadragon.Event): Promise<void>} OpenSeadragon.AsyncEventHandler
* @memberof OpenSeadragon
* @param {OpenSeadragon.Event} event - The event object containing event-specific properties.
* @returns {Promise<void>} This handler does not return a value.
*/
@ -62,7 +72,7 @@ $.EventSource.prototype = {
* for a given event. It is not removable with removeHandler().
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event
* @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to call when event
* is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged
* to the handler.
@ -72,10 +82,10 @@ $.EventSource.prototype = {
* @returns {Boolean} - True if the handler was added, false if it was rejected
*/
addOnceHandler: function(eventName, handler, userData, times, priority) {
var self = this;
const self = this;
times = times || 1;
var count = 0;
var onceHandler = function(event) {
let count = 0;
const onceHandler = function(event) {
count++;
if (count === times) {
self.removeHandler(eventName, onceHandler);
@ -89,7 +99,7 @@ $.EventSource.prototype = {
* Add an event handler for a given event.
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered.
* @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to call when event is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
* @returns {Boolean} - True if the handler was added, false if it was rejected
@ -101,12 +111,12 @@ $.EventSource.prototype = {
return false;
}
var events = this.events[ eventName ];
let events = this.events[ eventName ];
if ( !events ) {
this.events[ eventName ] = events = [];
}
if ( handler && $.isFunction( handler ) ) {
var index = events.length,
let index = events.length,
event = { handler: handler, userData: userData || null, priority: priority || 0 };
events[ index ] = event;
while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
@ -122,17 +132,16 @@ $.EventSource.prototype = {
* Remove a specific event handler for a given event.
* @function
* @param {String} eventName - Name of event for which the handler is to be removed.
* @param {OpenSeadragon.EventHandler} handler - Function to be removed.
* @param {OpenSeadragon.EventHandler|OpenSeadragon.AsyncEventHandler} handler - Function to be removed.
*/
removeHandler: function ( eventName, handler ) {
var events = this.events[ eventName ],
handlers = [],
i;
const events = this.events[ eventName ],
handlers = [];
if ( !events ) {
return;
}
if ( $.isArray( events ) ) {
for ( i = 0; i < events.length; i++ ) {
for ( let i = 0; i < events.length; i++ ) {
if ( events[i].handler !== handler ) {
handlers.push( events[ i ] );
}
@ -147,7 +156,7 @@ $.EventSource.prototype = {
* @returns {number} amount of events
*/
numberOfHandlers: function (eventName) {
var events = this.events[ eventName ];
const events = this.events[ eventName ];
if ( !events ) {
return 0;
}
@ -164,7 +173,7 @@ $.EventSource.prototype = {
if ( eventName ){
this.events[ eventName ] = [];
} else{
for ( var eventType in this.events ) {
for ( let eventType in this.events ) {
this.events[ eventType ] = [];
}
}
@ -176,7 +185,7 @@ $.EventSource.prototype = {
* @param {String} eventName - Name of event to get handlers for.
*/
getHandler: function ( eventName) {
var events = this.events[ eventName ];
let events = this.events[ eventName ];
if ( !events || !events.length ) {
return null;
}
@ -184,9 +193,8 @@ $.EventSource.prototype = {
[ events[ 0 ] ] :
Array.apply( null, events );
return function ( source, args ) {
var i,
length = events.length;
for ( i = 0; i < length; i++ ) {
let length = events.length;
for ( let i = 0; i < length; i++ ) {
if ( events[ i ] ) {
args.eventSource = source;
args.userData = events[ i ].userData;
@ -197,7 +205,46 @@ $.EventSource.prototype = {
},
/**
* Trigger an event, optionally passing additional information.
* 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.
* @param {any} bindTarget - Bound target to return with the promise on finish
*/
getAwaitingHandler: function ( eventName, bindTarget ) {
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(bindTarget);
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. Does not await async handlers, i.e.
* OpenSeadragon.AsyncEventHandler.
* @function
* @param {String} eventName - Name of event to register.
* @param {Object} eventArgs - Event-specific data.
@ -205,20 +252,40 @@ $.EventSource.prototype = {
*/
raiseEvent: function( eventName, eventArgs ) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
//$.console.log( "Event fired:", eventName );
if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
$.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
return false;
}
var handler = this.getHandler( eventName );
const handler = this.getHandler( eventName );
if ( handler ) {
handler( this, eventArgs || {} );
}
return true;
},
/**
* Trigger an event, optionally passing additional information.
* This events awaits every asynchronous or promise-returning function, i.e.
* OpenSeadragon.AsyncEventHandler.
* @param {String} eventName - Name of event to register.
* @param {Object} eventArgs - Event-specific data.
* @param {?} [bindTarget = null] - Promise-resolved value on the event finish
* @return {OpenSeadragon.Promise|undefined} - Promise resolved upon the event completion.
*/
raiseEventAwaiting: function ( eventName, eventArgs, bindTarget = null ) {
//uncomment if you want to get a log of all events
//$.console.log( "Awaiting event fired:", eventName );
const awaitingHandler = this.getAwaitingHandler(eventName, bindTarget);
if (awaitingHandler) {
return awaitingHandler(this, eventArgs || {});
}
return $.Promise.resolve(bindTarget);
},
/**
* Set an event name as being disabled, and provide an optional error message
* to be printed to the console
@ -239,7 +306,6 @@ $.EventSource.prototype = {
allowEventHandler(eventName){
delete this._rejectedEventList[eventName];
}
};
}( OpenSeadragon ));

View File

@ -68,12 +68,55 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
this.viewer.rejectEventHandler("tile-drawing", "The HTMLDrawer does not raise the tile-drawing event");
// Since the tile-drawn event is fired by this drawer, make sure handlers can be added for it
this.viewer.allowEventHandler("tile-drawn");
// works with canvas & image objects
function _prepareTile(tile, data) {
const element = $.makeNeutralElement( "div" );
const imgElement = data.cloneNode();
imgElement.style.msInterpolationMode = "nearest-neighbor";
imgElement.style.width = "100%";
imgElement.style.height = "100%";
const style = element.style;
style.position = "absolute";
return {
element, imgElement, style, data
};
}
// The actual placing logics will not happen at draw event, but when the cache is created:
$.convertor.learn("context2d", HTMLDrawer.canvasCacheType, (t, d) => _prepareTile(t, d.canvas), 1, 1);
$.convertor.learn("image", HTMLDrawer.imageCacheType, _prepareTile, 1, 1);
// Also learn how to move back, since these elements can be just used as-is
$.convertor.learn(HTMLDrawer.canvasCacheType, "context2d", (t, d) => d.data.getContext('2d'), 1, 3);
$.convertor.learn(HTMLDrawer.imageCacheType, "image", (t, d) => d.data, 1, 3);
function _freeTile(data) {
if ( data.imgElement && data.imgElement.parentNode ) {
data.imgElement.parentNode.removeChild( data.imgElement );
}
if ( data.element && data.element.parentNode ) {
data.element.parentNode.removeChild( data.element );
}
}
$.convertor.learnDestroy(HTMLDrawer.canvasCacheType, _freeTile);
$.convertor.learnDestroy(HTMLDrawer.imageCacheType, _freeTile);
}
static get imageCacheType() {
return 'htmlDrawer[image]';
}
static get canvasCacheType() {
return 'htmlDrawer[canvas]';
}
/**
* @returns {Boolean} always true
*/
static isSupported(){
static isSupported() {
return true;
}
@ -85,6 +128,10 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
return 'html';
}
getSupportedDataFormats() {
return [HTMLDrawer.imageCacheType, HTMLDrawer.canvasCacheType];
}
/**
* @param {TiledImage} tiledImage the tiled image that is calling the function
* @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
@ -99,8 +146,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
* @returns {Element} the div to draw into
*/
_createDrawingElement(){
let canvas = $.makeNeutralElement("div");
return canvas;
return $.makeNeutralElement("div");
}
/**
@ -200,13 +246,6 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
let container = this.canvas;
if (!tile.cacheImageRecord) {
$.console.warn(
'[Drawer._drawTileToHTML] attempting to draw tile %s when it\'s not cached',
tile.toString());
return;
}
if ( !tile.loaded ) {
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
@ -218,41 +257,29 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
//EXPERIMENTAL - trying to figure out how to scale the container
// content during animation of the container size.
if ( !tile.element ) {
var image = tile.getImage();
if (!image) {
return;
}
tile.element = $.makeNeutralElement( "div" );
tile.imgElement = image.cloneNode();
tile.imgElement.style.msInterpolationMode = "nearest-neighbor";
tile.imgElement.style.width = "100%";
tile.imgElement.style.height = "100%";
tile.style = tile.element.style;
tile.style.position = "absolute";
const dataObject = this.getDataToDraw(tile);
if (!dataObject) {
return;
}
if ( tile.element.parentNode !== container ) {
container.appendChild( tile.element );
if ( dataObject.element.parentNode !== container ) {
container.appendChild( dataObject.element );
}
if ( tile.imgElement.parentNode !== tile.element ) {
tile.element.appendChild( tile.imgElement );
if ( dataObject.imgElement.parentNode !== dataObject.element ) {
dataObject.element.appendChild( dataObject.imgElement );
}
tile.style.top = tile.position.y + "px";
tile.style.left = tile.position.x + "px";
tile.style.height = tile.size.y + "px";
tile.style.width = tile.size.x + "px";
dataObject.style.top = tile.position.y + "px";
dataObject.style.left = tile.position.x + "px";
dataObject.style.height = tile.size.y + "px";
dataObject.style.width = tile.size.x + "px";
if (tile.flipped) {
tile.style.transform = "scaleX(-1)";
dataObject.style.transform = "scaleX(-1)";
}
$.setElementOpacity( tile.element, tile.opacity );
$.setElementOpacity( dataObject.element, tile.opacity );
}
}
$.HTMLDrawer = HTMLDrawer;

View File

@ -263,7 +263,7 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea
if (data.preferredFormats) {
for (var f = 0; f < data.preferredFormats.length; f++ ) {
if ( OpenSeadragon.imageFormatSupported(data.preferredFormats[f]) ) {
if ( $.imageFormatSupported(data.preferredFormats[f]) ) {
data.tileFormat = data.preferredFormats[f];
break;
}
@ -503,6 +503,13 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea
return uri;
},
/**
* Equality comparator
*/
equals: function(otherSource) {
return this._id === otherSource._id;
},
__testonly__: {
canBeTiled: canBeTiled,
constructLevels: constructLevels

View File

@ -48,7 +48,7 @@
* @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
* @param {String} [options.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [options.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @param {Function} [options.callback] - Called once image has been downloaded.
* @param {Function} [options.abort] - Called when this image job is aborted.
* @param {Number} [options.timeout] - The max number of milliseconds that this image job may take to complete.
@ -98,7 +98,7 @@ $.ImageJob.prototype = {
var selfAbort = this.abort;
this.jobId = window.setTimeout(function () {
self.finish(null, null, "Image load exceeded timeout (" + self.timeout + " ms)");
self.fail("Image load exceeded timeout (" + self.timeout + " ms)", null);
}, this.timeout);
this.abort = function() {
@ -115,18 +115,48 @@ $.ImageJob.prototype = {
* Finish this job.
* @param {*} data data that has been downloaded
* @param {XMLHttpRequest} request reference to the request if used
* @param {string} errorMessage description upon failure
* @param {string} dataType data type identifier
* fallback compatibility behavior: dataType treated as errorMessage if data is falsey value
* @memberof OpenSeadragon.ImageJob#
*/
finish: function(data, request, errorMessage ) {
finish: function(data, request, dataType) {
if (!this.jobId) {
return;
}
// 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.request = request;
this.errorMsg = errorMessage;
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.dataType = null;
if (this.jobId) {
window.clearTimeout(this.jobId);
this.jobId = null;
}
this.callback(this);
}
};
@ -167,11 +197,12 @@ $.ImageLoader.prototype = {
* @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX.
* @param {String|Boolean} [options.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [options.postData] - POST parameters (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX
* requests.
* @param {Function} [options.callback] - Called once image has been downloaded.
* @param {Function} [options.abort] - Called when this image job is aborted.
* @returns {boolean} true if job was immediatelly started, false if queued
*/
addJob: function(options) {
if (!options.source) {
@ -184,10 +215,7 @@ $.ImageLoader.prototype = {
};
}
var _this = this,
complete = function(job) {
completeJob(_this, job, options.callback);
},
const _this = this,
jobOptions = {
src: options.src,
tile: options.tile || {},
@ -197,7 +225,7 @@ $.ImageLoader.prototype = {
crossOriginPolicy: options.crossOriginPolicy,
ajaxWithCredentials: options.ajaxWithCredentials,
postData: options.postData,
callback: complete,
callback: (job) => completeJob(_this, job, options.callback),
abort: options.abort,
timeout: this.timeout
},
@ -206,10 +234,17 @@ $.ImageLoader.prototype = {
if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) {
newJob.start();
this.jobsInProgress++;
return true;
}
else {
this.jobQueue.push( newJob );
}
this.jobQueue.push( newJob );
return false;
},
/**
* @returns {boolean} true if a job can be submitted
*/
canAcceptNewJob() {
return !this.jobLimit || this.jobsInProgress < this.jobLimit;
},
/**
@ -238,30 +273,30 @@ $.ImageLoader.prototype = {
* @param callback - Called once cleanup is finished.
*/
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);
}
var nextJob;
let nextJob;
loader.jobsInProgress--;
if ((!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) {
if (loader.canAcceptNewJob() && loader.jobQueue.length > 0) {
nextJob = loader.jobQueue.shift();
nextJob.start();
loader.jobsInProgress++;
}
if (loader.tileRetryMax > 0 && loader.jobQueue.length === 0) {
if ((!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.failedTiles.length > 0) {
nextJob = loader.failedTiles.shift();
setTimeout(function () {
nextJob.start();
}, loader.tileRetryDelay);
loader.jobsInProgress++;
}
}
if (loader.canAcceptNewJob() && loader.failedTiles.length > 0) {
nextJob = loader.failedTiles.shift();
setTimeout(function () {
nextJob.start();
}, loader.tileRetryDelay);
loader.jobsInProgress++;
}
}
callback(job.data, job.errorMsg, job.request);
callback(job.data, job.errorMsg, job.request, job.dataType);
}
}(OpenSeadragon));

View File

@ -31,268 +31,235 @@
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function ($) {
/**
* @class ImageTileSource
* @classdesc The ImageTileSource allows a simple image to be loaded
* into an OpenSeadragon Viewer.
* There are 2 ways to open an ImageTileSource:
* 1. viewer.open({type: 'image', url: fooUrl});
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
*
* With the first syntax, the crossOriginPolicy, ajaxWithCredentials and
* useCanvas options are inherited from the viewer if they are not
* specified directly in the options object.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.TileSource
* @param {Object} options Options object.
* @param {String} options.url URL of the image
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
* pyramid will be built internally to provide a better downsampling.
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
* 'Anonymous', 'use-credentials', and false. If false, image requests will
* not use CORS preventing internal pyramid building for images from other
* domains.
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
* @param {Boolean} [options.useCanvas=true] Set to false to prevent any use
* of the canvas API.
*/
$.ImageTileSource = class extends $.TileSource {
/**
* @class ImageTileSource
* @classdesc The ImageTileSource allows a simple image to be loaded
* into an OpenSeadragon Viewer.
* There are 2 ways to open an ImageTileSource:
* 1. viewer.open({type: 'image', url: fooUrl});
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
*
* With the first syntax, the crossOriginPolicy and ajaxWithCredentials
* options are inherited from the viewer if they are not
* specified directly in the options object.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.TileSource
* @param {Object} options Options object.
* @param {String} options.url URL of the image
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
* pyramid will be built internally to provide a better downsampling.
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
* 'Anonymous', 'use-credentials', and false. If false, image requests will
* not use CORS preventing internal pyramid building for images from other
* domains.
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
*/
$.ImageTileSource = function (options) {
options = $.extend({
constructor(props) {
super($.extend({
buildPyramid: true,
crossOriginPolicy: false,
ajaxWithCredentials: false
}, options);
$.TileSource.apply(this, [options]);
ajaxWithCredentials: false,
}, props));
}
};
/**
* 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 */{
/**
* Determine if the data and/or url imply the image service is supported by
* this tile source.
* @function
* @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) {
image.crossOrigin = this.crossOriginPolicy;
}
if (this.ajaxWithCredentials) {
image.useCredentials = this.ajaxWithCredentials;
}
if (this.crossOriginPolicy) {
image.crossOrigin = this.crossOriginPolicy;
}
if (this.ajaxWithCredentials) {
image.useCredentials = this.ajaxWithCredentials;
}
$.addEvent(image, 'load', function () {
_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.image = image;
_this.levels = _this._buildLevels(image);
_this.maxLevel = _this.levels.length - 1;
$.addEvent(image, 'load', function () {
_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;
_this.ready = true;
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('ready', {tileSource: _this});
});
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('ready', {tileSource: _this});
$.addEvent(image, 'error', function () {
_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 () {
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('open-failed', {
message: "Error loading image at " + url,
source: url
});
});
image.src = url;
}
/**
* @function
* @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;
},
/**
* @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
* @param {OpenSeadragon.Viewer} viewer the viewer that is calling
* destroy on the ImageTileSource
*/
destroy: function (viewer) {
this._freeupCanvasMemory(viewer);
},
/**
* Equality comparator
*/
equals(otherSource) {
return this.url === otherSource.url;
}
// private
//
// Builds the different levels of the pyramid if possible
// (i.e. if canvas API enabled and no canvas tainting issue).
_buildLevels: function () {
var levels = [{
url: this._image.src,
width: this._image.naturalWidth,
height: this._image.naturalHeight
}];
getTilePostData(level, x, y) {
return {level: level, x: x, y: y};
}
if (!this.buildPyramid || !$.supportsCanvas) {
// We don't need the image anymore. Allows it to be GC.
delete this._image;
return levels;
}
/**
* Retrieves a tile context 2D
* @deprecated
*/
getContext2D(level, x, y) {
$.console.error('Using [TiledImage.getContext2D] (for plain images only) is deprecated. ' +
'Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.');
return this._createContext2D();
}
var currentWidth = this._image.naturalWidth;
var currentHeight = this._image.naturalHeight;
downloadTileStart(job) {
const tileData = job.postData;
if (tileData.level === this.maxLevel) {
job.finish(this.image, null, "image");
return;
}
if (tileData.level >= this.minLevel && tileData.level <= this.maxLevel) {
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?`);
}
var bigCanvas = document.createElement("canvas");
var bigContext = bigCanvas.getContext("2d");
downloadTileAbort(job) {
//no-op
}
bigCanvas.width = currentWidth;
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;
// private
//
// 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
}];
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;
}
if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) {
return levels;
},
/**
* Free up canvas memory
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
* and Safari keeps canvas until its height and width will be set to 0).
* @function
*/
_freeupCanvasMemory: function (viewer) {
for (var i = 0; i < this.levels.length; i++) {
if(this.levels[i].context2D){
this.levels[i].context2D.canvas.height = 0;
this.levels[i].context2D.canvas.width = 0;
}
if(viewer){
/**
* Triggered when an image has just been unloaded
*
* @event image-unloaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {CanvasRenderingContext2D} context2D - The context that is being unloaded
* @private
*/
viewer.raiseEvent("image-unloaded", {
context2D: this.levels[i].context2D
});
}
let currentWidth = image.naturalWidth,
currentHeight = image.naturalHeight;
// We build smaller levels until either width or height becomes
// 2 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));

View File

@ -187,6 +187,21 @@ $.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, /** @lends OpenS
url = this.levels[ level ].url;
}
return url;
},
/**
* Equality comparator
*/
equals: function (otherSource) {
if (!otherSource.levels || otherSource.levels.length !== this.levels.length) {
return false;
}
for (let i = this.minLevel; i <= this.maxLevel; i++) {
if (this.levels[i].url !== otherSource.levels[i].url) {
return false;
}
}
return true;
}
} );

View File

@ -722,6 +722,12 @@
* NOTE: passing POST data from URL by this feature only supports string values, however,
* TileSource can send any data using POST as long as the header is correct
* (@see OpenSeadragon.TileSource.prototype.getTilePostData)
*
* @property {Boolean} [callTileLoadedWithCachedData=false]
* tile-loaded event is called only for tiles that downloaded new data or
* their data is stored in the original form in a suplementary cache object.
* Caches that render directly from re-used cache does not trigger this event again,
* as possible modifications would be applied twice.
*/
/**
@ -760,12 +766,16 @@
*/
/**
* @typedef {Object} DrawerOptions
* @typedef {Object.<string, Object>} DrawerOptions - give the renderer options (both shared - BaseDrawerOptions, and custom).
* Supports arbitrary keys: you can register any drawer on the OpenSeadragon namespace, it will get automatically recognized
* and its getType() implementation will define what key to specify the options with.
* @memberof OpenSeadragon
* @property {Object} webgl - options if the WebGLDrawer is used. No options are currently supported.
* @property {Object} canvas - options if the CanvasDrawer is used. No options are currently supported.
* @property {Object} html - options if the HTMLDrawer is used. No options are currently supported.
* @property {Object} custom - options if a custom drawer is used. No options are currently supported.
* @property {BaseDrawerOptions} [webgl] - options if the WebGLDrawer is used.
* @property {BaseDrawerOptions} [canvas] - options if the CanvasDrawer is used.
* @property {BaseDrawerOptions} [html] - options if the HTMLDrawer is used.
* @property {BaseDrawerOptions} [custom] - options if a custom drawer is used.
*
* //Note: if you want to add change options for target drawer change type to {BaseDrawerOptions & MyDrawerOpts}
*/
@ -863,16 +873,20 @@ function OpenSeadragon( options ){
* @private
*/
var class2type = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object AsyncFunction]': 'function',
'[object Promise]': 'promise',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object'
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object AsyncFunction]': 'function',
'[object Promise]': 'promise',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object',
'[object HTMLUnknownElement]': 'dom-node',
'[object HTMLImageElement]': 'image',
'[object HTMLCanvasElement]': 'canvas',
'[object CanvasRenderingContext2D]': 'context2d'
},
// Save a reference to some core methods
toString = Object.prototype.toString,
@ -1066,6 +1080,14 @@ function OpenSeadragon( options ){
return supported >= 3;
}());
/**
* If true, OpenSeadragon uses async execution, else it uses synchronous execution.
* Note that disabling async means no plugins that use Promises / async will work with OSD.
* @member {boolean}
* @memberof OpenSeadragon
*/
$.supportsAsync = true;
/**
* A ratio comparing the device screen's pixel density to the canvas's backing store pixel density,
* clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser.
@ -1231,6 +1253,7 @@ function OpenSeadragon( options ){
loadTilesWithAjax: false,
ajaxHeaders: {},
splitHashDataForPost: false,
callTileLoadedWithCachedData: false,
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
panHorizontal: true,
@ -2296,29 +2319,6 @@ function OpenSeadragon( options ){
event.stopPropagation();
},
// Deprecated
createCallback: function( object, method ) {
//TODO: This pattern is painful to use and debug. It's much cleaner
// to use pinning plus anonymous functions. Get rid of this
// pattern!
console.error('The createCallback function is deprecated and will be removed in future versions. Please use alternativeFunction instead.');
var initialArgs = [],
i;
for ( i = 2; i < arguments.length; i++ ) {
initialArgs.push( arguments[ i ] );
}
return function() {
var args = initialArgs.concat( [] ),
i;
for ( i = 0; i < arguments.length; i++ ) {
args.push( arguments[ i ] );
}
return method.apply( object, args );
};
},
/**
* Retrieves the value of a url parameter from the window.location string.
@ -2367,6 +2367,14 @@ function OpenSeadragon( options ){
},
/**
* Makes an AJAX request.
* @param {String} url - the url to request
* @param {Function} onSuccess
* @param {Function} onError
* @throws {Error}
* @returns {XMLHttpRequest}
* @deprecated deprecated way of calling this function
*//**
* Makes an AJAX request.
* @param {Object} options
* @param {String} options.url - the url to request
@ -2375,7 +2383,7 @@ function OpenSeadragon( options ){
* @param {Object} options.headers - headers to add to the AJAX request
* @param {String} options.responseType - the response type of the AJAX request
* @param {String} options.postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData), GET method used if null
* see TileSource::getTilePostData), GET method used if null
* @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials
* @throws {Error}
* @returns {XMLHttpRequest}
@ -2396,6 +2404,8 @@ function OpenSeadragon( options ){
responseType = url.responseType || null;
postData = url.postData || null;
url = url.url;
} else {
$.console.warn("OpenSeadragon.makeAjaxRequest() deprecated usage!");
}
var protocol = $.getUrlProtocol( url );
@ -2622,10 +2632,13 @@ function OpenSeadragon( options ){
* keys and booleans as values.
*/
setImageFormatsSupported: function(formats) {
//TODO: how to deal with this within the data pipeline?
// $.console.warn("setImageFormatsSupported method is deprecated. You should check that" +
// " the system supports your TileSources by implementing corresponding data type convertors.");
// eslint-disable-next-line no-use-before-define
$.extend(FILEFORMATS, formats);
}
},
});
@ -2891,6 +2904,122 @@ function OpenSeadragon( options ){
}
}
/**
* @template T
* @typedef {function(): OpenSeadragon.Promise<T>} AsyncNullaryFunction
* Represents an asynchronous function that takes no arguments and returns a promise of type T.
*/
/**
* @template T, A
* @typedef {function(A): OpenSeadragon.Promise<T>} AsyncUnaryFunction
* Represents an asynchronous function that:
* @param {A} arg - The single argument of type A.
* @returns {OpenSeadragon.Promise<T>} A promise that resolves to a value of type T.
*/
/**
* @template T, A, B
* @typedef {function(A, B): OpenSeadragon.Promise<T>} AsyncBinaryFunction
* Represents an asynchronous function that:
* @param {A} arg1 - The first argument of type A.
* @param {B} arg2 - The second argument of type B.
* @returns {OpenSeadragon.Promise<T>} A promise that resolves to a value of type T.
*/
/**
* Promise proxy in OpenSeadragon, enables $.supportsAsync feature.
* This proxy is also necessary because OperaMini does not implement Promises (checks fail).
* @type {PromiseConstructor}
*/
$.Promise = window["Promise"] && $.supportsAsync ? window["Promise"] : class {
constructor(handler) {
this._error = false;
this.__value = undefined;
try {
// Make sure to unwrap all nested promises!
handler(
(value) => {
while (value instanceof $.Promise) {
value = value._value;
}
this._value = value;
},
(error) => {
while (error instanceof $.Promise) {
error = error._value;
}
this._value = error;
this._error = true;
}
);
} catch (e) {
this._value = e;
this._error = true;
}
}
then(handler) {
if (!this._error) {
try {
this._value = handler(this._value);
} catch (e) {
this._value = e;
this._error = true;
}
}
return this;
}
catch(handler) {
if (this._error) {
try {
this._value = handler(this._value);
this._error = false;
} catch (e) {
this._value = e;
this._error = true;
}
}
return this;
}
get _value() {
return this.__value;
}
set _value(val) {
if (val && val.constructor === this.constructor) {
val = val._value; //unwrap
}
this.__value = val;
}
static resolve(value) {
return new this((resolve) => resolve(value));
}
static reject(error) {
return new this((_, reject) => reject(error));
}
static all(functions) {
return new this((resolve) => {
// no async support, just execute them
return resolve(functions.map(fn => fn()));
});
}
static race(functions) {
if (functions.length < 1) {
return this.resolve();
}
// no async support, just execute the first
return new this((resolve) => {
return resolve(functions[0]());
});
}
};
}(OpenSeadragon));

View File

@ -139,6 +139,13 @@ $.extend( $.OsmTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead
*/
getTileUrl: function( level, x, y ) {
return this.tilesUrl + (level - 8) + "/" + x + "/" + y + ".png";
},
/**
* Equality comparator
*/
equals: function(otherSource) {
return this.tilesUrl === otherSource.tilesUrl;
}
});

362
src/priorityqueue.js Normal file
View File

@ -0,0 +1,362 @@
/*
* OpenSeadragon - Queue
*
* Copyright (C) 2024 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
*/
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.
* @type {K}
* @private
*/
this.key = key;
/**
* The value.
* @type {V}
* @private
*/
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));

View File

@ -45,15 +45,15 @@
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
* this tile failed to load? )
* @param {String|Function} url The URL of this tile's image or a function that returns a url.
* @param {CanvasRenderingContext2D} context2D The context2D of this tile if it
* is provided directly by the tile source.
* @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it
* * is provided directly by the tile source. Deprecated: use Tile::addCache(...) instead.
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
* @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable).
* @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the
* drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing
* with HTML the entire tile is always used.
* @param {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @param {String} cacheKey key to act as a tile cache, must be unique for tiles with unique image data
*/
$.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) {
@ -103,16 +103,16 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
/**
* Private property to hold string url or url retriever function.
* Consumers should access via Tile.getUrl()
* @private
* @member {String|Function} url
* @memberof OpenSeadragon.Tile#
* @private
*/
this._url = url;
/**
* Post parameters for this tile. For example, it can be an URL-encoded string
* in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used
* @member {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @memberof OpenSeadragon.Tile#
*/
this.postData = postData;
@ -121,7 +121,9 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @member {CanvasRenderingContext2D} context2D
* @memberOf OpenSeadragon.Tile#
*/
this.context2D = context2D;
if (context2D) {
this.context2D = context2D;
}
/**
* Whether to load this tile's image with an AJAX request.
* @member {Boolean} loadWithAjax
@ -141,12 +143,10 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
" in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used.");
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
}
/**
* The unique cache key for this tile.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
this.cacheKey = cacheKey;
this._cKey = cacheKey || "";
this._ocKey = cacheKey || "";
/**
* Is this tile loaded?
* @member {Boolean} loaded
@ -159,26 +159,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @memberof OpenSeadragon.Tile#
*/
this.loading = false;
/**
* The HTML div element for this tile
* @member {Element} element
* @memberof OpenSeadragon.Tile#
*/
this.element = null;
/**
* The HTML img element for this tile.
* @member {Element} imgElement
* @memberof OpenSeadragon.Tile#
*/
this.imgElement = null;
/**
* The alias of this.element.style.
* @member {String} style
* @memberof OpenSeadragon.Tile#
*/
this.style = null;
/**
* This tile's position on screen, in pixels.
* @member {OpenSeadragon.Point} position
@ -212,9 +192,9 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
/**
* The squared distance of this tile to the viewport center.
* Use for comparing tiles.
* @private
* @member {Number} squaredDistance
* @memberof OpenSeadragon.Tile#
* @private
*/
this.squaredDistance = null;
/**
@ -258,6 +238,32 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @memberof OpenSeadragon.Tile#
*/
this.isBottomMost = false;
/**
* Owner of this tile. Do not change this property manually.
* @member {OpenSeadragon.TiledImage}
* @memberof OpenSeadragon.Tile#
*/
this.tiledImage = null;
/**
* Array of cached tile data associated with the tile.
* @member {Object}
* @private
*/
this._caches = {};
/**
* Processing flag, exempt the tile from removal when there are ongoing updates
* @member {Boolean|Number}
* @private
*/
this.processing = false;
/**
* Processing promise, resolves when the tile exits processing, or
* resolves immediatelly if not in the processing state.
* @member {OpenSeadragon.Promise}
* @private
*/
this.processingPromise = $.Promise.resolve();
};
/** @lends OpenSeadragon.Tile.prototype */
@ -273,11 +279,42 @@ $.Tile.prototype = {
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');
/**
* The unique main cache key for this tile. Created automatically
* from the given tiledImage.source.getTileHashKey(...) implementation.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
get cacheKey() {
return this._cKey;
},
set cacheKey(value) {
if (value === this.cacheKey) {
return;
}
const cache = this.getCache(value);
if (!cache) {
// It's better to first set cache, then change the key to existing one. Warn if otherwise.
$.console.warn("[Tile.cacheKey] should not be set manually. Use addCache() with setAsMain=true.");
}
this._updateMainCacheKey(value);
},
/**
* By default equal to tile.cacheKey, marks a cache associated with this tile
* that holds the cache original data (it was loaded with). In case you
* change the tile data, the tile original data should be left with the cache
* 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'.
* This key is used in cache resolution: in case new tile data is requested, if
* this cache key exists in the cache it is loaded.
* @member {String} originalCacheKey
* @memberof OpenSeadragon.Tile#
*/
set originalCacheKey(value) {
throw "Original Cache Key cannot be managed manually!";
},
get originalCacheKey() {
return this._ocKey;
},
/**
@ -288,7 +325,7 @@ $.Tile.prototype = {
* @returns {Image}
*/
get image() {
$.console.error("[Tile.image] property has been deprecated. Use [Tile.prototype.getImage] instead.");
$.console.error("[Tile.image] property has been deprecated. Use [Tile.getData] instead.");
return this.getImage();
},
@ -300,16 +337,80 @@ $.Tile.prototype = {
* @returns {String}
*/
get url() {
$.console.error("[Tile.url] property has been deprecated. Use [Tile.prototype.getUrl] instead.");
$.console.error("[Tile.url] property has been deprecated. Use [Tile.getUrl] instead.");
return this.getUrl();
},
/**
* The HTML div element for this tile
* @member {Element} element
* @memberof OpenSeadragon.Tile#
* @deprecated
*/
get element() {
$.console.error("Tile::element property is deprecated. Use cache API instead. Moreover, this property might be unstable.");
const cache = this.getCache();
if (!cache || !cache.loaded) {
return null;
}
if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) {
$.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!");
return null;
}
return cache.data.element;
},
/**
* The HTML img element for this tile.
* @member {Element} imgElement
* @memberof OpenSeadragon.Tile#
* @deprecated
*/
get imgElement() {
$.console.error("Tile::imgElement property is deprecated. Use cache API instead. Moreover, this property might be unstable.");
const cache = this.getCache();
if (!cache || !cache.loaded) {
return null;
}
if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) {
$.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!");
return null;
}
return cache.data.imgElement;
},
/**
* The alias of this.element.style.
* @member {String} style
* @memberof OpenSeadragon.Tile#
* @deprecated
*/
get style() {
$.console.error("Tile::style property is deprecated. Use cache API instead. Moreover, this property might be unstable.");
const cache = this.getCache();
if (!cache || !cache.loaded) {
return null;
}
if (cache.type !== OpenSeadragon.HTMLDrawer.canvasCacheType || cache.type !== OpenSeadragon.HTMLDrawer.imageCacheType) {
$.console.error("Access to HtmlDrawer property via Tile instance: HTMLDrawer must be used!");
return null;
}
return cache.data.style;
},
/**
* Get the Image object for this tile.
* @returns {Image}
* @returns {?Image}
*/
getImage: function() {
return this.cacheImageRecord.getImage();
$.console.error("[Tile.getImage] property has been deprecated. Use 'tile-invalidated' routine event instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey);
if (!cache) {
return undefined;
}
cache.transformTo("image");
return cache.data;
},
/**
@ -327,24 +428,290 @@ $.Tile.prototype = {
/**
* Get the CanvasRenderingContext2D instance for tile image data drawn
* onto Canvas if enabled and available
* @returns {CanvasRenderingContext2D}
* @returns {CanvasRenderingContext2D|undefined}
*/
getCanvasContext: function() {
return this.context2D || (this.cacheImageRecord && this.cacheImageRecord.getRenderedContext());
$.console.error("[Tile.getCanvasContext] property has been deprecated. Use 'tile-invalidated' routine event instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey);
if (!cache) {
return undefined;
}
cache.transformTo("context2d");
return cache.data;
},
/**
* The context2D of this tile if it is provided directly by the tile source.
* @deprecated
* @type {CanvasRenderingContext2D}
*/
get context2D() {
$.console.error("[Tile.context2D] property has been deprecated. Use 'tile-invalidated' routine event instead.");
return this.getCanvasContext();
},
/**
* 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-invalidated' routine event instead.");
const cache = this._caches[this.cacheKey];
if (cache) {
this.removeCache(this.cacheKey);
}
this.addCache(this.cacheKey, value, 'context2d', true, false);
},
/**
* 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::addCache.");
const cache = this._caches[this.cacheKey];
if (cache) {
this.removeCache(this.cacheKey);
}
if (value) {
if (value.loaded) {
this.addCache(this.cacheKey, value.data, value.type, true, false);
} else {
value.await().then(x => this.addCache(this.cacheKey, x, value.type, true, false));
}
}
},
/**
* Cache key for main cache that is 'cache-equal', but different from original cache key
* @return {string}
* @private
*/
buildDistinctMainCacheKey: function () {
return this.cacheKey === this.originalCacheKey ? "mod://" + this.originalCacheKey : this.cacheKey;
},
/**
* Read tile cache data object (CacheRecord)
* @param {string} [key=this.cacheKey] cache key to read that belongs to this tile
* @return {OpenSeadragon.CacheRecord}
*/
getCache: function(key = this._cKey) {
const cache = this._caches[key];
if (cache) {
cache.withTileReference(this);
}
return cache;
},
/**
* Create tile cache for given data object.
*
* Using `setAsMain` updates also main tile cache key - the main cache key used to draw this tile.
* In that case, the cache should be ready to be rendered immediatelly (converted to one of the supported formats
* of the currently employed drawer).
*
* NOTE: if the existing cache already exists,
* data parameter is ignored and inherited from the existing cache object.
* WARNING: if you override main tile cache key to point to a different cache, the invalidation routine
* will no longer work. If you need to modify tile main data, prefer to use invalidation routine instead.
*
* @param {string} key cache key, if unique, new cache object is created, else existing cache attached
* @param {*} data this data will be IGNORED if cache already exists; therefore if
* `typeof data === 'function'` holds (both async and normal functions), the data is called to obtain
* the data item: this is an optimization to load data only when necessary.
* @param {string} [type=undefined] data type, will be guessed if not provided (not recommended),
* if data is a callback the type is a mandatory field, not setting it results in undefined behaviour
* @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey,
* no effect if key === this.cacheKey
* @param [_safely=true] private
* @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to.
*/
addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) {
const tiledImage = this.tiledImage;
if (!tiledImage) {
return null; //async can access outside its lifetime
}
if (!type) {
if (!this.__typeWarningReported) {
$.console.warn(this, "[Tile.addCache] called without type specification. " +
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
this.__typeWarningReported = true;
}
if (typeof data === 'function') {
$.console.error("[TileCache.cacheTile] options.data as a callback requires type argument! Current is " + type);
}
type = $.convertor.guessType(data);
}
const overwritesMainCache = key === this.cacheKey;
if (_safely && (overwritesMainCache || setAsMain)) {
// Need to get the supported type for rendering out of the active drawer.
const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(type, supportedTypes);
$.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" +
`to render. Make sure OpenSeadragon.convertor was taught to convert ${type} to (one of): ${conversion.toString()}`);
}
const cachedItem = tiledImage._tileCache.cacheTile({
data: data,
dataType: type,
tile: this,
cacheKey: key,
cutoff: tiledImage.source.getClosestLevel(),
});
const havingRecord = this._caches[key];
if (havingRecord !== cachedItem) {
this._caches[key] = cachedItem;
if (havingRecord) {
havingRecord.removeTile(this);
tiledImage._tileCache.safeUnloadCache(havingRecord);
}
}
// Update cache key if differs and main requested
if (!overwritesMainCache && setAsMain) {
this._updateMainCacheKey(key);
}
return cachedItem;
},
/**
* Add cache object to the tile
*
* @param {string} key cache key, if unique, new cache object is created, else existing cache attached
* @param {OpenSeadragon.CacheRecord} cache the cache object to attach to this tile
* @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey,
* no effect if key === this.cacheKey
* @param [_safely=true] private
* @returns {OpenSeadragon.CacheRecord|null} - Returns cache parameter reference if attached.
*/
setCache(key, cache, setAsMain = false, _safely = true) {
const tiledImage = this.tiledImage;
if (!tiledImage) {
return null; //async can access outside its lifetime
}
const overwritesMainCache = key === this.cacheKey;
if (_safely) {
$.console.assert(cache instanceof $.CacheRecord, "[Tile.setCache] cache must be a CacheRecord object!");
if (overwritesMainCache || setAsMain) {
// Need to get the supported type for rendering out of the active drawer.
const supportedTypes = tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(cache.type, supportedTypes);
$.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 ${cache.type} to (one of): ${conversion.toString()}`);
}
}
const havingRecord = this._caches[key];
if (havingRecord !== cache) {
this._caches[key] = cache;
cache.addTile(this); // keep reference bidirectional
if (havingRecord) {
havingRecord.removeTile(this);
tiledImage._tileCache.safeUnloadCache(havingRecord);
}
}
// Update cache key if differs and main requested
if (!overwritesMainCache && setAsMain) {
this._updateMainCacheKey(key);
}
return cache;
},
/**
* Sets the main cache key for this tile and
* performs necessary updates
* @param value
* @private
*/
_updateMainCacheKey: function(value) {
let ref = this._caches[this._cKey];
if (ref) {
// make sure we free drawer internal cache if people change cache key externally
ref.destroyInternalCache();
}
this._cKey = value;
},
/**
* Get the number of caches available to this tile
* @returns {number} number of caches
*/
getCacheSize: function() {
return Object.values(this._caches).length;
},
/**
* Free tile cache. Removes by default the cache record if no other tile uses it.
* @param {string} key cache key, required
* @param {boolean} [freeIfUnused=true] set to false if zombie should be created
* @return {OpenSeadragon.CacheRecord|undefined} reference to the cache record if it was removed,
* undefined if removal was refused to perform (e.g. does not exist, it is an original data target etc.)
*/
removeCache: function(key, freeIfUnused = true) {
const deleteTarget = this._caches[key];
if (!deleteTarget) {
// try to erase anyway in case the cache got stuck in memory
this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, true);
return undefined;
}
const currentMainKey = this.cacheKey,
originalDataKey = this.originalCacheKey,
sameBuiltinKeys = currentMainKey === originalDataKey;
if (!sameBuiltinKeys && originalDataKey === key) {
$.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!",
"If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().");
return undefined;
}
if (currentMainKey === key) {
if (!sameBuiltinKeys && this._caches[originalDataKey]) {
// if we have original data let's revert back
this._updateMainCacheKey(originalDataKey);
} else {
$.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!",
"If you want to remove the main cache, first set different cache as main with tile.addCache()");
return undefined;
}
}
if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused, false)) {
//if we managed to free tile from record, we are sure we decreased cache count
delete this._caches[key];
}
return deleteTarget;
},
/**
* Get the ratio between current and original size.
* @function
* @returns {Float}
* @deprecated
* @returns {number}
*/
getScaleForEdgeSmoothing: function() {
var context;
if (this.cacheImageRecord) {
context = this.cacheImageRecord.getRenderedContext();
} else if (this.context2D) {
context = this.context2D;
} else {
// getCanvasContext is deprecated and so should be this method.
$.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:");
const context = this.getCanvasContext();
if (!context) {
$.console.warn(
'[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached',
this.toString());
@ -365,8 +732,8 @@ $.Tile.prototype = {
// the sketch canvas to the top and left and we must use negative coordinates to repaint it
// to the main canvas. In that case, some browsers throw:
// INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value.
var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2));
var y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2));
const x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2));
const y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2));
return new $.Point(x, y).minus(
this.position
.times($.pixelDensityRatio)
@ -378,21 +745,61 @@ $.Tile.prototype = {
},
/**
* Removes tile from its container.
* Reflect that a cache object was renamed. Called internally from TileCache.
* Do NOT call manually.
* @function
* @private
*/
unload: function() {
if ( this.imgElement && this.imgElement.parentNode ) {
this.imgElement.parentNode.removeChild( this.imgElement );
reflectCacheRenamed: function (oldKey, newKey) {
let cache = this._caches[oldKey];
if (!cache) {
return; // nothing to fix
}
if ( this.element && this.element.parentNode ) {
this.element.parentNode.removeChild( this.element );
// Do update via private refs, old key no longer exists in cache
if (oldKey === this._ocKey) {
this._ocKey = newKey;
}
if (oldKey === this._cKey) {
this._cKey = newKey;
}
// Working key is never updated, it will be invalidated (but do not dereference cache, just fix the pointers)
this._caches[newKey] = cache;
delete this._caches[oldKey];
},
/**
* Check if two tiles are data-equal
* @param {OpenSeadragon.Tile} tile
*/
equals(tile) {
return this._ocKey === tile._ocKey;
},
/**
* Removes tile from the system: it will still be present in the
* OSD memory, but marked as loaded=false, and its data will be erased.
* @param {boolean} [erase=false]
*/
unload: function(erase = false) {
if (!this.loaded) {
return;
}
this.tiledImage._tileCache.unloadTile(this, erase);
},
/**
* this method shall be called only by cache system when the tile is already empty of data
* @private
*/
_unload: function () {
this.tiledImage = null;
this._caches = {};
this._cacheSize = 0;
this.element = null;
this.imgElement = null;
this.loaded = false;
this.loading = false;
this._cKey = this._ocKey;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -163,6 +163,7 @@ $.TiledImage = function( options ) {
_needsUpdate: true, // Does the tiledImage need to update the viewport again?
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
_tilesLoading: 0, // The number of pending tile requests.
_zombieCache: false, // Allow cache to stay in memory upon deletion.
_tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile]
_lastDrawn: [], // array of tiles that were last fetched by the drawer
_isBlending: false, // Are any tiles still being blended?
@ -188,7 +189,8 @@ $.TiledImage = function( options ) {
preload: $.DEFAULT_SETTINGS.preload,
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency,
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame
maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame,
_currentMaxTilesPerFrame: (options.maxTilesPerFrame || $.DEFAULT_SETTINGS.maxTilesPerFrame) * 10
}, options );
this._preload = this.preload;
@ -229,6 +231,7 @@ $.TiledImage = function( options ) {
this._ownAjaxHeaders = {};
this.setAjaxHeaders(ajaxHeaders, false);
this._initialized = true;
// this.invalidatedAt = 0;
};
$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{
@ -277,14 +280,30 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
});
},
/**
* Forces the system consider all tiles in this tiled image
* as outdated, and fire tile update event on relevant tiles
* Detailed description is available within the 'tile-invalidated'
* event.
* @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
* @param {boolean} [viewportOnly=false] optionally invalidate only viewport-visible tiles if true
* @param {number} [tStamp=OpenSeadragon.now()] optionally provide tStamp of the update event
*/
requestInvalidate: function (restoreTiles = true, viewportOnly = false, tStamp = $.now()) {
const tiles = viewportOnly ? this._lastDrawn.map(x => x.tile) : this._tileCache.getLoadedTilesFor(this);
return this.viewer.world.requestTileInvalidateEvent(tiles, tStamp, restoreTiles);
},
/**
* Clears all tiles and triggers an update on the next call to
* {@link OpenSeadragon.TiledImage#update}.
*/
reset: function() {
this._tileCache.clearTilesFor(this);
this._currentMaxTilesPerFrame = this.maxTilesPerFrame * 10;
this.lastResetTime = $.now();
this._needsDraw = true;
this._fullyLoaded = false;
},
/**
@ -325,7 +344,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @returns {Boolean} whether the item still needs to be drawn due to blending
*/
setDrawn: function(){
this._needsDraw = this._isBlending || this._wasBlending;
this._needsDraw = this._isBlending || this._wasBlending ||
(this.opacity > 0 && this._lastDrawn.length < 1);
return this._needsDraw;
},
@ -353,10 +373,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/
destroy: function() {
this.reset();
if (this.source.destroy) {
this.source.destroy(this.viewer);
}
this.source.destroy(this.viewer);
},
/**
@ -907,13 +924,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this.flipped = flip;
},
get flipped(){
get flipped() {
return this._flipped;
},
set flipped(flipped){
set flipped(flipped) {
let changed = this._flipped !== !!flipped;
this._flipped = !!flipped;
if(changed){
if (changed && this._initialized) {
this.update(true);
this._needsDraw = true;
this._raiseBoundsChange();
@ -1162,7 +1179,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
ajaxHeaders = {};
}
if (!$.isPlainObject(ajaxHeaders)) {
console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
$.console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return;
}
@ -1227,6 +1244,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
_setScale: function(scale, immediately) {
var sameTarget = (this._scaleSpring.target.value === scale);
@ -1433,20 +1462,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
// Load the new 'best' n tiles
if (bestTiles && bestTiles.length > 0) {
bestTiles.forEach(function (tile) {
if (tile && !tile.context2D) {
for (let tile of bestTiles) {
if (tile) {
this._loadTile(tile, currentTime);
}
}, this);
}
this._needsDraw = true;
return false;
} else {
return this._tilesLoading === 0;
}
// Update
},
/**
@ -1736,12 +1761,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
_updateTile: function( x, y, level,
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
var tile = this._getTile(
const tile = this._getTile(
x, y,
level,
currentTime,
numberOfTiles
);
);
if( this.viewer ){
/**
@ -1784,22 +1810,20 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
levelVisibility
);
if (!tile.loaded) {
if (tile.context2D) {
this._setTileLoaded(tile);
} else {
var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
if (imageRecord) {
this._setTileLoaded(tile, imageRecord.getData());
}
}
// Try-find will populate tile with data if equal tile exists in system
if (!tile.loaded && !tile.loading && this._tryFindTileCacheRecord(tile)) {
loadingCoverage = true;
}
if ( tile.loading ) {
// the tile is already in the download queue
this._tilesLoading++;
} else if (!loadingCoverage) {
best = this._compareTiles( best, tile, this.maxTilesPerFrame );
// add tile to best tiles to load only when not loaded already
best = this._compareTiles( best, tile, this._currentMaxTilesPerFrame );
if (this._currentMaxTilesPerFrame > this.maxTilesPerFrame) {
this._currentMaxTilesPerFrame = Math.max(Math.ceil(this.maxTilesPerFrame / 2), this.maxTilesPerFrame);
}
}
return {
@ -1850,6 +1874,25 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
* @private
* @inner
* Try to find existing cache of the tile
* @param {OpenSeadragon.Tile} tile
*/
_tryFindTileCacheRecord: function(tile) {
let record = this._tileCache.getCacheRecord(tile.originalCacheKey);
if (!record) {
return false;
}
tile.loading = true;
this._setTileLoaded(tile, record.data, null, null, record.type);
return true;
},
/**
* @private
* @inner
* Obtains a tile at the given location.
* @private
* @param {Number} x
@ -1873,7 +1916,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
urlOrGetter,
post,
ajaxHeaders,
context2D,
tile,
tilesMatrix = this.tilesMatrix,
tileSource = this.source;
@ -1905,9 +1947,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
ajaxHeaders = null;
}
context2D = tileSource.getContext2D ?
tileSource.getContext2D(level, xMod, yMod) : undefined;
tile = new $.Tile(
level,
x,
@ -1915,7 +1954,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bounds,
exists,
urlOrGetter,
context2D,
undefined,
this.loadTilesWithAjax,
ajaxHeaders,
sourceBounds,
@ -1957,7 +1996,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
_loadTile: function(tile, time ) {
var _this = this;
tile.loading = true;
this._imageLoader.addJob({
tile.tiledImage = this;
if (!this._imageLoader.addJob({
src: tile.getUrl(),
tile: tile,
source: this.source,
@ -1966,13 +2006,29 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
ajaxHeaders: tile.ajaxHeaders,
crossOriginPolicy: this.crossOriginPolicy,
ajaxWithCredentials: this.ajaxWithCredentials,
callback: function( data, errorMsg, tileRequest ){
_this._onTileLoad( tile, time, data, errorMsg, tileRequest );
callback: function( data, errorMsg, tileRequest, dataType ){
_this._onTileLoad( tile, time, data, errorMsg, tileRequest, dataType );
},
abort: function() {
tile.loading = false;
}
});
})) {
/**
* Triggered if tile load job was added to a full queue.
* This allows to react upon e.g. network not being able to serve the tiles fast enough.
* @event job-queue-full
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Tile} tile - The tile that failed to load.
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to.
* @property {number} time - The time in milliseconds when the tile load began.
*/
this.viewer.raiseEvent("job-queue-full", {
tile: tile,
tiledImage: this,
time: time,
});
}
},
/**
@ -1983,9 +2039,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {*} data image data
* @param {String} errorMsg
* @param {XMLHttpRequest} tileRequest
* @param {String} [dataType=undefined] data type, derived automatically if not set
*/
_onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
if ( !data ) {
_onTileLoad: function( tile, time, data, errorMsg, tileRequest, dataType ) {
//data is set to null on error by image loader, allow custom falsey values (e.g. 0)
if ( data === null || data === undefined ) {
$.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
/**
* Triggered when a tile fails to load.
@ -2019,28 +2077,50 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return;
}
var _this = this,
finish = function() {
var ccc = _this.source;
var cutoff = ccc.getClosestLevel();
_this._setTileLoaded(tile, data, cutoff, tileRequest);
};
finish();
this._setTileLoaded(tile, data, null, tileRequest, dataType);
},
/**
* @private
* @param {OpenSeadragon.Tile} tile
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
* @param {Number|undefined} cutoff
* @param {XMLHttpRequest|undefined} tileRequest
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object,
* can be null: in that case, cache is assigned to a tile without further processing
* @param {?Number} cutoff ignored, @deprecated
* @param {?XMLHttpRequest} tileRequest
* @param {?String} [dataType=undefined] data type, derived automatically if not set
*/
_setTileLoaded: function(tile, data, cutoff, tileRequest) {
var increment = 0,
eventFinished = false,
_this = this;
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType) {
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
// does nothing if tile.cacheKey already present
let tileCacheCreated = false;
tile.addCache(tile.cacheKey, () => {
tileCacheCreated = true;
return data;
}, dataType, false, false);
let resolver = null,
increment = 0,
eventFinished = false;
const _this = this,
now = $.now();
function completionCallback() {
increment--;
if (increment > 0) {
return;
}
eventFinished = true;
//do not override true if set (false is default)
tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency(
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData
);
tile.loading = false;
tile.loaded = true;
_this.redraw();
resolver(tile);
}
function getCompletionCallback() {
if (eventFinished) {
@ -2051,76 +2131,81 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return completionCallback;
}
function completionCallback() {
increment--;
if (increment === 0) {
tile.loading = false;
tile.loaded = true;
tile.hasTransparency = _this.source.hasTransparency(
tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
);
if (!tile.context2D) {
_this._tileCache.cacheTile({
data: data,
tile: tile,
cutoff: cutoff,
tiledImage: _this
});
}
/**
* Triggered when a tile is loaded and pre-processing is compelete,
* and the tile is ready to draw.
*
* @event tile-ready
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
* @private
*/
_this.viewer.raiseEvent("tile-ready", {
tile: tile,
tiledImage: _this,
tileRequest: tileRequest
});
_this._needsDraw = true;
}
function markTileAsReady() {
const fallbackCompletion = getCompletionCallback();
/**
* 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.
* This event is _awaiting_, it supports asynchronous functions or functions that return a promise.
*
* @event tile-loaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @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. Deprecated
* @property {String} dataType type of the data
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
* @property {OpenSeadragon.Promise} - Promise resolved when the tile gets fully loaded.
* NOTE: do no await the promise in the handler: you will create a deadlock!
* @property {function} getCompletionCallback - deprecated
*/
_this.viewer.raiseEventAwaiting("tile-loaded", {
tile: tile,
tiledImage: _this,
tileRequest: tileRequest,
promise: new $.Promise(resolve => {
resolver = resolve;
}),
get image() {
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile-invalidated' event to modify data instead.");
return data;
},
get data() {
$.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile-invalidated' event to modify data instead.");
return data;
},
getCompletionCallback: function () {
$.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " +
"use async event handlers instead, execution order is deducted by addHandler(...) priority argument.");
return getCompletionCallback();
},
}).catch(() => {
$.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.");
}).then(fallbackCompletion);
}
/**
* 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.
*
* @event tile-loaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @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 {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
* @property {function} getCompletionCallback - A function giving a callback to call
* when the asynchronous processing of the image is done. The image will be
* marked as entirely loaded when the callback has been called once for each
* call to getCompletionCallback.
*/
var fallbackCompletion = getCompletionCallback();
this.viewer.raiseEvent("tile-loaded", {
tile: tile,
tiledImage: this,
tileRequest: tileRequest,
get image() {
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
return data;
},
data: data,
getCompletionCallback: getCompletionCallback
});
eventFinished = true;
// In case the completion callback is never called, we at least force it once.
fallbackCompletion();
if (tileCacheCreated) {
_this.viewer.world.requestTileInvalidateEvent([tile], now, false, true).then(markTileAsReady);
} else {
// Tile-invalidated not called on each tile, but only on tiles with new data! Verify we share the main cache
const origCache = tile.getCache(tile.originalCacheKey);
for (let t of origCache._tiles) {
// if there exists a tile that has different main cache, inherit it as a main cache
if (t.cacheKey !== tile.cacheKey) {
// add reference also to the main cache, no matter what the other tile state has
// completion of the invaldate event should take care of all such tiles
const targetMainCache = t.getCache();
tile.setCache(t.cacheKey, targetMainCache, true, false);
break;
} else if (t.processing) {
// Await once processing finishes - mark tile as loaded
t.processingPromise.then(t => {
const targetMainCache = t.getCache();
tile.setCache(t.cacheKey, targetMainCache, true, false);
markTileAsReady();
});
return;
}
}
markTileAsReady();
}
},
@ -2170,7 +2255,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
});
},
/**
* Returns true if the given tile provides coverage to lower-level tiles of
* lower resolution representing the same content. If neither x nor y is

View File

@ -55,7 +55,8 @@
* @param {Object} options
* You can either specify a URL, or literally define the TileSource (by specifying
* width, height, tileSize, tileOverlap, minLevel, and maxLevel). For the former,
* the extending class is expected to implement 'getImageInfo' and 'configure'.
* the extending class is expected to implement 'supports' and 'configure'.
* Note that _in this case, the child class of getImageInfo() is ignored!_
* For the latter, the construction is assumed to occur through
* the extending classes implementation of 'configure'.
* @param {String} [options.url]
@ -72,6 +73,7 @@
* @param {Boolean} [options.splitHashDataForPost]
* First occurrence of '#' in the options.url is used to split URL
* and the latter part is treated as POST data (applies to getImageInfo(...))
* Does not work if getImageInfo() is overridden and used (see the options description)
* @param {Number} [options.width]
* Width of the source image at max resolution in pixels.
* @param {Number} [options.height]
@ -139,6 +141,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
* @member {Number} aspectRatio
@ -170,6 +178,8 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
* @memberof OpenSeadragon.TileSource#
*/
// TODO potentially buggy behavior: what if .url is used by child class before it calls super constructor?
// this can happen if old JS class definition is used
if( 'string' === $.type( arguments[ 0 ] ) ){
this.url = arguments[0];
}
@ -425,6 +435,13 @@ $.TileSource.prototype = {
/**
* Responsible for retrieving, and caching the
* image metadata pertinent to this TileSources implementation.
* There are three scenarios of opening a tile source: providing a parseable string, plain object, or an URL.
* This method is only called by OSD if the TileSource configuration is a non-parseable string (~url).
*
* The string can contain a hash `#` symbol, followed by
* key=value arguments. If this is the case, this method sends this
* data as a POST body.
*
* @function
* @param {String} url
* @throws {Error}
@ -554,7 +571,7 @@ $.TileSource.prototype = {
* @property {String} message
* @property {String} source
* @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'open-failed', {
@ -586,6 +603,17 @@ $.TileSource.prototype = {
return false;
},
/**
* Check whether two tileSources are equal. This is used for example
* when replacing tile-sources, which turns on the zombie cache before
* old item removal.
* @param {OpenSeadragon.TileSource} otherSource
* @returns {Boolean}
*/
equals: function (otherSource) {
return false;
},
/**
* Responsible for parsing and configuring the
* image metadata pertinent to this TileSources implementation.
@ -608,6 +636,16 @@ $.TileSource.prototype = {
throw new Error( "Method not implemented." );
},
/**
* Shall this source need to free some objects
* upon unloading, it must be done here. For example, canvas
* size must be set to 0 for safari to free.
* @param {OpenSeadragon.Viewer} viewer
*/
destroy: function ( viewer ) {
//no-op
},
/**
* Responsible for retrieving the url which will return an image for the
* region specified by the given x, y, and level components.
@ -683,9 +721,12 @@ $.TileSource.prototype = {
* 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.
*
* In case a tile has context2D property defined (TileSource.prototype.getContext2D)
* or its context2D is set manually; the cache is not used and this function
* is irrelevant.
* You can return falsey tile cache key, in which case the tile will
* be created without invoking ImageJob --- but with data=null. Then,
* 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.
* @param {Number} level tile level it was fetched with
* @param {Number} x x-coordinate in the pyramid level
@ -693,6 +734,9 @@ $.TileSource.prototype = {
* @param {String} url 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)
* @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) {
function withHeaders(hash) {
@ -723,10 +767,15 @@ $.TileSource.prototype = {
/**
* 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
*/
hasTransparency: function(context2D, url, ajaxHeaders, post) {
return !!context2D || url.match('.png');
return url.match('.png');
},
/**
@ -738,41 +787,45 @@ $.TileSource.prototype = {
* @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 {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,
* see TileSource::getPostData) or null
* @param {?String|?Object} [context.postData] - HTTP POST data (usually but not necessarily
* in k=v&k2=v2... form, see TileSource::getTilePostData) or null
* @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,
* be it successful or unsuccessful request.
* Usage: context.finish(data, request, errMessage). Pass the downloaded data object or null upon failure.
* Add also reference to an ajax request if used. Provide error message in case of failure.
* @param {Function} [context.finish] - Should be called unless abort() was executed upon successful
* data retrieval.
* Usage: context.finish(data, request, dataType=undefined). Pass the downloaded data object
* 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.
* Usage: context.abort().
* @param {Function} [context.callback] @private - Called automatically once image has been downloaded
* Usage: if you decide to abort the request (no fail/finish will be called), call context.abort().
* @param {Function} [context.callback] Private parameter. Called automatically once image has been downloaded
* (triggered by finish).
* @param {Number} [context.timeout] @private - The max number of milliseconds that
* @param {Number} [context.timeout] Private parameter. The max number of milliseconds that
* this image job may take to complete.
* @param {string} [context.errorMsg] @private - The final error message, default null (set by finish).
* @param {string} [context.errorMsg] Private parameter. The final error message, default null (set by finish).
*/
downloadTileStart: function (context) {
var dataStore = context.userData,
const dataStore = context.userData,
image = new Image();
dataStore.image = image;
dataStore.request = null;
var finish = function(error) {
if (!image) {
context.finish(null, dataStore.request, "Image load failed: undefined Image instance.");
const finalize = function(error) {
if (error || !image) {
context.fail(error || "[downloadTileStart] Image load failed: undefined Image instance.",
dataStore.request);
return;
}
image.onload = image.onerror = image.onabort = null;
context.finish(error ? null : image, dataStore.request, error);
context.finish(image, dataStore.request, "image");
};
image.onload = function () {
finish();
finalize();
};
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
@ -792,21 +845,21 @@ $.TileSource.prototype = {
try {
blb = new window.Blob([request.response]);
} catch (e) {
var BlobBuilder = (
const BlobBuilder = (
window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder
);
if (e.name === 'TypeError' && BlobBuilder) {
var bb = new BlobBuilder();
const bb = new BlobBuilder();
bb.append(request.response);
blb = bb.getBlob();
}
}
// If the blob is empty for some reason consider the image load a failure.
if (blb.size === 0) {
finish("Empty image response.");
finalize("[downloadTileStart] Empty image response.");
} else {
// 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.
@ -814,7 +867,7 @@ $.TileSource.prototype = {
}
},
error: function(request) {
finish("Image load aborted - XHR error");
finalize("[downloadTileStart] Image load aborted - XHR error");
}
});
} else {
@ -828,6 +881,8 @@ $.TileSource.prototype = {
/**
* Provide means of aborting the execution.
* 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 {*} [context.userData] - Empty object to attach (and mainly read) your own data.
*/
@ -846,33 +901,44 @@ $.TileSource.prototype = {
* 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.
*
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* Note that
* - 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 {Tile} tile instance the cache was created with
* @param {OpenSeadragon.Tile} tile instance the cache was created with
* @deprecated
*/
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.
* 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) {
cacheObject._data = null;
cacheObject._renderedContext = null;
$.console.error("[TileSource.destroyTileCache] has been deprecated. Use cache API of a tile instead.");
//no-op, handled internally
},
/**
* Raw data getter
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @returns {*} cache data
* Raw data getter, should return anything that is compatible with the system, or undefined
* if the system can handle it.
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @returns {OpenSeadragon.Promise<?>} cache data
* @deprecated
*/
getTileCacheData: function(cacheObject) {
return cacheObject._data;
$.console.error("[TileSource.getTileCacheData] has been deprecated. Use cache API of a tile instead.");
return cacheObject.getDataAs(undefined, false);
},
/**
@ -880,11 +946,14 @@ $.TileSource.prototype = {
* - plugins might need image representation of the data
* - div HTML rendering relies on image element presence
* 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
* @deprecated
*/
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.getImage();
},
/**
@ -892,21 +961,13 @@ $.TileSource.prototype = {
* - most heavily used rendering method is a canvas-based approach,
* 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.
* @param {object} cacheObject context cache object
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @returns {CanvasRenderingContext2D} context of the canvas representation of the cache data
* @deprecated
*/
getTileCacheDataAsContext2D: function(cacheObject) {
if (!cacheObject._renderedContext) {
var canvas = document.createElement( 'canvas' );
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;
$.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead.");
return cacheObject.getRenderedContext();
}
};

View File

@ -131,6 +131,13 @@ $.extend( $.TmsTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead
var yTiles = this.getNumTiles( level ).y - 1;
return this.tilesUrl + level + "/" + x + "/" + (yTiles - y) + ".png";
},
/**
* Equality comparator
*/
equals: function (otherSource) {
return this.tilesUrl === otherSource.tilesUrl;
}
});

View File

@ -432,6 +432,7 @@ $.Viewer = function( options ) {
// Create the tile cache
this.tileCache = new $.TileCache({
viewer: this,
maxImageCacheCount: this.maxImageCacheCount
});
@ -761,6 +762,30 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
return this;
},
/**
* Updates data within every tile in the viewer. Should be called
* when tiles are outdated and should be re-processed. Useful mainly
* for plugins that change tile data.
* @function
* @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
* @fires OpenSeadragon.Viewer.event:tile-invalidated
* @return {OpenSeadragon.Promise<?>}
*/
requestInvalidate: function (restoreTiles = true) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return $.Promise.resolve();
}
const tStamp = $.now();
const worldPromise = this.world.requestInvalidate(restoreTiles, tStamp);
if (!this.navigator) {
return worldPromise;
}
const navigatorPromise = this.navigator.world.requestInvalidate(restoreTiles, tStamp);
return $.Promise.all([worldPromise, navigatorPromise]);
},
/**
* @function
@ -787,6 +812,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
THIS[ this.hash ].animating = false;
this.world.removeAll();
this.tileCache.clear();
this.imageLoader.clear();
/**
@ -1004,7 +1030,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* @returns {Boolean}
*/
isMouseNavEnabled: function () {
return this.innerTracker.isTracking();
return this.innerTracker.tracking;
},
/**
@ -1110,7 +1136,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
ajaxHeaders = {};
}
if (!$.isPlainObject(ajaxHeaders)) {
console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
$.console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return;
}
if (propagate === undefined) {
@ -1545,7 +1571,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas.
* @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
* its top left corner in degrees.
* @param {Boolean} [options.flipped=false] Whether to horizontally flip the image.
@ -1683,11 +1711,15 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
_this._loadQueue.splice(0, 1);
if (queueItem.options.replace) {
var newIndex = _this.world.getIndexOfItem(queueItem.options.replaceItem);
const replaced = queueItem.options.replaceItem;
const newIndex = _this.world.getIndexOfItem(replaced);
if (newIndex !== -1) {
queueItem.options.index = newIndex;
}
_this.world.removeItem(queueItem.options.replaceItem);
if (!replaced._zombieCache && replaced.source.equals(queueItem.tileSource)) {
replaced.allowZombieCache(true);
}
_this.world.removeItem(replaced);
}
tiledImage = new $.TiledImage({
@ -1727,7 +1759,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
loadTilesWithAjax: queueItem.options.loadTilesWithAjax,
ajaxHeaders: queueItem.options.ajaxHeaders,
debugMode: _this.debugMode,
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency,
callTileLoadedWithCachedData: _this.callTileLoadedWithCachedData,
});
if (_this.collectionMode) {
@ -2697,7 +2730,7 @@ function getTileSourceImplementation( viewer, tileSource, imgOptions, successCal
waitUntilReady(new $TileSource(options), tileSource);
}
} else {
//can assume it's already a tile source implementation
//can assume it's already a tile source implementation, force inheritance
waitUntilReady(tileSource, tileSource);
}
});

View File

@ -40,23 +40,23 @@
/**
* @class OpenSeadragon.WebGLDrawer
* @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer
* loads tile data as textures to the graphics card as soon as it is available (via the tile-ready event),
* and unloads the data (via the image-unloaded event). The drawer utilizes a context-dependent two pass drawing pipeline.
* For the first pass, tile composition for a given TiledImage is always done using a canvas with a WebGL context.
* This allows tiles to be stitched together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
* overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output canvas
* with a Context2d context. This allows applications to have access to pixel data and other functionality provided by
* Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options, including compositeOperation,
* clip, croppingPolygons, and debugMode are implemented using Context2d operations; in these scenarios, each TiledImage is
* drawn onto the output canvas immediately after the tile composition step (pass 1). Otherwise, for efficiency, all TiledImages
* are copied over to the output canvas at once, after all tiles have been composited for all images.
* defines its own data type that ensures textures are correctly loaded to and deleted from the GPU memory.
* The drawer utilizes a context-dependent two pass drawing pipeline. For the first pass, tile composition
* for a given TiledImage is always done using a canvas with a WebGL context. This allows tiles to be stitched
* together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
* overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output
* canvas with a Context2d context. This allows applications to have access to pixel data and other functionality
* provided by Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options,
* including compositeOperation, clip, croppingPolygons, and debugMode are implemented using Context2d operations;
* in these scenarios, each TiledImage is drawn onto the output canvas immediately after the tile composition step
* (pass 1). Otherwise, for efficiency, all TiledImages are copied over to the output canvas at once, after all
* tiles have been composited for all images.
* @param {Object} options - Options for this Drawer.
* @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer.
* @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport.
* @param {Element} options.element - Parent element.
* @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
*/
OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
super(options);
@ -76,15 +76,11 @@
// private members
this._destroyed = false;
this._TextureMap = new Map();
this._TileMap = new Map();
this._gl = null;
this._firstPass = null;
this._secondPass = null;
this._glFrameBuffer = null;
this._renderToTexture = null;
this._glFramebufferToCanvasTransform = null;
this._outputCanvas = null;
this._outputContext = null;
this._clippingCanvas = null;
@ -94,12 +90,6 @@
this._imageSmoothingEnabled = true; // will be updated by setImageSmoothingEnabled
// Add listeners for events that require modifying the scene or camera
this._boundToTileReady = ev => this._tileReadyHandler(ev);
this._boundToImageUnloaded = ev => this._imageUnloadedHandler(ev);
this.viewer.addHandler("tile-ready", this._boundToTileReady);
this.viewer.addHandler("image-unloaded", this._boundToImageUnloaded);
// Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire
this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event");
this.viewer.rejectEventHandler("tile-drawing", "The WebGLDrawer does not raise the tile-drawing event");
@ -110,9 +100,23 @@
this._setupCanvases();
this._setupRenderer();
this.context = this._outputContext; // API required by tests
this._supportedFormats = this._setupTextureHandlers();
this._requiredFormats = this._supportedFormats;
this._setupCallCount = 1;
}
this.context = this._outputContext; // API required by tests
}
get defaultOptions() {
return {
// use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref)
usePrivateCache: true
};
}
getSupportedDataFormats() {
return this._supportedFormats;
}
// Public API required by all Drawer implementations
/**
@ -137,8 +141,6 @@
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
this._unloadTextures();
// Delete all our created resources
gl.deleteBuffer(this._secondPass.bufferOutputPosition);
gl.deleteFramebuffer(this._glFrameBuffer);
@ -156,11 +158,6 @@
ext.loseContext();
}
// unbind our event listeners from the viewer
this.viewer.removeHandler("tile-ready", this._boundToTileReady);
this.viewer.removeHandler("image-unloaded", this._boundToImageUnloaded);
this.viewer.removeHandler("resize", this._resizeHandler);
// set our webgl context reference to null to enable garbage collection
this._gl = null;
@ -204,7 +201,7 @@
/**
*
* @returns 'webgl'
* @returns {string} 'webgl'
*/
getType(){
return 'webgl';
@ -243,6 +240,17 @@
if(!this._backupCanvasDrawer){
this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
this._backupCanvasDrawer.getSupportedDataFormats = () => this._supportedFormats;
this._backupCanvasDrawer.getDataToDraw = (tile) => {
const cache = tile.getCache(tile.cacheKey);
if (!cache) {
$.console.warn("Attempt to draw tile %s when not cached!", tile);
return undefined;
}
const dataCache = cache.getDataForRendering(this, tile);
// Use CPU Data for the drawer instead
return dataCache && dataCache.cpuData;
};
}
return this._backupCanvasDrawer;
@ -379,23 +387,14 @@
let tile = tilesToDraw[tileIndex].tile;
let indexInDrawArray = tileIndex % maxTextures;
let numTilesToDraw = indexInDrawArray + 1;
let tileContext = tile.getCanvasContext();
const textureInfo = this.getDataToDraw(tile);
let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
if(!textureInfo){
// tile was not processed in the tile-ready event (this can happen
// if this drawer was created after the tile was downloaded)
this._tileReadyHandler({tile: tile, tiledImage: tiledImage});
// retry getting textureInfo
textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
}
if(textureInfo){
if (textureInfo) {
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
} else {
// console.log('No tile info', tile);
}
if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){
// We've filled up the buffers: time to draw this set of tiles
@ -473,8 +472,6 @@
}
}
});
if(renderingBufferHasImageData){
@ -483,6 +480,10 @@
}
getRequiredDataFormats() {
return this._requiredFormats;
}
// Public API required by all Drawer implementations
/**
* Sets whether image smoothing is enabled or disabled
@ -491,9 +492,15 @@
setImageSmoothingEnabled(enabled){
if( this._imageSmoothingEnabled !== enabled ){
this._imageSmoothingEnabled = enabled;
this._unloadTextures();
this.viewer.world.draw();
// Todo consider removing old type handlers if _supportedFormats had already types defined,
// and remove support for rendering old types...
const newFormats = this._setupTextureHandlers(); // re-sets the type to enforce re-initialization
this._supportedFormats.push(...newFormats);
this._requiredFormats = newFormats;
return this.viewer.requestInvalidate();
}
return $.Promise.resolve();
}
/**
@ -876,6 +883,97 @@
this.viewer.addHandler("resize", this._resizeHandler);
}
_setupTextureHandlers() {
const tex2DCompatibleLoader = (tile, data) => {
let tiledImage = tile.tiledImage;
let gl = this._gl;
let texture;
let position;
if (!tiledImage.isTainted()) {
if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){
tiledImage.setTainted(true);
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
} else {
let sourceWidthFraction, sourceHeightFraction;
if (tile.sourceBounds) {
sourceWidthFraction = Math.min(tile.sourceBounds.width, data.width) / data.width;
sourceHeightFraction = Math.min(tile.sourceBounds.height, data.height) / data.height;
} else {
sourceWidthFraction = 1;
sourceHeightFraction = 1;
}
// create a gl Texture for this tile and bind the canvas with the image data
texture = gl.createTexture();
let overlap = tiledImage.source.tileOverlap;
if( overlap > 0){
// calculate the normalized position of the rect to actually draw
// discarding overlap.
let overlapFraction = this._calculateOverlapFraction(tile, tiledImage);
let left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction;
let top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction;
let right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction;
let bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction;
position = this._makeQuadVertexBuffer(left, right, top, bottom);
} else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) {
// no overlap and no padding: this texture can use the unit quad as its position data
position = this._unitQuad;
} else {
position = this._makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction);
}
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter());
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._textureFilter());
try {
// This depends on gl.TEXTURE_2D being bound to the texture
// associated with this canvas before calling this function
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
} catch (e){
// Todo a bit dirty re-use of the tainted flag, but makes the code more stable
tiledImage.setTainted(true);
$.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e);
this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.');
}
}
}
// TextureInfo stored in the cache
return {
texture: texture,
position: position,
cpuData: data // Reference to the outer cache data, used to draw if webgl canont be used
};
};
const tex2DCompatibleDestructor = textureInfo => {
if (textureInfo) {
this._gl.deleteTexture(textureInfo.texture);
}
};
const thisType = `${this.getId()}_${this._setupCallCount++}_TEX_2D`;
// Differentiate type also based on type used to upload data: we can support bidirectional conversion.
const c2dTexType = thisType + ":context2d",
imageTexType = thisType + ":image";
// We should be OK uploading any of these types. The complexity is selected to be O(3n), should be
// more than linear pass over pixels
$.convertor.learn("context2d", c2dTexType, (t, d) => tex2DCompatibleLoader(t, d.canvas), 1, 3);
$.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3);
$.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor);
$.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor);
return [c2dTexType, imageTexType];
}
// private
_makeQuadVertexBuffer(left, right, top, bottom){
return new Float32Array([
@ -887,92 +985,6 @@
right, top]);
}
// private
_tileReadyHandler(event){
let tile = event.tile;
let tiledImage = event.tiledImage;
// If a tiledImage is already known to be tainted, don't try to upload any
// textures to webgl, because they won't be used even if it succeeds
if(tiledImage.isTainted()){
return;
}
let tileContext = tile.getCanvasContext();
let canvas = tileContext && tileContext.canvas;
// if the tile doesn't provide a canvas, or is tainted by cross-origin
// data, marked the TiledImage as tainted so the canvas drawer can be
// used instead, and return immediately - tainted data cannot be uploaded to webgl
if(!canvas || $.isCanvasTainted(canvas)){
const wasTainted = tiledImage.isTainted();
if(!wasTainted){
tiledImage.setTainted(true);
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
}
return;
}
let textureInfo = this._TextureMap.get(canvas);
// if this is a new image for us, create a texture
if(!textureInfo){
let gl = this._gl;
// create a gl Texture for this tile and bind the canvas with the image data
let texture = gl.createTexture();
let position;
let overlap = tiledImage.source.tileOverlap;
// deal with tiles where there is padding, i.e. the pixel data doesn't take up the entire provided canvas
let sourceWidthFraction, sourceHeightFraction;
if (tile.sourceBounds) {
sourceWidthFraction = Math.min(tile.sourceBounds.width, canvas.width) / canvas.width;
sourceHeightFraction = Math.min(tile.sourceBounds.height, canvas.height) / canvas.height;
} else {
sourceWidthFraction = 1;
sourceHeightFraction = 1;
}
if( overlap > 0){
// calculate the normalized position of the rect to actually draw
// discarding overlap.
let overlapFraction = this._calculateOverlapFraction(tile, tiledImage);
let left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction;
let top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction;
let right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction;
let bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction;
position = this._makeQuadVertexBuffer(left, right, top, bottom);
} else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) {
// no overlap and no padding: this texture can use the unit quad as its position data
position = this._unitQuad;
} else {
position = this._makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction);
}
let textureInfo = {
texture: texture,
position: position,
};
// add it to our _TextureMap
this._TextureMap.set(canvas, textureInfo);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter());
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._textureFilter());
// Upload the image into the texture.
this._uploadImageData(tileContext);
}
}
// private
_calculateOverlapFraction(tile, tiledImage){
let overlap = tiledImage.source.tileOverlap;
@ -989,51 +1001,13 @@
}
// private
_unloadTextures(){
let canvases = Array.from(this._TextureMap.keys());
canvases.forEach(canvas => {
this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap
});
}
// _unloadTextures(){
// let canvases = Array.from(this._TextureMap.keys());
// canvases.forEach(canvas => {
// this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap
// });
// }
// private
_uploadImageData(tileContext){
let gl = this._gl;
let canvas = tileContext.canvas;
try{
if(!canvas){
throw('Tile context does not have a canvas', tileContext);
}
// This depends on gl.TEXTURE_2D being bound to the texture
// associated with this canvas before calling this function
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
} catch (e){
$.console.error('Error uploading image data to WebGL', e);
}
}
// private
_imageUnloadedHandler(event){
let canvas = event.context2D.canvas;
this._cleanupImageData(canvas);
}
// private
_cleanupImageData(tileCanvas){
let textureInfo = this._TextureMap.get(tileCanvas);
//remove from the map
this._TextureMap.delete(tileCanvas);
//release the texture from the GPU
if(textureInfo){
this._gl.deleteTexture(textureInfo.texture);
}
}
// private
_setClip(){
// no-op: called by _renderToClippingCanvas when tiledImage._clip is truthy
// so that tests will pass.
@ -1340,9 +1314,7 @@
return shaderProgram;
}
};
}( OpenSeadragon ));

View File

@ -69,6 +69,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
/**
* Add the specified item.
* @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.
* @fires OpenSeadragon.World.event:add-item
* @fires OpenSeadragon.World.event:metrics-change
@ -231,6 +232,209 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
}
},
/**
* Forces the system consider all tiles across all tiled images
* as outdated, and fire tile update event on relevant tiles
* Detailed description is available within the 'tile-invalidated'
* event.
* @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
* @param {number} [tStamp=OpenSeadragon.now()] optionally provide tStamp of the update event
* @function
* @fires OpenSeadragon.Viewer.event:tile-invalidated
* @return {OpenSeadragon.Promise<?>}
*/
requestInvalidate: function (restoreTiles = true, tStamp = $.now()) {
$.__updated = tStamp;
// const priorityTiles = this._items.map(item => item._lastDrawn.map(x => x.tile)).flat();
// const promise = this.requestTileInvalidateEvent(priorityTiles, tStamp, restoreTiles);
// return promise.then(() => this.requestTileInvalidateEvent(this.viewer.tileCache.getLoadedTilesFor(null), tStamp, restoreTiles));
// Tile-first retrieval fires computation on tiles that share cache, which are filtered out by processing property
return this.requestTileInvalidateEvent(this.viewer.tileCache.getLoadedTilesFor(null), tStamp, restoreTiles);
// Cache-first update tile retrieval is nicer since there might be many tiles sharing
// return this.requestTileInvalidateEvent(new Set(Object.values(this.viewer.tileCache._cachesLoaded)
// .map(c => !c._destroyed && c._tiles[0])), tStamp, restoreTiles);
},
/**
* Requests tile data update.
* @function OpenSeadragon.Viewer.prototype._updateSequenceButtons
* @private
* @param {Iterable<OpenSeadragon.Tile>} tilesToProcess tiles to update
* @param {Number} tStamp timestamp in milliseconds, if active timestamp of the same value is executing,
* changes are added to the cycle, else they await next iteration
* @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
* @param {Boolean} [_allowTileUnloaded=false] internal flag for calling on tiles that come new to the system
* @fires OpenSeadragon.Viewer.event:tile-invalidated
* @return {OpenSeadragon.Promise<?>}
*/
requestTileInvalidateEvent: function(tilesToProcess, tStamp, restoreTiles = true, _allowTileUnloaded = false) {
if (!this.viewer.isOpen()) {
return $.Promise.resolve();
}
const tileList = [],
tileFinishResolvers = [];
for (const tile of tilesToProcess) {
// We allow re-execution on tiles that are in process but have too low processing timestamp,
// which must be solved by ensuring subsequent data calls in the suddenly outdated processing
// pipeline take no effect.
if (!tile || (!_allowTileUnloaded && !tile.loaded)) {
continue;
}
const tileCache = tile.getCache(tile.originalCacheKey);
if (tileCache.__invStamp && tileCache.__invStamp >= tStamp) {
continue;
}
for (let t of tileCache._tiles) {
// Mark all related tiles as processing and register callback to unmark later on
t.processing = tStamp;
t.processingPromise = new $.Promise((resolve) => {
tileFinishResolvers.push(() => {
t.processing = false;
resolve(t);
});
});
}
tileCache.__invStamp = tStamp;
tileList.push(tile);
}
if (!tileList.length) {
return $.Promise.resolve();
}
// We call the event on the parent viewer window no matter what
const eventTarget = this.viewer.viewer || this.viewer;
// However, we must pick the correct drawer reference (navigator VS viewer)
const supportedFormats = this.viewer.drawer.getRequiredDataFormats();
const keepInternalCacheCopy = this.viewer.drawer.options.usePrivateCache;
const drawerId = this.viewer.drawer.getId();
const jobList = tileList.map(tile => {
const tiledImage = tile.tiledImage;
const originalCache = tile.getCache(tile.originalCacheKey);
let workingCache = null;
const getWorkingCacheData = (type) => {
if (workingCache) {
return workingCache.getDataAs(type, false);
}
const targetCopyKey = restoreTiles ? tile.originalCacheKey : tile.cacheKey;
const origCache = tile.getCache(targetCopyKey);
if (!origCache) {
$.console.error("[Tile::getData] There is no cache available for tile with key %s", targetCopyKey);
return $.Promise.reject();
}
// Here ensure type is defined, rquired by data callbacks
type = type || origCache.type;
workingCache = new $.CacheRecord().withTileReference(tile);
return origCache.getDataAs(type, true).then(data => {
workingCache.addTile(tile, data, type);
return workingCache.data;
});
};
const setWorkingCacheData = (value, type) => {
if (!workingCache) {
workingCache = new $.CacheRecord().withTileReference(tile);
workingCache.addTile(tile, value, type);
return $.Promise.resolve();
}
return workingCache.setDataAs(value, type);
};
const atomicCacheSwap = () => {
if (workingCache) {
let newCacheKey = tile.buildDistinctMainCacheKey();
tiledImage._tileCache.injectCache({
tile: tile,
cache: workingCache,
targetKey: newCacheKey,
setAsMainCache: true,
tileAllowNotLoaded: tile.loading
});
} else if (restoreTiles) {
// If we requested restore, perform now
tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true);
}
};
/**
* @event tile-invalidated
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
* @property {OpenSeadragon.Tile} tile
* @property {AsyncNullaryFunction<boolean>} outdated - predicate that evaluates to true if the event
* is outdated and should not be longer processed (has no effect)
* @property {AsyncUnaryFunction<any, string>} getData - get data of desired type (string argument)
* @property {AsyncBinaryFunction<undefined, any, string>} setData - set data (any)
* and the type of the data (string)
* @property {function} resetData - function that deletes any previous data modification in the current
* execution pipeline
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
return eventTarget.raiseEventAwaiting('tile-invalidated', {
tile: tile,
tiledImage: tiledImage,
outdated: () => originalCache.__invStamp !== tStamp || (!tile.loaded && !tile.loading),
getData: getWorkingCacheData,
setData: setWorkingCacheData,
resetData: () => {
if (workingCache) {
workingCache.destroy();
workingCache = null;
}
}
}).then(_ => {
if (originalCache.__invStamp === tStamp && (tile.loaded || tile.loading)) {
if (workingCache) {
return workingCache.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(c => {
if (c && originalCache.__invStamp === tStamp) {
atomicCacheSwap();
originalCache.__invStamp = null;
}
});
}
// If we requested restore, perform now
if (restoreTiles) {
const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey);
return freshOriginalCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then((c) => {
if (c && originalCache.__invStamp === tStamp) {
atomicCacheSwap();
originalCache.__invStamp = null;
}
});
}
// Preventive call to ensure we stay compatible
const freshMainCacheRef = tile.getCache();
return freshMainCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(() => {
atomicCacheSwap();
originalCache.__invStamp = null;
});
} else if (workingCache) {
workingCache.destroy();
workingCache = null;
}
return null;
}).catch(e => {
$.console.error("Update routine error:", e);
});
});
return $.Promise.all(jobList).then(() => {
for (let resolve of tileFinishResolvers) {
resolve();
}
this.draw();
});
},
/**
* Clears all tiles and triggers updates for all items.
*/
@ -261,9 +465,9 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
draw: function() {
this.viewer.drawer.draw(this._items);
this._needsDraw = false;
this._items.forEach((item) => {
for (let item of this._items) {
this._needsDraw = item.setDrawn() || this._needsDraw;
});
}
},
/**

View File

@ -77,7 +77,7 @@
options.minLevel = 0;
options.maxLevel = options.gridSize.length - 1;
OpenSeadragon.TileSource.apply(this, [options]);
$.TileSource.apply(this, [options]);
};
$.extend($.ZoomifyTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ZoomifyTileSource.prototype */ {
@ -143,6 +143,13 @@
result = Math.floor(num / 256);
return this.tilesUrl + 'TileGroup' + result + '/' + level + '-' + x + '-' + y + '.' + this.fileFormat;
},
/**
* Equality comparator
*/
equals: function (otherSource) {
return this.tilesUrl === otherSource.tilesUrl;
}
});

300
style.css Normal file
View File

@ -0,0 +1,300 @@
* {
margin: 0;
padding: 0;
outline: 0;
}
body {
padding: 80px 100px;
font: 13px "Helvetica Neue", "Lucida Grande", "Arial";
background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9));
background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9);
background-repeat: no-repeat;
color: #555;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 {
font-size: 22px;
color: #343434;
}
h1 em, h2 em {
padding: 0 5px;
font-weight: normal;
}
h1 {
font-size: 60px;
}
h2 {
margin-top: 10px;
}
h3 {
margin: 5px 0 10px 0;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
font-size: 18px;
}
ul li {
list-style: none;
}
ul li:hover {
cursor: pointer;
color: #2e2e2e;
}
ul li .path {
padding-left: 5px;
font-weight: bold;
}
ul li .line {
padding-right: 5px;
font-style: italic;
}
ul li:first-child .path {
padding-left: 0;
}
p {
line-height: 1.5;
}
a {
color: #555;
text-decoration: none;
}
a:hover {
color: #303030;
}
#stacktrace {
margin-top: 15px;
}
.directory h1 {
margin-bottom: 15px;
font-size: 18px;
}
ul#files {
width: 100%;
height: 100%;
overflow: hidden;
}
ul#files li {
float: left;
width: 30%;
line-height: 25px;
margin: 1px;
}
ul#files li a {
display: block;
height: 25px;
border: 1px solid transparent;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
overflow: hidden;
white-space: nowrap;
}
ul#files li a:focus,
ul#files li a:hover {
background: rgba(255,255,255,0.65);
border: 1px solid #ececec;
}
ul#files li a.highlight {
-webkit-transition: background .4s ease-in-out;
background: #ffff4f;
border-color: #E9DC51;
}
#search {
display: block;
position: fixed;
top: 20px;
right: 20px;
width: 90px;
-webkit-transition: width ease 0.2s, opacity ease 0.4s;
-moz-transition: width ease 0.2s, opacity ease 0.4s;
-webkit-border-radius: 32px;
-moz-border-radius: 32px;
-webkit-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
-moz-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
-webkit-font-smoothing: antialiased;
text-align: left;
font: 13px "Helvetica Neue", Arial, sans-serif;
padding: 4px 10px;
border: none;
background: transparent;
margin-bottom: 0;
outline: none;
opacity: 0.7;
color: #888;
}
#search:focus {
width: 120px;
opacity: 1.0;
}
/*views*/
#files span {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
text-indent: 10px;
}
#files .name {
background-repeat: no-repeat;
}
#files .icon .name {
text-indent: 28px;
}
/*tiles*/
.view-tiles .name {
width: 100%;
background-position: 8px 5px;
margin-left: 30px;
}
.view-tiles .size,
.view-tiles .date {
display: none;
}
.view-tiles a {
position: relative;
}
/*hack: reuse empty to find folders*/
#files .size:empty {
width: 20px;
height: 14px;
background-color: #f9d342; /* Folder color */
position: absolute;
border-radius: 4px;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); /* Optional shadow for effect */
display: block !important;
float: left;
left: 13px;
top: 5px;
}
#files .size:empty:before {
content: '';
position: absolute;
top: -2px;
left: 2px;
width: 12px;
height: 4px;
background-color: #f9d342;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
#files .size:empty:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 8px;
height: 4px;
background-color: #e8c233; /* Slightly darker shade for the tab */
border-top-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/*details*/
ul#files.view-details li {
float: none;
display: block;
width: 90%;
}
ul#files.view-details li.header {
height: 25px;
background: #000;
color: #fff;
font-weight: bold;
}
.view-details .header {
border-radius: 5px;
}
.view-details .name {
width: 60%;
background-position: 8px 5px;
}
.view-details .size {
width: 10%;
}
.view-details .date {
width: 30%;
}
.view-details .size,
.view-details .date {
text-align: right;
direction: rtl;
}
/*mobile*/
@media (max-width: 768px) {
body {
font-size: 13px;
line-height: 16px;
padding: 0;
}
#search {
position: static;
width: 100%;
font-size: 2em;
line-height: 1.8em;
text-indent: 10px;
border: 0;
border-radius: 0;
padding: 10px 0;
margin: 0;
}
#search:focus {
width: 100%;
border: 0;
opacity: 1;
}
.directory h1 {
font-size: 2em;
line-height: 1.5em;
color: #fff;
background: #000;
padding: 15px 10px;
margin: 0;
}
ul#files {
border-top: 1px solid #cacaca;
}
ul#files li {
float: none;
width: auto !important;
display: block;
border-bottom: 1px solid #cacaca;
font-size: 2em;
line-height: 1.2em;
text-indent: 0;
margin: 0;
}
ul#files li:nth-child(odd) {
background: #e0e0e0;
}
ul#files li a {
height: auto;
border: 0;
border-radius: 0;
padding: 15px 10px;
}
ul#files li a:focus,
ul#files li a:hover {
border: 0;
}
#files .header,
#files .size,
#files .date {
display: none !important;
}
#files .name {
float: none;
display: inline-block;
width: 100%;
text-indent: 0;
background-position: 0 50%;
}
#files .icon .name {
text-indent: 41px;
}
#files .size:empty {
top: 23px;
left: 5px;
}
}

View File

@ -58,6 +58,7 @@
<!-- Helpers -->
<script src="/test/helpers/legacy.mouse.shim.js"></script>
<script src="/test/helpers/mocks.js"></script>
<script src="/test/helpers/test.js"></script>
<script src="/test/helpers/touch.js"></script>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>OpenSeadragon Basic Demo</title>
<script type="text/javascript" src='../../build/openseadragon/openseadragon.js'></script>
<script type="text/javascript" src='../lib/jquery-1.9.1.min.js'></script>
<style type="text/css">
.openseadragon1 {
width: 800px;
height: 600px;
}
</style>
</head>
<body>
<div>
Simple demo page to show a default OpenSeadragon viewer.
</div>
<div id="contentDiv" class="openseadragon1"></div>
<script type="text/javascript">
var viewer = OpenSeadragon({
// debugMode: true,
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
tileSources: "../data/testpattern.dzi",
drawer: 'html',
showNavigator: true,
});
</script>
</body>
</html>

View File

@ -41,8 +41,9 @@
constrainDuringPan: true,
visibilityRatio: 1,
prefixUrl: "../../build/openseadragon/images/",
minZoomImageRatio: 1
minZoomImageRatio: 1,
crossOriginPolicy: 'Anonymous',
});
</script>
</body>
</html>
</html>

View File

@ -0,0 +1,843 @@
/*
* Modified and maintained by the OpenSeadragon Community.
*
* This software was orignally developed at the National Institute of Standards and
* Technology by employees of the Federal Government. NIST assumes
* no responsibility whatsoever for its use by other parties, and makes no
* guarantees, expressed or implied, about its quality, reliability, or
* any other characteristic.
* @author Antoine Vandecreme <antoine.vandecreme@nist.gov>
*/
/**
* This class is an improvement over the basic jQuery spinner to support
* 'Enter' to update the value (with validity checks).
* @param {Object} options Options object
* @return {Spinner} A spinner object
*/
class Spinner {
constructor(options) {
options.$element.html('<input type="text" size="1" ' +
'class="ui-widget-content ui-corner-all"/>');
const self = this,
$spinner = options.$element.find('input');
this.value = options.init;
$spinner.spinner({
min: options.min,
max: options.max,
step: options.step,
spin: function(event, ui) {
/*jshint unused:true */
self.value = ui.value;
options.updateCallback(self.value);
}
});
$spinner.val(this.value);
$spinner.keyup(function(e) {
if (e.which === 13) {
if (!this.value.match(/^-?\d?\.?\d*$/)) {
this.value = options.init;
} else if (options.min !== undefined &&
this.value < options.min) {
this.value = options.min;
} else if (options.max !== undefined &&
this.value > options.max) {
this.value = options.max;
}
self.value = this.value;
options.updateCallback(self.value);
}
});
}
getValue() {
return this.value;
}
}
class SpinnerSlider {
constructor(options) {
let idIncrement = 0;
this.hash = idIncrement++;
const spinnerId = 'wdzt-spinner-slider-spinner-' + this.hash;
const sliderId = 'wdzt-spinner-slider-slider-' + this.hash;
this.value = options.init;
const self = this;
options.$element.html(`
<div class="wdzt-table-layout wdzt-full-width">
<div class="wdzt-row-layout">
<div class="wdzt-cell-layout">
<input id="${spinnerId}" type="text" size="1"
class="ui-widget-content ui-corner-all"/>
</div>
<div class="wdzt-cell-layout wdzt-full-width">
<div id="${sliderId}" class="wdzt-menu-slider">
</div>
</div>
</div>
</div>
`);
const $slider = options.$element.find('#' + sliderId)
.slider({
min: options.min,
max: options.sliderMax !== undefined ?
options.sliderMax : options.max,
step: options.step,
value: this.value,
slide: function (event, ui) {
/*jshint unused:true */
self.value = ui.value;
$spinner.spinner('value', self.value);
options.updateCallback(self.value);
}
});
const $spinner = options.$element.find('#' + spinnerId)
.spinner({
min: options.min,
max: options.max,
step: options.step,
spin: function (event, ui) {
/*jshint unused:true */
self.value = ui.value;
$slider.slider('value', self.value);
options.updateCallback(self.value);
}
});
$spinner.val(this.value);
$spinner.keyup(function (e) {
if (e.which === 13) {
self.value = $spinner.spinner('value');
$slider.slider('value', self.value);
options.updateCallback(self.value);
}
});
}
getValue () {
return this.value;
};
}
const switcher = new DrawerSwitcher();
switcher.addDrawerOption("drawer");
$("#title-drawer").html(switcher.activeName("drawer"));
switcher.render("#title-banner");
const sources = {
'Highsmith': "https://openseadragon.github.io/example-images/highsmith/highsmith.dzi",
'Rainbow Grid': "../../data/testpattern.dzi",
'Leaves': "../../data/iiif_2_0_sizes/info.json",
"Duomo":"https://openseadragon.github.io/example-images/duomo/duomo.dzi",
}
const url = new URL(window.location);
const targetSource = url.searchParams.get("image") || Object.values(sources)[0];
const viewer = window.viewer = new OpenSeadragon({
id: 'openseadragon',
prefixUrl: '/build/openseadragon/images/',
tileSources: targetSource,
crossOriginPolicy: 'Anonymous',
drawer: switcher.activeImplementation("drawer"),
showNavigator: true,
wrapHorizontal: true,
gestureSettingsMouse: {
clickToZoom: false
}
});
$("#image-select")
.html(Object.entries(sources).map(([k, v]) =>
`<option value="${v}" ${targetSource === v ? "selected" : ""}>${k}</option>`).join("\n"))
.on('change', e => {
url.searchParams.set('image', e.target.value);
window.history.pushState(null, '', url.toString());
viewer.addTiledImage({tileSource: e.target.value, index: 0, replace: true});
});
// Prevent Caman from caching the canvas because without this:
// 1. We have a memory leak
// 2. Non-caman filters in between 2 camans filters get ignored.
Caman.Store.put = function() {};
// List of filters with their templates.
const availableFilters = [
{
name: 'Invert',
generate: function() {
return {
html: '',
getParams: function() {
return '';
},
getFilter: function() {
/*eslint new-cap: 0*/
return OpenSeadragon.Filters.INVERT();
}
};
}
}, {
name: 'Colormap',
generate: function(updateCallback) {
const cmaps = {
aCm: [ [0,0,0], [0,4,0], [0,8,0], [0,12,0], [0,16,0], [0,20,0], [0,24,0], [0,28,0], [0,32,0], [0,36,0], [0,40,0], [0,44,0], [0,48,0], [0,52,0], [0,56,0], [0,60,0], [0,64,0], [0,68,0], [0,72,0], [0,76,0], [0,80,0], [0,85,0], [0,89,0], [0,93,0], [0,97,0], [0,101,0], [0,105,0], [0,109,0], [0,113,0], [0,117,0], [0,121,0], [0,125,0], [0,129,2], [0,133,5], [0,137,7], [0,141,10], [0,145,13], [0,149,15], [0,153,18], [0,157,21], [0,161,23], [0,165,26], [0,170,29], [0,174,31], [0,178,34], [0,182,37], [0,186,39], [0,190,42], [0,194,45], [0,198,47], [0,202,50], [0,206,53], [0,210,55], [0,214,58], [0,218,61], [0,222,63], [0,226,66], [0,230,69], [0,234,71], [0,238,74], [0,242,77], [0,246,79], [0,250,82], [0,255,85], [3,251,87], [7,247,90], [11,243,92], [15,239,95], [19,235,98], [23,231,100], [27,227,103], [31,223,106], [35,219,108], [39,215,111], [43,211,114], [47,207,116], [51,203,119], [55,199,122], [59,195,124], [63,191,127], [67,187,130], [71,183,132], [75,179,135], [79,175,138], [83,171,140], [87,167,143], [91,163,146], [95,159,148], [99,155,151], [103,151,154], [107,147,156], [111,143,159], [115,139,162], [119,135,164], [123,131,167], [127,127,170], [131,123,172], [135,119,175], [139,115,177], [143,111,180], [147,107,183], [151,103,185], [155,99,188], [159,95,191], [163,91,193], [167,87,196], [171,83,199], [175,79,201], [179,75,204], [183,71,207], [187,67,209], [191,63,212], [195,59,215], [199,55,217], [203,51,220], [207,47,223], [211,43,225], [215,39,228], [219,35,231], [223,31,233], [227,27,236], [231,23,239], [235,19,241], [239,15,244], [243,11,247], [247,7,249], [251,3,252], [255,0,255], [255,0,251], [255,0,247], [255,0,244], [255,0,240], [255,0,237], [255,0,233], [255,0,230], [255,0,226], [255,0,223], [255,0,219], [255,0,216], [255,0,212], [255,0,208], [255,0,205], [255,0,201], [255,0,198], [255,0,194], [255,0,191], [255,0,187], [255,0,184], [255,0,180], [255,0,177], [255,0,173], [255,0,170], [255,0,166], [255,0,162], [255,0,159], [255,0,155], [255,0,152], [255,0,148], [255,0,145], [255,0,141], [255,0,138], [255,0,134], [255,0,131], [255,0,127], [255,0,123], [255,0,119], [255,0,115], [255,0,112], [255,0,108], [255,0,104], [255,0,100], [255,0,96], [255,0,92], [255,0,88], [255,0,85], [255,0,81], [255,0,77], [255,0,73], [255,0,69], [255,0,65], [255,0,61], [255,0,57], [255,0,54], [255,0,50], [255,0,46], [255,0,42], [255,0,38], [255,0,34], [255,0,30], [255,0,27], [255,0,23], [255,0,19], [255,0,15], [255,0,11], [255,0,7], [255,0,3], [255,0,0], [255,4,0], [255,8,0], [255,12,0], [255,17,0], [255,21,0], [255,25,0], [255,30,0], [255,34,0], [255,38,0], [255,43,0], [255,47,0], [255,51,0], [255,56,0], [255,60,0], [255,64,0], [255,69,0], [255,73,0], [255,77,0], [255,82,0], [255,86,0], [255,90,0], [255,95,0], [255,99,0], [255,103,0], [255,108,0], [255,112,0], [255,116,0], [255,121,0], [255,125,0], [255,129,0], [255,133,0], [255,138,0], [255,142,0], [255,146,0], [255,151,0], [255,155,0], [255,159,0], [255,164,0], [255,168,0], [255,172,0], [255,177,0], [255,181,0], [255,185,0], [255,190,0], [255,194,0], [255,198,0], [255,203,0], [255,207,0], [255,211,0], [255,216,0], [255,220,0], [255,224,0], [255,229,0], [255,233,0], [255,237,0], [255,242,0], [255,246,0], [255,250,0], [255,255,0]],
bCm: [ [0,0,0], [0,0,4], [0,0,8], [0,0,12], [0,0,16], [0,0,20], [0,0,24], [0,0,28], [0,0,32], [0,0,36], [0,0,40], [0,0,44], [0,0,48], [0,0,52], [0,0,56], [0,0,60], [0,0,64], [0,0,68], [0,0,72], [0,0,76], [0,0,80], [0,0,85], [0,0,89], [0,0,93], [0,0,97], [0,0,101], [0,0,105], [0,0,109], [0,0,113], [0,0,117], [0,0,121], [0,0,125], [0,0,129], [0,0,133], [0,0,137], [0,0,141], [0,0,145], [0,0,149], [0,0,153], [0,0,157], [0,0,161], [0,0,165], [0,0,170], [0,0,174], [0,0,178], [0,0,182], [0,0,186], [0,0,190], [0,0,194], [0,0,198], [0,0,202], [0,0,206], [0,0,210], [0,0,214], [0,0,218], [0,0,222], [0,0,226], [0,0,230], [0,0,234], [0,0,238], [0,0,242], [0,0,246], [0,0,250], [0,0,255], [3,0,251], [7,0,247], [11,0,243], [15,0,239], [19,0,235], [23,0,231], [27,0,227], [31,0,223], [35,0,219], [39,0,215], [43,0,211], [47,0,207], [51,0,203], [55,0,199], [59,0,195], [63,0,191], [67,0,187], [71,0,183], [75,0,179], [79,0,175], [83,0,171], [87,0,167], [91,0,163], [95,0,159], [99,0,155], [103,0,151], [107,0,147], [111,0,143], [115,0,139], [119,0,135], [123,0,131], [127,0,127], [131,0,123], [135,0,119], [139,0,115], [143,0,111], [147,0,107], [151,0,103], [155,0,99], [159,0,95], [163,0,91], [167,0,87], [171,0,83], [175,0,79], [179,0,75], [183,0,71], [187,0,67], [191,0,63], [195,0,59], [199,0,55], [203,0,51], [207,0,47], [211,0,43], [215,0,39], [219,0,35], [223,0,31], [227,0,27], [231,0,23], [235,0,19], [239,0,15], [243,0,11], [247,0,7], [251,0,3], [255,0,0], [255,3,0], [255,7,0], [255,11,0], [255,15,0], [255,19,0], [255,23,0], [255,27,0], [255,31,0], [255,35,0], [255,39,0], [255,43,0], [255,47,0], [255,51,0], [255,55,0], [255,59,0], [255,63,0], [255,67,0], [255,71,0], [255,75,0], [255,79,0], [255,83,0], [255,87,0], [255,91,0], [255,95,0], [255,99,0], [255,103,0], [255,107,0], [255,111,0], [255,115,0], [255,119,0], [255,123,0], [255,127,0], [255,131,0], [255,135,0], [255,139,0], [255,143,0], [255,147,0], [255,151,0], [255,155,0], [255,159,0], [255,163,0], [255,167,0], [255,171,0], [255,175,0], [255,179,0], [255,183,0], [255,187,0], [255,191,0], [255,195,0], [255,199,0], [255,203,0], [255,207,0], [255,211,0], [255,215,0], [255,219,0], [255,223,0], [255,227,0], [255,231,0], [255,235,0], [255,239,0], [255,243,0], [255,247,0], [255,251,0], [255,255,0], [255,255,3], [255,255,7], [255,255,11], [255,255,15], [255,255,19], [255,255,23], [255,255,27], [255,255,31], [255,255,35], [255,255,39], [255,255,43], [255,255,47], [255,255,51], [255,255,55], [255,255,59], [255,255,63], [255,255,67], [255,255,71], [255,255,75], [255,255,79], [255,255,83], [255,255,87], [255,255,91], [255,255,95], [255,255,99], [255,255,103], [255,255,107], [255,255,111], [255,255,115], [255,255,119], [255,255,123], [255,255,127], [255,255,131], [255,255,135], [255,255,139], [255,255,143], [255,255,147], [255,255,151], [255,255,155], [255,255,159], [255,255,163], [255,255,167], [255,255,171], [255,255,175], [255,255,179], [255,255,183], [255,255,187], [255,255,191], [255,255,195], [255,255,199], [255,255,203], [255,255,207], [255,255,211], [255,255,215], [255,255,219], [255,255,223], [255,255,227], [255,255,231], [255,255,235], [255,255,239], [255,255,243], [255,255,247], [255,255,251], [255,255,255]],
bbCm: [ [0,0,0], [2,0,0], [4,0,0], [6,0,0], [8,0,0], [10,0,0], [12,0,0], [14,0,0], [16,0,0], [18,0,0], [20,0,0], [22,0,0], [24,0,0], [26,0,0], [28,0,0], [30,0,0], [32,0,0], [34,0,0], [36,0,0], [38,0,0], [40,0,0], [42,0,0], [44,0,0], [46,0,0], [48,0,0], [50,0,0], [52,0,0], [54,0,0], [56,0,0], [58,0,0], [60,0,0], [62,0,0], [64,0,0], [66,0,0], [68,0,0], [70,0,0], [72,0,0], [74,0,0], [76,0,0], [78,0,0], [80,0,0], [82,0,0], [84,0,0], [86,0,0], [88,0,0], [90,0,0], [92,0,0], [94,0,0], [96,0,0], [98,0,0], [100,0,0], [102,0,0], [104,0,0], [106,0,0], [108,0,0], [110,0,0], [112,0,0], [114,0,0], [116,0,0], [118,0,0], [120,0,0], [122,0,0], [124,0,0], [126,0,0], [128,1,0], [130,3,0], [132,5,0], [134,7,0], [136,9,0], [138,11,0], [140,13,0], [142,15,0], [144,17,0], [146,19,0], [148,21,0], [150,23,0], [152,25,0], [154,27,0], [156,29,0], [158,31,0], [160,33,0], [162,35,0], [164,37,0], [166,39,0], [168,41,0], [170,43,0], [172,45,0], [174,47,0], [176,49,0], [178,51,0], [180,53,0], [182,55,0], [184,57,0], [186,59,0], [188,61,0], [190,63,0], [192,65,0], [194,67,0], [196,69,0], [198,71,0], [200,73,0], [202,75,0], [204,77,0], [206,79,0], [208,81,0], [210,83,0], [212,85,0], [214,87,0], [216,89,0], [218,91,0], [220,93,0], [222,95,0], [224,97,0], [226,99,0], [228,101,0], [230,103,0], [232,105,0], [234,107,0], [236,109,0], [238,111,0], [240,113,0], [242,115,0], [244,117,0], [246,119,0], [248,121,0], [250,123,0], [252,125,0], [255,127,0], [255,129,1], [255,131,3], [255,133,5], [255,135,7], [255,137,9], [255,139,11], [255,141,13], [255,143,15], [255,145,17], [255,147,19], [255,149,21], [255,151,23], [255,153,25], [255,155,27], [255,157,29], [255,159,31], [255,161,33], [255,163,35], [255,165,37], [255,167,39], [255,169,41], [255,171,43], [255,173,45], [255,175,47], [255,177,49], [255,179,51], [255,181,53], [255,183,55], [255,185,57], [255,187,59], [255,189,61], [255,191,63], [255,193,65], [255,195,67], [255,197,69], [255,199,71], [255,201,73], [255,203,75], [255,205,77], [255,207,79], [255,209,81], [255,211,83], [255,213,85], [255,215,87], [255,217,89], [255,219,91], [255,221,93], [255,223,95], [255,225,97], [255,227,99], [255,229,101], [255,231,103], [255,233,105], [255,235,107], [255,237,109], [255,239,111], [255,241,113], [255,243,115], [255,245,117], [255,247,119], [255,249,121], [255,251,123], [255,253,125], [255,255,127], [255,255,129], [255,255,131], [255,255,133], [255,255,135], [255,255,137], [255,255,139], [255,255,141], [255,255,143], [255,255,145], [255,255,147], [255,255,149], [255,255,151], [255,255,153], [255,255,155], [255,255,157], [255,255,159], [255,255,161], [255,255,163], [255,255,165], [255,255,167], [255,255,169], [255,255,171], [255,255,173], [255,255,175], [255,255,177], [255,255,179], [255,255,181], [255,255,183], [255,255,185], [255,255,187], [255,255,189], [255,255,191], [255,255,193], [255,255,195], [255,255,197], [255,255,199], [255,255,201], [255,255,203], [255,255,205], [255,255,207], [255,255,209], [255,255,211], [255,255,213], [255,255,215], [255,255,217], [255,255,219], [255,255,221], [255,255,223], [255,255,225], [255,255,227], [255,255,229], [255,255,231], [255,255,233], [255,255,235], [255,255,237], [255,255,239], [255,255,241], [255,255,243], [255,255,245], [255,255,247], [255,255,249], [255,255,251], [255,255,253], [255,255,255]],
blueCm: [ [0,0,0], [0,0,1], [0,0,2], [0,0,3], [0,0,4], [0,0,5], [0,0,6], [0,0,7], [0,0,8], [0,0,9], [0,0,10], [0,0,11], [0,0,12], [0,0,13], [0,0,14], [0,0,15], [0,0,16], [0,0,17], [0,0,18], [0,0,19], [0,0,20], [0,0,21], [0,0,22], [0,0,23], [0,0,24], [0,0,25], [0,0,26], [0,0,27], [0,0,28], [0,0,29], [0,0,30], [0,0,31], [0,0,32], [0,0,33], [0,0,34], [0,0,35], [0,0,36], [0,0,37], [0,0,38], [0,0,39], [0,0,40], [0,0,41], [0,0,42], [0,0,43], [0,0,44], [0,0,45], [0,0,46], [0,0,47], [0,0,48], [0,0,49], [0,0,50], [0,0,51], [0,0,52], [0,0,53], [0,0,54], [0,0,55], [0,0,56], [0,0,57], [0,0,58], [0,0,59], [0,0,60], [0,0,61], [0,0,62], [0,0,63], [0,0,64], [0,0,65], [0,0,66], [0,0,67], [0,0,68], [0,0,69], [0,0,70], [0,0,71], [0,0,72], [0,0,73], [0,0,74], [0,0,75], [0,0,76], [0,0,77], [0,0,78], [0,0,79], [0,0,80], [0,0,81], [0,0,82], [0,0,83], [0,0,84], [0,0,85], [0,0,86], [0,0,87], [0,0,88], [0,0,89], [0,0,90], [0,0,91], [0,0,92], [0,0,93], [0,0,94], [0,0,95], [0,0,96], [0,0,97], [0,0,98], [0,0,99], [0,0,100], [0,0,101], [0,0,102], [0,0,103], [0,0,104], [0,0,105], [0,0,106], [0,0,107], [0,0,108], [0,0,109], [0,0,110], [0,0,111], [0,0,112], [0,0,113], [0,0,114], [0,0,115], [0,0,116], [0,0,117], [0,0,118], [0,0,119], [0,0,120], [0,0,121], [0,0,122], [0,0,123], [0,0,124], [0,0,125], [0,0,126], [0,0,127], [0,0,128], [0,0,129], [0,0,130], [0,0,131], [0,0,132], [0,0,133], [0,0,134], [0,0,135], [0,0,136], [0,0,137], [0,0,138], [0,0,139], [0,0,140], [0,0,141], [0,0,142], [0,0,143], [0,0,144], [0,0,145], [0,0,146], [0,0,147], [0,0,148], [0,0,149], [0,0,150], [0,0,151], [0,0,152], [0,0,153], [0,0,154], [0,0,155], [0,0,156], [0,0,157], [0,0,158], [0,0,159], [0,0,160], [0,0,161], [0,0,162], [0,0,163], [0,0,164], [0,0,165], [0,0,166], [0,0,167], [0,0,168], [0,0,169], [0,0,170], [0,0,171], [0,0,172], [0,0,173], [0,0,174], [0,0,175], [0,0,176], [0,0,177], [0,0,178], [0,0,179], [0,0,180], [0,0,181], [0,0,182], [0,0,183], [0,0,184], [0,0,185], [0,0,186], [0,0,187], [0,0,188], [0,0,189], [0,0,190], [0,0,191], [0,0,192], [0,0,193], [0,0,194], [0,0,195], [0,0,196], [0,0,197], [0,0,198], [0,0,199], [0,0,200], [0,0,201], [0,0,202], [0,0,203], [0,0,204], [0,0,205], [0,0,206], [0,0,207], [0,0,208], [0,0,209], [0,0,210], [0,0,211], [0,0,212], [0,0,213], [0,0,214], [0,0,215], [0,0,216], [0,0,217], [0,0,218], [0,0,219], [0,0,220], [0,0,221], [0,0,222], [0,0,223], [0,0,224], [0,0,225], [0,0,226], [0,0,227], [0,0,228], [0,0,229], [0,0,230], [0,0,231], [0,0,232], [0,0,233], [0,0,234], [0,0,235], [0,0,236], [0,0,237], [0,0,238], [0,0,239], [0,0,240], [0,0,241], [0,0,242], [0,0,243], [0,0,244], [0,0,245], [0,0,246], [0,0,247], [0,0,248], [0,0,249], [0,0,250], [0,0,251], [0,0,252], [0,0,253], [0,0,254], [0,0,255]],
coolCm: [ [0,0,0], [0,0,1], [0,0,3], [0,0,5], [0,0,7], [0,0,9], [0,0,11], [0,0,13], [0,0,15], [0,0,17], [0,0,18], [0,0,20], [0,0,22], [0,0,24], [0,0,26], [0,0,28], [0,0,30], [0,0,32], [0,0,34], [0,0,35], [0,0,37], [0,0,39], [0,0,41], [0,0,43], [0,0,45], [0,0,47], [0,0,49], [0,0,51], [0,0,52], [0,0,54], [0,0,56], [0,0,58], [0,0,60], [0,0,62], [0,0,64], [0,0,66], [0,0,68], [0,0,69], [0,0,71], [0,0,73], [0,0,75], [0,0,77], [0,0,79], [0,0,81], [0,0,83], [0,0,85], [0,0,86], [0,0,88], [0,0,90], [0,0,92], [0,0,94], [0,0,96], [0,0,98], [0,0,100], [0,0,102], [0,0,103], [0,0,105], [0,1,107], [0,2,109], [0,4,111], [0,5,113], [0,6,115], [0,8,117], [0,9,119], [0,10,120], [0,12,122], [0,13,124], [0,14,126], [0,16,128], [0,17,130], [0,18,132], [0,20,134], [0,21,136], [0,23,137], [0,24,139], [0,25,141], [0,27,143], [0,28,145], [1,29,147], [1,31,149], [1,32,151], [1,33,153], [1,35,154], [2,36,156], [2,37,158], [2,39,160], [2,40,162], [2,42,164], [3,43,166], [3,44,168], [3,46,170], [3,47,171], [4,48,173], [4,50,175], [4,51,177], [4,52,179], [4,54,181], [5,55,183], [5,56,185], [5,58,187], [5,59,188], [5,61,190], [6,62,192], [6,63,194], [6,65,196], [6,66,198], [7,67,200], [7,69,202], [7,70,204], [7,71,205], [7,73,207], [8,74,209], [8,75,211], [8,77,213], [8,78,215], [8,80,217], [9,81,219], [9,82,221], [9,84,222], [9,85,224], [9,86,226], [10,88,228], [10,89,230], [10,90,232], [10,92,234], [11,93,236], [11,94,238], [11,96,239], [11,97,241], [11,99,243], [12,100,245], [12,101,247], [12,103,249], [12,104,251], [12,105,253], [13,107,255], [13,108,255], [13,109,255], [13,111,255], [14,112,255], [14,113,255], [14,115,255], [14,116,255], [14,118,255], [15,119,255], [15,120,255], [15,122,255], [15,123,255], [15,124,255], [16,126,255], [16,127,255], [16,128,255], [16,130,255], [17,131,255], [17,132,255], [17,134,255], [17,135,255], [17,136,255], [18,138,255], [18,139,255], [18,141,255], [18,142,255], [18,143,255], [19,145,255], [19,146,255], [19,147,255], [19,149,255], [19,150,255], [20,151,255], [20,153,255], [20,154,255], [20,155,255], [21,157,255], [21,158,255], [21,160,255], [21,161,255], [21,162,255], [22,164,255], [22,165,255], [22,166,255], [22,168,255], [22,169,255], [23,170,255], [23,172,255], [23,173,255], [23,174,255], [24,176,255], [24,177,255], [24,179,255], [24,180,255], [24,181,255], [25,183,255], [25,184,255], [25,185,255], [29,187,255], [32,188,255], [36,189,255], [40,191,255], [44,192,255], [47,193,255], [51,195,255], [55,196,255], [58,198,255], [62,199,255], [66,200,255], [69,202,255], [73,203,255], [77,204,255], [81,206,255], [84,207,255], [88,208,255], [92,210,255], [95,211,255], [99,212,255], [103,214,255], [106,215,255], [110,217,255], [114,218,255], [118,219,255], [121,221,255], [125,222,255], [129,223,255], [132,225,255], [136,226,255], [140,227,255], [143,229,255], [147,230,255], [151,231,255], [155,233,255], [158,234,255], [162,236,255], [166,237,255], [169,238,255], [173,240,255], [177,241,255], [180,242,255], [184,244,255], [188,245,255], [192,246,255], [195,248,255], [199,249,255], [203,250,255], [206,252,255], [210,253,255], [214,255,255], [217,255,255], [221,255,255], [225,255,255], [229,255,255], [232,255,255], [236,255,255], [240,255,255], [243,255,255], [247,255,255], [251,255,255], [255,255,255]],
cubehelix0Cm: [ [0,0,0], [2,1,2], [5,2,5], [5,2,5], [6,2,6], [7,2,7], [10,3,10], [12,5,12], [13,5,14], [14,5,16], [15,5,17], [16,6,20], [17,7,22], [18,8,24], [19,9,26], [20,10,28], [21,11,30], [22,12,33], [22,13,34], [22,14,36], [22,15,38], [24,16,40], [25,17,43], [25,18,45], [25,19,46], [25,20,48], [25,22,50], [25,23,51], [25,25,53], [25,26,54], [25,28,56], [25,28,57], [25,29,59], [25,30,61], [25,33,62], [25,35,63], [25,36,65], [25,37,67], [25,38,68], [25,40,70], [25,43,71], [24,45,72], [23,46,73], [22,48,73], [22,49,75], [22,51,76], [22,52,76], [22,54,76], [22,56,76], [22,57,77], [22,59,78], [22,61,79], [21,63,79], [20,66,79], [20,67,79], [20,68,79], [20,68,79], [20,71,79], [20,73,79], [20,75,78], [20,77,77], [20,79,76], [20,80,76], [20,81,76], [21,83,75], [22,85,74], [22,86,73], [22,89,72], [22,91,71], [23,92,71], [24,93,71], [25,94,71], [26,96,70], [28,99,68], [28,100,68], [29,101,67], [30,102,66], [31,102,65], [32,103,64], [33,104,63], [35,105,62], [38,107,61], [39,107,60], [39,108,59], [40,109,58], [43,110,57], [45,112,56], [47,113,55], [49,113,54], [51,114,53], [54,116,52], [58,117,51], [60,117,50], [62,117,49], [63,117,48], [66,118,48], [68,119,48], [71,119,48], [73,119,48], [76,119,48], [79,120,47], [81,121,46], [84,122,45], [87,122,45], [91,122,45], [94,122,46], [96,122,47], [99,122,48], [103,122,48], [107,122,48], [109,122,49], [112,122,50], [114,122,51], [118,122,52], [122,122,53], [124,122,54], [127,122,55], [130,122,56], [133,122,57], [137,122,58], [140,122,60], [142,122,62], [145,122,63], [149,122,66], [153,122,68], [155,121,70], [158,120,72], [160,119,73], [162,119,75], [164,119,77], [165,119,79], [169,119,81], [173,119,84], [175,119,86], [176,119,89], [178,119,91], [181,119,95], [183,119,99], [186,120,102], [188,121,104], [191,122,107], [192,122,110], [193,122,114], [195,122,117], [197,122,119], [198,122,122], [200,122,126], [201,122,130], [202,123,132], [203,124,135], [204,124,137], [204,125,141], [205,126,144], [206,127,147], [207,127,151], [209,127,155], [209,128,158], [210,129,160], [211,130,163], [211,131,167], [211,132,170], [211,133,173], [211,134,175], [211,135,178], [211,136,182], [211,137,186], [211,138,188], [211,139,191], [211,140,193], [211,142,196], [211,145,198], [210,146,201], [209,147,204], [209,147,206], [209,150,209], [209,153,211], [208,153,213], [207,154,215], [206,155,216], [206,157,218], [206,158,220], [206,160,221], [205,163,224], [204,165,226], [203,167,228], [202,169,230], [201,170,232], [201,172,233], [201,173,234], [200,175,235], [199,176,236], [198,178,237], [197,181,238], [196,183,239], [196,185,239], [196,187,239], [196,188,239], [195,191,240], [193,193,242], [193,194,242], [193,195,242], [193,196,242], [193,198,242], [193,199,242], [193,201,242], [193,204,242], [193,206,242], [193,208,242], [193,209,242], [193,211,242], [193,212,242], [193,214,242], [194,215,242], [195,217,242], [196,219,242], [196,220,242], [196,221,242], [197,223,241], [198,225,240], [198,226,239], [200,228,239], [201,229,239], [202,230,239], [203,231,239], [204,232,239], [205,233,239], [206,234,239], [207,235,239], [208,236,239], [209,237,239], [210,238,239], [212,238,239], [214,239,239], [215,240,239], [216,242,239], [218,243,239], [220,243,239], [221,244,239], [224,246,239], [226,247,239], [228,247,240], [230,247,241], [232,247,242], [234,248,242], [237,249,242], [238,249,243], [240,249,243], [242,249,244], [243,251,246], [244,252,247], [246,252,249], [248,252,250], [249,252,252], [251,253,253], [253,254,254], [255,255,255]],
cubehelix1Cm: [ [0,0,0], [2,0,2], [5,0,5], [6,0,7], [8,0,10], [10,0,12], [12,1,15], [15,2,17], [17,2,20], [18,2,22], [20,2,25], [21,2,29], [22,2,33], [23,3,35], [24,4,38], [25,5,40], [25,6,44], [25,7,48], [26,8,51], [27,9,53], [28,10,56], [28,11,59], [28,12,63], [27,14,66], [26,16,68], [25,17,71], [25,18,73], [25,19,74], [25,20,76], [24,22,80], [22,25,84], [22,27,85], [21,28,87], [20,30,89], [19,33,91], [17,35,94], [16,37,95], [14,39,96], [12,40,96], [11,43,99], [10,45,102], [8,47,102], [6,49,103], [5,51,104], [2,54,104], [0,58,104], [0,60,104], [0,62,104], [0,63,104], [0,66,104], [0,68,104], [0,71,104], [0,73,104], [0,76,104], [0,79,103], [0,81,102], [0,84,102], [0,86,99], [0,89,96], [0,91,96], [0,94,95], [0,96,94], [0,99,91], [0,102,89], [0,103,86], [0,105,84], [0,107,81], [0,109,79], [0,112,76], [0,113,73], [0,115,71], [0,117,68], [0,119,65], [0,122,61], [0,124,58], [0,125,56], [0,127,53], [0,128,51], [0,129,48], [0,130,45], [0,132,42], [0,135,38], [0,136,35], [0,136,33], [0,137,30], [3,138,26], [7,140,22], [10,140,21], [12,140,19], [15,140,17], [19,141,14], [22,142,10], [26,142,8], [29,142,6], [33,142,5], [38,142,2], [43,142,0], [46,142,0], [50,142,0], [53,142,0], [57,141,0], [62,141,0], [66,140,0], [72,140,0], [79,140,0], [83,139,0], [87,138,0], [91,137,0], [98,136,0], [104,135,0], [108,134,0], [113,133,0], [117,132,0], [123,131,0], [130,130,0], [134,129,0], [138,128,0], [142,127,0], [149,126,0], [155,124,0], [159,123,0], [164,121,1], [168,119,2], [174,118,6], [181,117,10], [185,116,12], [189,115,15], [193,114,17], [197,113,21], [200,113,24], [204,112,28], [209,110,33], [214,109,38], [217,108,41], [221,107,45], [224,107,48], [228,105,54], [232,104,61], [234,103,64], [237,102,68], [239,102,71], [243,102,77], [247,102,84], [249,101,89], [250,100,94], [252,99,99], [253,99,105], [255,99,112], [255,99,117], [255,99,122], [255,99,127], [255,99,131], [255,99,136], [255,99,140], [255,100,147], [255,102,155], [255,102,159], [255,102,164], [255,102,168], [255,103,174], [255,104,181], [255,105,185], [255,106,189], [255,107,193], [255,108,200], [255,109,206], [255,111,210], [255,113,215], [255,114,219], [253,117,224], [252,119,229], [250,120,232], [249,121,236], [247,122,239], [244,124,244], [242,127,249], [240,130,251], [238,132,253], [237,135,255], [234,136,255], [232,138,255], [229,140,255], [226,142,255], [224,145,255], [222,147,255], [221,150,255], [219,153,255], [215,156,255], [211,160,255], [209,162,255], [208,164,255], [206,165,255], [204,169,255], [201,173,255], [199,175,255], [198,178,255], [196,181,255], [193,183,255], [191,186,255], [189,188,255], [187,191,255], [186,193,255], [185,195,255], [184,197,255], [183,198,255], [182,202,255], [181,206,255], [180,208,255], [179,209,255], [178,211,255], [177,214,255], [175,216,255], [175,218,255], [175,220,255], [175,221,255], [175,224,255], [175,226,255], [176,228,255], [177,230,255], [178,232,255], [179,234,255], [181,237,255], [181,238,255], [182,238,255], [183,239,255], [184,240,252], [186,242,249], [187,243,249], [189,243,248], [191,244,247], [192,245,246], [194,246,245], [196,247,244], [198,248,243], [201,249,242], [203,250,242], [204,251,242], [206,252,242], [210,252,240], [214,252,239], [216,252,240], [219,252,241], [221,252,242], [224,253,242], [226,255,242], [229,255,243], [232,255,243], [234,255,244], [238,255,246], [242,255,247], [243,255,248], [245,255,249], [247,255,249], [249,255,251], [252,255,253], [255,255,255]],
greenCm: [ [0,0,0], [0,1,0], [0,2,0], [0,3,0], [0,4,0], [0,5,0], [0,6,0], [0,7,0], [0,8,0], [0,9,0], [0,10,0], [0,11,0], [0,12,0], [0,13,0], [0,14,0], [0,15,0], [0,16,0], [0,17,0], [0,18,0], [0,19,0], [0,20,0], [0,21,0], [0,22,0], [0,23,0], [0,24,0], [0,25,0], [0,26,0], [0,27,0], [0,28,0], [0,29,0], [0,30,0], [0,31,0], [0,32,0], [0,33,0], [0,34,0], [0,35,0], [0,36,0], [0,37,0], [0,38,0], [0,39,0], [0,40,0], [0,41,0], [0,42,0], [0,43,0], [0,44,0], [0,45,0], [0,46,0], [0,47,0], [0,48,0], [0,49,0], [0,50,0], [0,51,0], [0,52,0], [0,53,0], [0,54,0], [0,55,0], [0,56,0], [0,57,0], [0,58,0], [0,59,0], [0,60,0], [0,61,0], [0,62,0], [0,63,0], [0,64,0], [0,65,0], [0,66,0], [0,67,0], [0,68,0], [0,69,0], [0,70,0], [0,71,0], [0,72,0], [0,73,0], [0,74,0], [0,75,0], [0,76,0], [0,77,0], [0,78,0], [0,79,0], [0,80,0], [0,81,0], [0,82,0], [0,83,0], [0,84,0], [0,85,0], [0,86,0], [0,87,0], [0,88,0], [0,89,0], [0,90,0], [0,91,0], [0,92,0], [0,93,0], [0,94,0], [0,95,0], [0,96,0], [0,97,0], [0,98,0], [0,99,0], [0,100,0], [0,101,0], [0,102,0], [0,103,0], [0,104,0], [0,105,0], [0,106,0], [0,107,0], [0,108,0], [0,109,0], [0,110,0], [0,111,0], [0,112,0], [0,113,0], [0,114,0], [0,115,0], [0,116,0], [0,117,0], [0,118,0], [0,119,0], [0,120,0], [0,121,0], [0,122,0], [0,123,0], [0,124,0], [0,125,0], [0,126,0], [0,127,0], [0,128,0], [0,129,0], [0,130,0], [0,131,0], [0,132,0], [0,133,0], [0,134,0], [0,135,0], [0,136,0], [0,137,0], [0,138,0], [0,139,0], [0,140,0], [0,141,0], [0,142,0], [0,143,0], [0,144,0], [0,145,0], [0,146,0], [0,147,0], [0,148,0], [0,149,0], [0,150,0], [0,151,0], [0,152,0], [0,153,0], [0,154,0], [0,155,0], [0,156,0], [0,157,0], [0,158,0], [0,159,0], [0,160,0], [0,161,0], [0,162,0], [0,163,0], [0,164,0], [0,165,0], [0,166,0], [0,167,0], [0,168,0], [0,169,0], [0,170,0], [0,171,0], [0,172,0], [0,173,0], [0,174,0], [0,175,0], [0,176,0], [0,177,0], [0,178,0], [0,179,0], [0,180,0], [0,181,0], [0,182,0], [0,183,0], [0,184,0], [0,185,0], [0,186,0], [0,187,0], [0,188,0], [0,189,0], [0,190,0], [0,191,0], [0,192,0], [0,193,0], [0,194,0], [0,195,0], [0,196,0], [0,197,0], [0,198,0], [0,199,0], [0,200,0], [0,201,0], [0,202,0], [0,203,0], [0,204,0], [0,205,0], [0,206,0], [0,207,0], [0,208,0], [0,209,0], [0,210,0], [0,211,0], [0,212,0], [0,213,0], [0,214,0], [0,215,0], [0,216,0], [0,217,0], [0,218,0], [0,219,0], [0,220,0], [0,221,0], [0,222,0], [0,223,0], [0,224,0], [0,225,0], [0,226,0], [0,227,0], [0,228,0], [0,229,0], [0,230,0], [0,231,0], [0,232,0], [0,233,0], [0,234,0], [0,235,0], [0,236,0], [0,237,0], [0,238,0], [0,239,0], [0,240,0], [0,241,0], [0,242,0], [0,243,0], [0,244,0], [0,245,0], [0,246,0], [0,247,0], [0,248,0], [0,249,0], [0,250,0], [0,251,0], [0,252,0], [0,253,0], [0,254,0], [0,255,0]],
greyCm: [ [0,0,0], [1,1,1], [2,2,2], [3,3,3], [4,4,4], [5,5,5], [6,6,6], [7,7,7], [8,8,8], [9,9,9], [10,10,10], [11,11,11], [12,12,12], [13,13,13], [14,14,14], [15,15,15], [16,16,16], [17,17,17], [18,18,18], [19,19,19], [20,20,20], [21,21,21], [22,22,22], [23,23,23], [24,24,24], [25,25,25], [26,26,26], [27,27,27], [28,28,28], [29,29,29], [30,30,30], [31,31,31], [32,32,32], [33,33,33], [34,34,34], [35,35,35], [36,36,36], [37,37,37], [38,38,38], [39,39,39], [40,40,40], [41,41,41], [42,42,42], [43,43,43], [44,44,44], [45,45,45], [46,46,46], [47,47,47], [48,48,48], [49,49,49], [50,50,50], [51,51,51], [52,52,52], [53,53,53], [54,54,54], [55,55,55], [56,56,56], [57,57,57], [58,58,58], [59,59,59], [60,60,60], [61,61,61], [62,62,62], [63,63,63], [64,64,64], [65,65,65], [66,66,66], [67,67,67], [68,68,68], [69,69,69], [70,70,70], [71,71,71], [72,72,72], [73,73,73], [74,74,74], [75,75,75], [76,76,76], [77,77,77], [78,78,78], [79,79,79], [80,80,80], [81,81,81], [82,82,82], [83,83,83], [84,84,84], [85,85,85], [86,86,86], [87,87,87], [88,88,88], [89,89,89], [90,90,90], [91,91,91], [92,92,92], [93,93,93], [94,94,94], [95,95,95], [96,96,96], [97,97,97], [98,98,98], [99,99,99], [100,100,100], [101,101,101], [102,102,102], [103,103,103], [104,104,104], [105,105,105], [106,106,106], [107,107,107], [108,108,108], [109,109,109], [110,110,110], [111,111,111], [112,112,112], [113,113,113], [114,114,114], [115,115,115], [116,116,116], [117,117,117], [118,118,118], [119,119,119], [120,120,120], [121,121,121], [122,122,122], [123,123,123], [124,124,124], [125,125,125], [126,126,126], [127,127,127], [128,128,128], [129,129,129], [130,130,130], [131,131,131], [132,132,132], [133,133,133], [134,134,134], [135,135,135], [136,136,136], [137,137,137], [138,138,138], [139,139,139], [140,140,140], [141,141,141], [142,142,142], [143,143,143], [144,144,144], [145,145,145], [146,146,146], [147,147,147], [148,148,148], [149,149,149], [150,150,150], [151,151,151], [152,152,152], [153,153,153], [154,154,154], [155,155,155], [156,156,156], [157,157,157], [158,158,158], [159,159,159], [160,160,160], [161,161,161], [162,162,162], [163,163,163], [164,164,164], [165,165,165], [166,166,166], [167,167,167], [168,168,168], [169,169,169], [170,170,170], [171,171,171], [172,172,172], [173,173,173], [174,174,174], [175,175,175], [176,176,176], [177,177,177], [178,178,178], [179,179,179], [180,180,180], [181,181,181], [182,182,182], [183,183,183], [184,184,184], [185,185,185], [186,186,186], [187,187,187], [188,188,188], [189,189,189], [190,190,190], [191,191,191], [192,192,192], [193,193,193], [194,194,194], [195,195,195], [196,196,196], [197,197,197], [198,198,198], [199,199,199], [200,200,200], [201,201,201], [202,202,202], [203,203,203], [204,204,204], [205,205,205], [206,206,206], [207,207,207], [208,208,208], [209,209,209], [210,210,210], [211,211,211], [212,212,212], [213,213,213], [214,214,214], [215,215,215], [216,216,216], [217,217,217], [218,218,218], [219,219,219], [220,220,220], [221,221,221], [222,222,222], [223,223,223], [224,224,224], [225,225,225], [226,226,226], [227,227,227], [228,228,228], [229,229,229], [230,230,230], [231,231,231], [232,232,232], [233,233,233], [234,234,234], [235,235,235], [236,236,236], [237,237,237], [238,238,238], [239,239,239], [240,240,240], [241,241,241], [242,242,242], [243,243,243], [244,244,244], [245,245,245], [246,246,246], [247,247,247], [248,248,248], [249,249,249], [250,250,250], [251,251,251], [252,252,252], [253,253,253], [254,254,254], [255,255,255]],
heCm: [ [0,0,0], [42,0,10], [85,0,21], [127,0,31], [127,0,47], [127,0,63], [127,0,79], [127,0,95], [127,0,102], [127,0,109], [127,0,116], [127,0,123], [127,0,131], [127,0,138], [127,0,145], [127,0,152], [127,0,159], [127,8,157], [127,17,155], [127,25,153], [127,34,151], [127,42,149], [127,51,147], [127,59,145], [127,68,143], [127,76,141], [127,85,139], [127,93,136], [127,102,134], [127,110,132], [127,119,130], [127,127,128], [127,129,126], [127,131,124], [127,133,122], [127,135,120], [127,137,118], [127,139,116], [127,141,114], [127,143,112], [127,145,110], [127,147,108], [127,149,106], [127,151,104], [127,153,102], [127,155,100], [127,157,98], [127,159,96], [127,161,94], [127,163,92], [127,165,90], [127,167,88], [127,169,86], [127,171,84], [127,173,82], [127,175,80], [127,177,77], [127,179,75], [127,181,73], [127,183,71], [127,185,69], [127,187,67], [127,189,65], [127,191,63], [128,191,64], [129,191,65], [130,191,66], [131,192,67], [132,192,68], [133,192,69], [134,192,70], [135,193,71], [136,193,72], [137,193,73], [138,193,74], [139,194,75], [140,194,76], [141,194,77], [142,194,78], [143,195,79], [144,195,80], [145,195,81], [146,195,82], [147,196,83], [148,196,84], [149,196,85], [150,196,86], [151,196,87], [152,197,88], [153,197,89], [154,197,90], [155,197,91], [156,198,92], [157,198,93], [158,198,94], [159,198,95], [160,199,96], [161,199,97], [162,199,98], [163,199,99], [164,200,100], [165,200,101], [166,200,102], [167,200,103], [168,201,104], [169,201,105], [170,201,106], [171,201,107], [172,202,108], [173,202,109], [174,202,110], [175,202,111], [176,202,112], [177,203,113], [178,203,114], [179,203,115], [180,203,116], [181,204,117], [182,204,118], [183,204,119], [184,204,120], [185,205,121], [186,205,122], [187,205,123], [188,205,124], [189,206,125], [190,206,126], [191,206,127], [191,206,128], [192,207,129], [192,207,130], [193,208,131], [193,208,132], [194,208,133], [194,209,134], [195,209,135], [195,209,136], [196,210,137], [196,210,138], [197,211,139], [197,211,140], [198,211,141], [198,212,142], [199,212,143], [199,212,144], [200,213,145], [200,213,146], [201,214,147], [201,214,148], [202,214,149], [202,215,150], [203,215,151], [203,216,152], [204,216,153], [204,216,154], [205,217,155], [205,217,156], [206,217,157], [206,218,158], [207,218,159], [207,219,160], [208,219,161], [208,219,162], [209,220,163], [209,220,164], [210,220,165], [210,221,166], [211,221,167], [211,222,168], [212,222,169], [212,222,170], [213,223,171], [213,223,172], [214,223,173], [214,224,174], [215,224,175], [215,225,176], [216,225,177], [216,225,178], [217,226,179], [217,226,180], [218,226,181], [218,227,182], [219,227,183], [219,228,184], [220,228,185], [220,228,186], [221,229,187], [221,229,188], [222,230,189], [222,230,190], [223,230,191], [223,231,192], [224,231,193], [224,231,194], [225,232,195], [225,232,196], [226,233,197], [226,233,198], [227,233,199], [227,234,200], [228,234,201], [228,234,202], [229,235,203], [229,235,204], [230,236,205], [230,236,206], [231,236,207], [231,237,208], [232,237,209], [232,237,210], [233,238,211], [233,238,212], [234,239,213], [234,239,214], [235,239,215], [235,240,216], [236,240,217], [236,240,218], [237,241,219], [237,241,220], [238,242,221], [238,242,222], [239,242,223], [239,243,224], [240,243,225], [240,244,226], [241,244,227], [241,244,228], [242,245,229], [242,245,230], [243,245,231], [243,246,232], [244,246,233], [244,247,234], [245,247,235], [245,247,236], [246,248,237], [246,248,238], [247,248,239], [247,249,240], [248,249,241], [248,250,242], [249,250,243], [249,250,244], [250,251,245], [250,251,246], [251,251,247], [251,252,248], [252,252,249], [252,253,250], [253,253,251], [253,253,252], [254,254,253], [254,254,254], [255,255,255]],
heatCm: [ [0,0,0], [2,1,0], [5,2,0], [8,3,0], [11,4,0], [14,5,0], [17,6,0], [20,7,0], [23,8,0], [26,9,0], [29,10,0], [32,11,0], [35,12,0], [38,13,0], [41,14,0], [44,15,0], [47,16,0], [50,17,0], [53,18,0], [56,19,0], [59,20,0], [62,21,0], [65,22,0], [68,23,0], [71,24,0], [74,25,0], [77,26,0], [80,27,0], [83,28,0], [85,29,0], [88,30,0], [91,31,0], [94,32,0], [97,33,0], [100,34,0], [103,35,0], [106,36,0], [109,37,0], [112,38,0], [115,39,0], [118,40,0], [121,41,0], [124,42,0], [127,43,0], [130,44,0], [133,45,0], [136,46,0], [139,47,0], [142,48,0], [145,49,0], [148,50,0], [151,51,0], [154,52,0], [157,53,0], [160,54,0], [163,55,0], [166,56,0], [169,57,0], [171,58,0], [174,59,0], [177,60,0], [180,61,0], [183,62,0], [186,63,0], [189,64,0], [192,65,0], [195,66,0], [198,67,0], [201,68,0], [204,69,0], [207,70,0], [210,71,0], [213,72,0], [216,73,0], [219,74,0], [222,75,0], [225,76,0], [228,77,0], [231,78,0], [234,79,0], [237,80,0], [240,81,0], [243,82,0], [246,83,0], [249,84,0], [252,85,0], [255,86,0], [255,87,0], [255,88,0], [255,89,0], [255,90,0], [255,91,0], [255,92,0], [255,93,0], [255,94,0], [255,95,0], [255,96,0], [255,97,0], [255,98,0], [255,99,0], [255,100,0], [255,101,0], [255,102,0], [255,103,0], [255,104,0], [255,105,0], [255,106,0], [255,107,0], [255,108,0], [255,109,0], [255,110,0], [255,111,0], [255,112,0], [255,113,0], [255,114,0], [255,115,0], [255,116,0], [255,117,0], [255,118,0], [255,119,0], [255,120,0], [255,121,0], [255,122,0], [255,123,0], [255,124,0], [255,125,0], [255,126,0], [255,127,0], [255,128,0], [255,129,0], [255,130,0], [255,131,0], [255,132,0], [255,133,0], [255,134,0], [255,135,0], [255,136,0], [255,137,0], [255,138,0], [255,139,0], [255,140,0], [255,141,0], [255,142,0], [255,143,0], [255,144,0], [255,145,0], [255,146,0], [255,147,0], [255,148,0], [255,149,0], [255,150,0], [255,151,0], [255,152,0], [255,153,0], [255,154,0], [255,155,0], [255,156,0], [255,157,0], [255,158,0], [255,159,0], [255,160,0], [255,161,0], [255,162,0], [255,163,0], [255,164,0], [255,165,0], [255,166,3], [255,167,6], [255,168,9], [255,169,12], [255,170,15], [255,171,18], [255,172,21], [255,173,24], [255,174,27], [255,175,30], [255,176,33], [255,177,36], [255,178,39], [255,179,42], [255,180,45], [255,181,48], [255,182,51], [255,183,54], [255,184,57], [255,185,60], [255,186,63], [255,187,66], [255,188,69], [255,189,72], [255,190,75], [255,191,78], [255,192,81], [255,193,85], [255,194,88], [255,195,91], [255,196,94], [255,197,97], [255,198,100], [255,199,103], [255,200,106], [255,201,109], [255,202,112], [255,203,115], [255,204,118], [255,205,121], [255,206,124], [255,207,127], [255,208,130], [255,209,133], [255,210,136], [255,211,139], [255,212,142], [255,213,145], [255,214,148], [255,215,151], [255,216,154], [255,217,157], [255,218,160], [255,219,163], [255,220,166], [255,221,170], [255,222,173], [255,223,176], [255,224,179], [255,225,182], [255,226,185], [255,227,188], [255,228,191], [255,229,194], [255,230,197], [255,231,200], [255,232,203], [255,233,206], [255,234,209], [255,235,212], [255,236,215], [255,237,218], [255,238,221], [255,239,224], [255,240,227], [255,241,230], [255,242,233], [255,243,236], [255,244,239], [255,245,242], [255,246,245], [255,247,248], [255,248,251], [255,249,255], [255,250,255], [255,251,255], [255,252,255], [255,253,255], [255,254,255], [255,255,255]],
rainbowCm: [ [255,0,255], [250,0,255], [245,0,255], [240,0,255], [235,0,255], [230,0,255], [225,0,255], [220,0,255], [215,0,255], [210,0,255], [205,0,255], [200,0,255], [195,0,255], [190,0,255], [185,0,255], [180,0,255], [175,0,255], [170,0,255], [165,0,255], [160,0,255], [155,0,255], [150,0,255], [145,0,255], [140,0,255], [135,0,255], [130,0,255], [125,0,255], [120,0,255], [115,0,255], [110,0,255], [105,0,255], [100,0,255], [95,0,255], [90,0,255], [85,0,255], [80,0,255], [75,0,255], [70,0,255], [65,0,255], [60,0,255], [55,0,255], [50,0,255], [45,0,255], [40,0,255], [35,0,255], [30,0,255], [25,0,255], [20,0,255], [15,0,255], [10,0,255], [5,0,255], [0,0,255], [0,5,255], [0,10,255], [0,15,255], [0,20,255], [0,25,255], [0,30,255], [0,35,255], [0,40,255], [0,45,255], [0,50,255], [0,55,255], [0,60,255], [0,65,255], [0,70,255], [0,75,255], [0,80,255], [0,85,255], [0,90,255], [0,95,255], [0,100,255], [0,105,255], [0,110,255], [0,115,255], [0,120,255], [0,125,255], [0,130,255], [0,135,255], [0,140,255], [0,145,255], [0,150,255], [0,155,255], [0,160,255], [0,165,255], [0,170,255], [0,175,255], [0,180,255], [0,185,255], [0,190,255], [0,195,255], [0,200,255], [0,205,255], [0,210,255], [0,215,255], [0,220,255], [0,225,255], [0,230,255], [0,235,255], [0,240,255], [0,245,255], [0,250,255], [0,255,255], [0,255,250], [0,255,245], [0,255,240], [0,255,235], [0,255,230], [0,255,225], [0,255,220], [0,255,215], [0,255,210], [0,255,205], [0,255,200], [0,255,195], [0,255,190], [0,255,185], [0,255,180], [0,255,175], [0,255,170], [0,255,165], [0,255,160], [0,255,155], [0,255,150], [0,255,145], [0,255,140], [0,255,135], [0,255,130], [0,255,125], [0,255,120], [0,255,115], [0,255,110], [0,255,105], [0,255,100], [0,255,95], [0,255,90], [0,255,85], [0,255,80], [0,255,75], [0,255,70], [0,255,65], [0,255,60], [0,255,55], [0,255,50], [0,255,45], [0,255,40], [0,255,35], [0,255,30], [0,255,25], [0,255,20], [0,255,15], [0,255,10], [0,255,5], [0,255,0], [5,255,0], [10,255,0], [15,255,0], [20,255,0], [25,255,0], [30,255,0], [35,255,0], [40,255,0], [45,255,0], [50,255,0], [55,255,0], [60,255,0], [65,255,0], [70,255,0], [75,255,0], [80,255,0], [85,255,0], [90,255,0], [95,255,0], [100,255,0], [105,255,0], [110,255,0], [115,255,0], [120,255,0], [125,255,0], [130,255,0], [135,255,0], [140,255,0], [145,255,0], [150,255,0], [155,255,0], [160,255,0], [165,255,0], [170,255,0], [175,255,0], [180,255,0], [185,255,0], [190,255,0], [195,255,0], [200,255,0], [205,255,0], [210,255,0], [215,255,0], [220,255,0], [225,255,0], [230,255,0], [235,255,0], [240,255,0], [245,255,0], [250,255,0], [255,255,0], [255,250,0], [255,245,0], [255,240,0], [255,235,0], [255,230,0], [255,225,0], [255,220,0], [255,215,0], [255,210,0], [255,205,0], [255,200,0], [255,195,0], [255,190,0], [255,185,0], [255,180,0], [255,175,0], [255,170,0], [255,165,0], [255,160,0], [255,155,0], [255,150,0], [255,145,0], [255,140,0], [255,135,0], [255,130,0], [255,125,0], [255,120,0], [255,115,0], [255,110,0], [255,105,0], [255,100,0], [255,95,0], [255,90,0], [255,85,0], [255,80,0], [255,75,0], [255,70,0], [255,65,0], [255,60,0], [255,55,0], [255,50,0], [255,45,0], [255,40,0], [255,35,0], [255,30,0], [255,25,0], [255,20,0], [255,15,0], [255,10,0], [255,5,0], [255,0,0]],
redCm: [ [0,0,0], [1,0,0], [2,0,0], [3,0,0], [4,0,0], [5,0,0], [6,0,0], [7,0,0], [8,0,0], [9,0,0], [10,0,0], [11,0,0], [12,0,0], [13,0,0], [14,0,0], [15,0,0], [16,0,0], [17,0,0], [18,0,0], [19,0,0], [20,0,0], [21,0,0], [22,0,0], [23,0,0], [24,0,0], [25,0,0], [26,0,0], [27,0,0], [28,0,0], [29,0,0], [30,0,0], [31,0,0], [32,0,0], [33,0,0], [34,0,0], [35,0,0], [36,0,0], [37,0,0], [38,0,0], [39,0,0], [40,0,0], [41,0,0], [42,0,0], [43,0,0], [44,0,0], [45,0,0], [46,0,0], [47,0,0], [48,0,0], [49,0,0], [50,0,0], [51,0,0], [52,0,0], [53,0,0], [54,0,0], [55,0,0], [56,0,0], [57,0,0], [58,0,0], [59,0,0], [60,0,0], [61,0,0], [62,0,0], [63,0,0], [64,0,0], [65,0,0], [66,0,0], [67,0,0], [68,0,0], [69,0,0], [70,0,0], [71,0,0], [72,0,0], [73,0,0], [74,0,0], [75,0,0], [76,0,0], [77,0,0], [78,0,0], [79,0,0], [80,0,0], [81,0,0], [82,0,0], [83,0,0], [84,0,0], [85,0,0], [86,0,0], [87,0,0], [88,0,0], [89,0,0], [90,0,0], [91,0,0], [92,0,0], [93,0,0], [94,0,0], [95,0,0], [96,0,0], [97,0,0], [98,0,0], [99,0,0], [100,0,0], [101,0,0], [102,0,0], [103,0,0], [104,0,0], [105,0,0], [106,0,0], [107,0,0], [108,0,0], [109,0,0], [110,0,0], [111,0,0], [112,0,0], [113,0,0], [114,0,0], [115,0,0], [116,0,0], [117,0,0], [118,0,0], [119,0,0], [120,0,0], [121,0,0], [122,0,0], [123,0,0], [124,0,0], [125,0,0], [126,0,0], [127,0,0], [128,0,0], [129,0,0], [130,0,0], [131,0,0], [132,0,0], [133,0,0], [134,0,0], [135,0,0], [136,0,0], [137,0,0], [138,0,0], [139,0,0], [140,0,0], [141,0,0], [142,0,0], [143,0,0], [144,0,0], [145,0,0], [146,0,0], [147,0,0], [148,0,0], [149,0,0], [150,0,0], [151,0,0], [152,0,0], [153,0,0], [154,0,0], [155,0,0], [156,0,0], [157,0,0], [158,0,0], [159,0,0], [160,0,0], [161,0,0], [162,0,0], [163,0,0], [164,0,0], [165,0,0], [166,0,0], [167,0,0], [168,0,0], [169,0,0], [170,0,0], [171,0,0], [172,0,0], [173,0,0], [174,0,0], [175,0,0], [176,0,0], [177,0,0], [178,0,0], [179,0,0], [180,0,0], [181,0,0], [182,0,0], [183,0,0], [184,0,0], [185,0,0], [186,0,0], [187,0,0], [188,0,0], [189,0,0], [190,0,0], [191,0,0], [192,0,0], [193,0,0], [194,0,0], [195,0,0], [196,0,0], [197,0,0], [198,0,0], [199,0,0], [200,0,0], [201,0,0], [202,0,0], [203,0,0], [204,0,0], [205,0,0], [206,0,0], [207,0,0], [208,0,0], [209,0,0], [210,0,0], [211,0,0], [212,0,0], [213,0,0], [214,0,0], [215,0,0], [216,0,0], [217,0,0], [218,0,0], [219,0,0], [220,0,0], [221,0,0], [222,0,0], [223,0,0], [224,0,0], [225,0,0], [226,0,0], [227,0,0], [228,0,0], [229,0,0], [230,0,0], [231,0,0], [232,0,0], [233,0,0], [234,0,0], [235,0,0], [236,0,0], [237,0,0], [238,0,0], [239,0,0], [240,0,0], [241,0,0], [242,0,0], [243,0,0], [244,0,0], [245,0,0], [246,0,0], [247,0,0], [248,0,0], [249,0,0], [250,0,0], [251,0,0], [252,0,0], [253,0,0], [254,0,0], [255,0,0]],
standardCm: [ [0,0,0], [0,0,3], [1,1,6], [2,2,9], [3,3,12], [4,4,15], [5,5,18], [6,6,21], [7,7,24], [8,8,27], [9,9,30], [10,10,33], [10,10,36], [11,11,39], [12,12,42], [13,13,45], [14,14,48], [15,15,51], [16,16,54], [17,17,57], [18,18,60], [19,19,63], [20,20,66], [20,20,69], [21,21,72], [22,22,75], [23,23,78], [24,24,81], [25,25,85], [26,26,88], [27,27,91], [28,28,94], [29,29,97], [30,30,100], [30,30,103], [31,31,106], [32,32,109], [33,33,112], [34,34,115], [35,35,118], [36,36,121], [37,37,124], [38,38,127], [39,39,130], [40,40,133], [40,40,136], [41,41,139], [42,42,142], [43,43,145], [44,44,148], [45,45,151], [46,46,154], [47,47,157], [48,48,160], [49,49,163], [50,50,166], [51,51,170], [51,51,173], [52,52,176], [53,53,179], [54,54,182], [55,55,185], [56,56,188], [57,57,191], [58,58,194], [59,59,197], [60,60,200], [61,61,203], [61,61,206], [62,62,209], [63,63,212], [64,64,215], [65,65,218], [66,66,221], [67,67,224], [68,68,227], [69,69,230], [70,70,233], [71,71,236], [71,71,239], [72,72,242], [73,73,245], [74,74,248], [75,75,251], [76,76,255], [0,78,0], [1,80,1], [2,82,2], [3,84,3], [4,87,4], [5,89,5], [6,91,6], [7,93,7], [8,95,8], [9,97,9], [9,99,9], [10,101,10], [11,103,11], [12,105,12], [13,108,13], [14,110,14], [15,112,15], [16,114,16], [17,116,17], [18,118,18], [18,120,18], [19,122,19], [20,124,20], [21,126,21], [22,129,22], [23,131,23], [24,133,24], [25,135,25], [26,137,26], [27,139,27], [27,141,27], [28,143,28], [29,145,29], [30,147,30], [31,150,31], [32,152,32], [33,154,33], [34,156,34], [35,158,35], [36,160,36], [36,162,36], [37,164,37], [38,166,38], [39,168,39], [40,171,40], [41,173,41], [42,175,42], [43,177,43], [44,179,44], [45,181,45], [45,183,45], [46,185,46], [47,187,47], [48,189,48], [49,192,49], [50,194,50], [51,196,51], [52,198,52], [53,200,53], [54,202,54], [54,204,54], [55,206,55], [56,208,56], [57,210,57], [58,213,58], [59,215,59], [60,217,60], [61,219,61], [62,221,62], [63,223,63], [63,225,63], [64,227,64], [65,229,65], [66,231,66], [67,234,67], [68,236,68], [69,238,69], [70,240,70], [71,242,71], [72,244,72], [72,246,72], [73,248,73], [74,250,74], [75,252,75], [76,255,76], [78,0,0], [80,1,1], [82,2,2], [84,3,3], [86,4,4], [88,5,5], [91,6,6], [93,7,7], [95,8,8], [97,8,8], [99,9,9], [101,10,10], [103,11,11], [105,12,12], [107,13,13], [109,14,14], [111,15,15], [113,16,16], [115,16,16], [118,17,17], [120,18,18], [122,19,19], [124,20,20], [126,21,21], [128,22,22], [130,23,23], [132,24,24], [134,24,24], [136,25,25], [138,26,26], [140,27,27], [142,28,28], [144,29,29], [147,30,30], [149,31,31], [151,32,32], [153,32,32], [155,33,33], [157,34,34], [159,35,35], [161,36,36], [163,37,37], [165,38,38], [167,39,39], [169,40,40], [171,40,40], [174,41,41], [176,42,42], [178,43,43], [180,44,44], [182,45,45], [184,46,46], [186,47,47], [188,48,48], [190,48,48], [192,49,49], [194,50,50], [196,51,51], [198,52,52], [201,53,53], [203,54,54], [205,55,55], [207,56,56], [209,56,56], [211,57,57], [213,58,58], [215,59,59], [217,60,60], [219,61,61], [221,62,62], [223,63,63], [225,64,64], [228,64,64], [230,65,65], [232,66,66], [234,67,67], [236,68,68], [238,69,69], [240,70,70], [242,71,71], [244,72,72], [246,72,72], [248,73,73], [250,74,74], [252,75,75], [255,76,76]]
};
let cmapOptions = '';
Object.keys(cmaps).forEach(function(c) {
cmapOptions += '<option value="' + c + '">' + c + '</option>';
});
const $html = $('<div>' +
' Colormap: <select id="cmapSelect">' + cmapOptions +
' </select><br>' +
' Center: <span id="cmapCenter"></span>' +
'</div>');
const cmapUpdate = function() {
const val = $('#cmapSelect').val();
$('#cmapSelect').change(function() {
updateCallback(val);
});
return cmaps[val];
};
const spinnerSlider = new Spinner({
$element: $html.find('#cmapCenter'),
init: 128,
min: 1,
sliderMax: 254,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
/*eslint new-cap: 0*/
return OpenSeadragon.Filters.COLORMAP(cmapUpdate(), spinnerSlider.getValue());
}
};
}
}, {
name: 'Colorize',
help: 'The adjustment range (strength) is from 0 to 100.' +
'The higher the value, the closer the colors in the ' +
'image shift towards the given adjustment color.' +
'Color values are between 0 to 255',
generate: function(updateCallback) {
const redSpinnerId = 'redSpinner-' + idIncrement;
const greenSpinnerId = 'greenSpinner-' + idIncrement;
const blueSpinnerId = 'blueSpinner-' + idIncrement;
const strengthSpinnerId = 'strengthSpinner-' + idIncrement;
/*eslint max-len: 0*/
const $html = $('<div class="wdzt-table-layout">' +
'<div class="wdzt-row-layout">' +
' <div class="wdzt-cell-layout">' +
' Red: <span id="' + redSpinnerId + '"></span>' +
' </div>' +
' <div class="wdzt-cell-layout">' +
' Green: <span id="' + greenSpinnerId + '"></span>' +
' </div>' +
' <div class="wdzt-cell-layout">' +
' Blue: <span id="' + blueSpinnerId + '"></span>' +
' </div>' +
' <div class="wdzt-cell-layout">' +
' Strength: <span id="' + strengthSpinnerId + '"></span>' +
' </div>' +
'</div>' +
'</div>');
const redSpinner = new Spinner({
$element: $html.find('#' + redSpinnerId),
init: 100,
min: 0,
max: 255,
step: 1,
updateCallback: updateCallback
});
const greenSpinner = new Spinner({
$element: $html.find('#' + greenSpinnerId),
init: 20,
min: 0,
max: 255,
step: 1,
updateCallback: updateCallback
});
const blueSpinner = new Spinner({
$element: $html.find('#' + blueSpinnerId),
init: 20,
min: 0,
max: 255,
step: 1,
updateCallback: updateCallback
});
const strengthSpinner = new Spinner({
$element: $html.find('#' + strengthSpinnerId),
init: 50,
min: 0,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
const red = redSpinner.getValue();
const green = greenSpinner.getValue();
const blue = blueSpinner.getValue();
const strength = strengthSpinner.getValue();
return 'R: ' + red + ' G: ' + green + ' B: ' + blue +
' S: ' + strength;
},
getFilter: function() {
const red = redSpinner.getValue();
const green = greenSpinner.getValue();
const blue = blueSpinner.getValue();
const strength = strengthSpinner.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.colorize(red, green, blue, strength);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Contrast',
help: 'Range is from 0 to infinity, although sane values are from 0 ' +
'to 4 or 5. Values between 0 and 1 will lessen the contrast ' +
'while values greater than 1 will increase it.',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 1.3,
min: 0,
sliderMax: 4,
step: 0.1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
return OpenSeadragon.Filters.CONTRAST(
spinnerSlider.getValue());
}
};
}
}, {
name: 'Exposure',
help: 'Range is -100 to 100. Values < 0 will decrease ' +
'exposure while values > 0 will increase exposure',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 10,
min: -100,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.exposure(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Gamma',
help: 'Range is from 0 to infinity, although sane values ' +
'are from 0 to 4 or 5. Values between 0 and 1 will ' +
'lessen the contrast while values greater than 1 will increase it.',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 0.5,
min: 0,
sliderMax: 5,
step: 0.1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return OpenSeadragon.Filters.GAMMA(value);
}
};
}
}, {
name: 'Hue',
help: 'hue value is between 0 to 100 representing the ' +
'percentage of Hue shift in the 0 to 360 range',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 20,
min: 0,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.hue(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Saturation',
help: 'saturation value has to be between -100 and 100',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 50,
min: -100,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.saturation(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Vibrance',
help: 'vibrance value has to be between -100 and 100',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 50,
min: -100,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.vibrance(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Sepia',
help: 'sepia value has to be between 0 and 100',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 50,
min: 0,
max: 100,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.sepia(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Noise',
help: 'Noise cannot be smaller than 0',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 50,
min: 0,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
const value = spinnerSlider.getValue();
return function(context) {
const promise = getPromiseResolver();
Caman(context.canvas, function() {
this.noise(value);
this.render(promise.call.back);
});
return promise.promise;
};
}
};
}
}, {
name: 'Greyscale',
generate: function() {
return {
html: '',
getParams: function() {
return '';
},
getFilter: function() {
return OpenSeadragon.Filters.GREYSCALE();
}
};
}
}, {
name: 'Sobel Edge',
generate: function() {
return {
html: '',
getParams: function() {
return '';
},
getFilter: function() {
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
const originalPixels = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
const oneRowOffset = context.canvas.width * 4;
const onePixelOffset = 4;
let Gy, Gx, idx = 0;
for (let i = 1; i < context.canvas.height - 1; i += 1) {
idx = oneRowOffset * i + 4;
for (let j = 1; j < context.canvas.width - 1; j += 1) {
Gy = originalPixels[idx - onePixelOffset + oneRowOffset] + 2 * originalPixels[idx + oneRowOffset] + originalPixels[idx + onePixelOffset + oneRowOffset];
Gy = Gy - (originalPixels[idx - onePixelOffset - oneRowOffset] + 2 * originalPixels[idx - oneRowOffset] + originalPixels[idx + onePixelOffset - oneRowOffset]);
Gx = originalPixels[idx + onePixelOffset - oneRowOffset] + 2 * originalPixels[idx + onePixelOffset] + originalPixels[idx + onePixelOffset + oneRowOffset];
Gx = Gx - (originalPixels[idx - onePixelOffset - oneRowOffset] + 2 * originalPixels[idx - onePixelOffset] + originalPixels[idx - onePixelOffset + oneRowOffset]);
pixels[idx] = Math.sqrt(Gx * Gx + Gy * Gy); // 0.5*Math.abs(Gx) + 0.5*Math.abs(Gy);//100*Math.atan(Gy,Gx);
pixels[idx + 1] = 0;
pixels[idx + 2] = 0;
idx += 4;
}
}
context.putImageData(imgData, 0, 0);
};
}
};
}
}, {
name: 'Brightness',
help: 'Brightness must be between -255 (darker) and 255 (brighter).',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 50,
min: -255,
max: 255,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
return OpenSeadragon.Filters.BRIGHTNESS(
spinnerSlider.getValue());
}
};
}
}, {
name: 'Erosion',
help: 'The erosion kernel size must be an odd number.',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinner = new Spinner({
$element: $html,
init: 3,
min: 3,
step: 2,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinner.getValue();
},
getFilter: function() {
return OpenSeadragon.Filters.MORPHOLOGICAL_OPERATION(
spinner.getValue(), Math.min);
}
};
}
}, {
name: 'Dilation',
help: 'The dilation kernel size must be an odd number.',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinner = new Spinner({
$element: $html,
init: 3,
min: 3,
step: 2,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinner.getValue();
},
getFilter: function() {
return OpenSeadragon.Filters.MORPHOLOGICAL_OPERATION(
spinner.getValue(), Math.max);
}
};
}
}, {
name: 'Thresholding',
help: 'The threshold must be between 0 and 255.',
generate: function(updateCallback) {
const $html = $('<div></div>');
const spinnerSlider = new SpinnerSlider({
$element: $html,
init: 127,
min: 0,
max: 255,
step: 1,
updateCallback: updateCallback
});
return {
html: $html,
getParams: function() {
return spinnerSlider.getValue();
},
getFilter: function() {
return OpenSeadragon.Filters.THRESHOLDING(
spinnerSlider.getValue());
}
};
}
}];
availableFilters.sort(function(f1, f2) {
return f1.name.localeCompare(f2.name);
});
let idIncrement = 0;
const hashTable = {};
availableFilters.forEach(function(filter) {
const $li = $('<li></li>');
const $plus = $('<img src="static/plus.png" alt="+" class="button">');
$li.append($plus);
$li.append(filter.name);
$li.appendTo($('#available'));
$plus.click(function() {
const id = 'selected_' + idIncrement++;
const generatedFilter = filter.generate(updateFilters);
hashTable[id] = {
name: filter.name,
generatedFilter: generatedFilter
};
const $li = $('<li id="' + id + '"><div class="wdzt-table-layout"><div class="wdzt-row-layout"></div></div></li>');
const $minus = $('<div class="wdzt-cell-layout"><img src="static/minus.png" alt="-" class="button"></div>');
$li.find('.wdzt-row-layout').append($minus);
$li.find('.wdzt-row-layout').append('<div class="wdzt-cell-layout filterLabel">' + filter.name + '</div>');
if (filter.help) {
const $help = $('<div class="wdzt-cell-layout"><span title="' + filter.help + '">&nbsp;?&emsp;</span></div>');
$help.tooltip();
$li.find('.wdzt-row-layout').append($help);
}
$li.find('.wdzt-row-layout').append(
$('<div class="wdzt-cell-layout wdzt-full-width"></div>')
.append(generatedFilter.html));
$minus.click(function() {
delete hashTable[id];
$li.remove();
updateFilters();
});
$li.appendTo($('#selected'));
updateFilters();
});
});
$('#selected').sortable({
containment: 'parent',
axis: 'y',
tolerance: 'pointer',
update: updateFilters
});
function getPromiseResolver() {
let call = {};
let promise = new OpenSeadragon.Promise(resolve => {
call.back = resolve;
});
return {call, promise};
}
function updateFilters() {
const filters = [];
$('#selected li').each(function() {
const id = this.id;
const filter = hashTable[id];
filters.push(filter.generatedFilter.getFilter());
});
viewer.setFilterOptions({
filters: {
processors: filters
}
});
}
window.debugCache = function () {
for (let cacheKey in viewer.tileCache._cachesLoaded) {
let cache = viewer.tileCache._cachesLoaded[cacheKey];
if (!cache.loaded) {
console.log(cacheKey, "skipping...");
}
if (cache.type === "context2d") {
console.log(cacheKey, cache.data.canvas.width, cache.data.canvas.height);
} else {
console.log(cacheKey, cache.data);
}
}
}
// Monitoring of tiles:
let monitoredTile = null;
async function updateCanvas(node, tile, targetCacheKey) {
const data = await tile.getCache(targetCacheKey)?.getDataAs('context2d', true);
if (!data) {
const text = document.createElement("span");
text.innerHTML = targetCacheKey + "<br> empty";
node.replaceChildren(text);
} else {
node.replaceChildren(data.canvas);
}
}
async function processTile(tile) {
console.log("Selected tile", tile);
await Promise.all([
updateCanvas(document.getElementById("tile-original"), tile, tile.originalCacheKey),
updateCanvas(document.getElementById("tile-main"), tile, tile.cacheKey),
]);
}
viewer.addHandler('tile-invalidated', async event => {
if (event.tile === monitoredTile) {
await processTile(monitoredTile);
}
}, null, -Infinity); // as a last handler
// When testing code, you can call in OSD $.debugTile(message, tile) and it will log only for selected tiles on the canvas
OpenSeadragon.debugTile = function (msg, t) {
if (monitoredTile && monitoredTile.x === t.x && monitoredTile.y === t.y && monitoredTile.level === t.level) {
console.log(msg, t);
}
}
viewer.addHandler("canvas-release", e => {
const tiledImage = viewer.world.getItemAt(viewer.world.getItemCount()-1);
if (!tiledImage) {
monitoredTile = null;
return;
}
const position = viewer.viewport.windowToViewportCoordinates(e.position);
let tiles = tiledImage._lastDrawn;
for (let i = 0; i < tiles.length; i++) {
if (tiles[i].tile.bounds.containsPoint(position)) {
monitoredTile = tiles[i].tile;
return processTile(monitoredTile);
}
}
monitoredTile = null;
});

View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<!--
/*
* Modified and maintained by the OpenSeadragon Community.
*
* This software was orignally developed at the National Institute of Standards and
* Technology by employees of the Federal Government. NIST assumes
* no responsibility whatsoever for its use by other parties, and makes no
* guarantees, expressed or implied, about its quality, reliability, or
* any other characteristic.
*/
-->
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenSeadragon Filtering Plugin Demo</title>
<script type="text/javascript" src='/build/openseadragon/openseadragon.js'></script>
<!-- JQuery -->
<script src="/test/lib/jquery-1.9.1.min.js"></script>
<link rel="stylesheet" href="/test/lib/jquery-ui-1.10.2/css/smoothness/jquery-ui-1.10.2.min.css">
<script src="/test/lib/jquery-ui-1.10.2/js/jquery-ui-1.10.2.min.js"></script>
<!-- Local -->
<link rel="stylesheet" href="style.css">
<script src="plugin.js"></script>
<script src="/test/helpers/drawer-switcher.js"></script>
<!-- Thirdparty -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/camanjs/4.1.2/caman.full.min.js"></script>
</head>
<body>
<section class="home home-title" id="title-banner">
<h1>OpenSeadragon filtering plugin demo: <span id="title-drawer"></span></h1>
<p>You might want to check the plugin repository to see if the plugin code is up to date.</p>
</section>
<section class="demo">
<div class="wdzt-table-layout wdzt-full-width">
<div class="wdzt-row-layout">
<div class="wdzt-cell-layout column-2">
<div id="openseadragon"></div>
</div>
<div class="wdzt-cell-layout column-2">
<select id="image-select">
</select>
<h3>Available filters</h3>
<ul id="available">
</ul>
<h3>Selected filters</h3>
<ul id="selected"></ul>
<p>Drag and drop the selected filters to set their order.</p>
<br>
<label>
<input type="checkbox" onchange="viewer.setDebugMode(this.checked);">
Debug mode
</label>
</div>
</div>
</div>
</section>
<section class="monitoring">
Monitoring of a tile lifecycle: (use filters and click on a tile to start monitoring)
<div style="display: flex">
<div id="tile-original"></div>
<div id="tile-main"></div>
</div>
</section>
<script src="demo.js"></script>
</body>
</html>

View File

@ -0,0 +1,337 @@
/*
* Modified and maintained by the OpenSeadragon Community.
*
* This software was orignally developed at the National Institute of Standards and
* Technology by employees of the Federal Government. NIST assumes
* no responsibility whatsoever for its use by other parties, and makes no
* guarantees, expressed or implied, about its quality, reliability, or
* any other characteristic.
* @author Antoine Vandecreme <antoine.vandecreme@nist.gov>
*/
(function() {
'use strict';
const $ = window.OpenSeadragon;
if (!$) {
throw new Error('OpenSeadragon is missing.');
}
$.Viewer.prototype.setFilterOptions = function(options) {
if (!this.filterPluginInstance) {
options = options || {};
options.viewer = this;
this.filterPluginInstance = new $.FilterPlugin(options);
} else {
setOptions(this.filterPluginInstance, options);
}
};
/**
* @class FilterPlugin
* @param {Object} options The options
* @param {OpenSeadragon.Viewer} options.viewer The viewer to attach this
* plugin to.
* @param {Object[]} options.filters The filters to apply to the images.
* @param {OpenSeadragon.TiledImage[]} options.filters[x].items The tiled images
* on which to apply the filter.
* @param {function|function[]} options.filters[x].processors The processing
* function(s) to apply to the images. The parameter of this function is
* the context to modify.
*/
$.FilterPlugin = function(options) {
options = options || {};
if (!options.viewer) {
throw new Error('A viewer must be specified.');
}
const self = this;
this.viewer = options.viewer;
this.viewer.addHandler('tile-invalidated', applyFilters);
setOptions(this, options);
async function applyFilters(e) {
const tiledImage = e.tiledImage,
processors = getFiltersProcessors(self, tiledImage);
if (processors.length === 0) {
return;
}
const contextCopy = await e.getData('context2d');
if (!contextCopy) return;
for (let i = 0; i < processors.length; i++) {
if (e.outdated()) return;
await processors[i](contextCopy);
}
if (e.outdated()) return;
await e.setData(contextCopy, 'context2d');
}
};
function setOptions(instance, options) {
options = options || {};
const filters = options.filters;
instance.filters = !filters ? [] :
$.isArray(filters) ? filters : [filters];
for (let i = 0; i < instance.filters.length; i++) {
const filter = instance.filters[i];
if (!filter.processors) {
throw new Error('Filter processors must be specified.');
}
filter.processors = $.isArray(filter.processors) ?
filter.processors : [filter.processors];
}
instance.viewer.requestInvalidate();
}
function getFiltersProcessors(instance, item) {
if (instance.filters.length === 0) {
return [];
}
let globalProcessors = null;
for (let i = 0; i < instance.filters.length; i++) {
const filter = instance.filters[i];
if (!filter.items) {
globalProcessors = filter.processors;
} else if (filter.items === item ||
$.isArray(filter.items) && filter.items.indexOf(item) >= 0) {
return filter.processors;
}
}
return globalProcessors ? globalProcessors : [];
}
$.Filters = {
THRESHOLDING: function(threshold) {
if (threshold < 0 || threshold > 255) {
throw new Error('Threshold must be between 0 and 255.');
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const v = (r + g + b) / 3;
pixels[i] = pixels[i + 1] = pixels[i + 2] =
v < threshold ? 0 : 255;
}
context.putImageData(imgData, 0, 0);
};
},
BRIGHTNESS: function(adjustment) {
if (adjustment < -255 || adjustment > 255) {
throw new Error(
'Brightness adjustment must be between -255 and 255.');
}
const precomputedBrightness = [];
for (let i = 0; i < 256; i++) {
precomputedBrightness[i] = i + adjustment;
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = precomputedBrightness[pixels[i]];
pixels[i + 1] = precomputedBrightness[pixels[i + 1]];
pixels[i + 2] = precomputedBrightness[pixels[i + 2]];
}
context.putImageData(imgData, 0, 0);
};
},
CONTRAST: function(adjustment) {
if (adjustment < 0) {
throw new Error('Contrast adjustment must be positive.');
}
const precomputedContrast = [];
for (let i = 0; i < 256; i++) {
precomputedContrast[i] = i * adjustment;
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = precomputedContrast[pixels[i]];
pixels[i + 1] = precomputedContrast[pixels[i + 1]];
pixels[i + 2] = precomputedContrast[pixels[i + 2]];
}
context.putImageData(imgData, 0, 0);
};
},
GAMMA: function(adjustment) {
if (adjustment < 0) {
throw new Error('Gamma adjustment must be positive.');
}
const precomputedGamma = [];
for (let i = 0; i < 256; i++) {
precomputedGamma[i] = Math.pow(i / 255, adjustment) * 255;
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = precomputedGamma[pixels[i]];
pixels[i + 1] = precomputedGamma[pixels[i + 1]];
pixels[i + 2] = precomputedGamma[pixels[i + 2]];
}
context.putImageData(imgData, 0, 0);
};
},
GREYSCALE: function() {
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
const val = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = val;
pixels[i + 1] = val;
pixels[i + 2] = val;
}
context.putImageData(imgData, 0, 0);
};
},
INVERT: function() {
const precomputedInvert = [];
for (let i = 0; i < 256; i++) {
precomputedInvert[i] = 255 - i;
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pixels = imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = precomputedInvert[pixels[i]];
pixels[i + 1] = precomputedInvert[pixels[i + 1]];
pixels[i + 2] = precomputedInvert[pixels[i + 2]];
}
context.putImageData(imgData, 0, 0);
};
},
MORPHOLOGICAL_OPERATION: function(kernelSize, comparator) {
if (kernelSize % 2 === 0) {
throw new Error('The kernel size must be an odd number.');
}
const kernelHalfSize = Math.floor(kernelSize / 2);
if (!comparator) {
throw new Error('A comparator must be defined.');
}
return function(context) {
const width = context.canvas.width;
const height = context.canvas.height;
const imgData = context.getImageData(0, 0, width, height);
const originalPixels = context.getImageData(0, 0, width, height)
.data;
let offset;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
offset = (y * width + x) * 4;
let r = originalPixels[offset],
g = originalPixels[offset + 1],
b = originalPixels[offset + 2];
for (let j = 0; j < kernelSize; j++) {
for (let i = 0; i < kernelSize; i++) {
const pixelX = x + i - kernelHalfSize;
const pixelY = y + j - kernelHalfSize;
if (pixelX >= 0 && pixelX < width &&
pixelY >= 0 && pixelY < height) {
offset = (pixelY * width + pixelX) * 4;
r = comparator(originalPixels[offset], r);
g = comparator(
originalPixels[offset + 1], g);
b = comparator(
originalPixels[offset + 2], b);
}
}
}
imgData.data[offset] = r;
imgData.data[offset + 1] = g;
imgData.data[offset + 2] = b;
}
}
context.putImageData(imgData, 0, 0);
};
},
CONVOLUTION: function(kernel) {
if (!$.isArray(kernel)) {
throw new Error('The kernel must be an array.');
}
const kernelSize = Math.sqrt(kernel.length);
if ((kernelSize + 1) % 2 !== 0) {
throw new Error('The kernel must be a square matrix with odd' +
'width and height.');
}
const kernelHalfSize = (kernelSize - 1) / 2;
return function(context) {
const width = context.canvas.width;
const height = context.canvas.height;
const imgData = context.getImageData(0, 0, width, height);
const originalPixels = context.getImageData(0, 0, width, height)
.data;
let offset;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let j = 0; j < kernelSize; j++) {
for (let i = 0; i < kernelSize; i++) {
const pixelX = x + i - kernelHalfSize;
const pixelY = y + j - kernelHalfSize;
if (pixelX >= 0 && pixelX < width &&
pixelY >= 0 && pixelY < height) {
offset = (pixelY * width + pixelX) * 4;
const weight = kernel[j * kernelSize + i];
r += originalPixels[offset] * weight;
g += originalPixels[offset + 1] * weight;
b += originalPixels[offset + 2] * weight;
}
}
}
offset = (y * width + x) * 4;
imgData.data[offset] = r;
imgData.data[offset + 1] = g;
imgData.data[offset + 2] = b;
}
}
context.putImageData(imgData, 0, 0);
};
},
COLORMAP: function(cmap, ctr) {
const resampledCmap = cmap.slice(0);
const diff = 255 - ctr;
for (let i = 0; i < 256; i++) {
let position = i > ctr ?
Math.min((i - ctr) / diff * 128 + 128,255) | 0 :
Math.max(0, i / (ctr / 128)) | 0;
resampledCmap[i] = cmap[position];
}
return function(context) {
const imgData = context.getImageData(
0, 0, context.canvas.width, context.canvas.height);
const pxl = imgData.data;
for (let i = 0; i < pxl.length; i += 4) {
const v = (pxl[i] + pxl[i + 1] + pxl[i + 2]) / 3 | 0;
const c = resampledCmap[v];
pxl[i] = c[0];
pxl[i + 1] = c[1];
pxl[i + 2] = c[2];
}
context.putImageData(imgData, 0, 0);
};
}
};
}());

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,81 @@
/*
* Modified and maintained by the OpenSeadragon Community.
*
* This software was orignally developed at the National Institute of Standards and
* Technology by employees of the Federal Government. NIST assumes
* no responsibility whatsoever for its use by other parties, and makes no
* guarantees, expressed or implied, about its quality, reliability, or
* any other characteristic.
*/
.demo {
line-height: normal;
}
.demo h3 {
margin-top: 5px;
margin-bottom: 5px;
}
#openseadragon {
width: 100%;
height: 700px;
background-color: black;
}
.wdzt-table-layout {
display: table;
}
.wdzt-row-layout {
display: table-row;
}
.wdzt-cell-layout {
display: table-cell;
}
.wdzt-full-width {
width: 100%;
}
.wdzt-menu-slider {
margin-left: 10px;
margin-right: 10px;
}
.column-2 {
width: 50%;
vertical-align: top;
padding: 3px;
}
#available {
list-style-type: none;
}
ul {
padding: 0;
border: 1px solid black;
min-height: 25px;
}
li {
padding: 3px;
}
#selected {
list-style-type: none;
}
.button {
cursor: pointer;
vertical-align: text-top;
}
.filterLabel {
min-width: 120px;
}
#selected .filterLabel {
cursor: move;
}

View File

@ -59,7 +59,8 @@
this.viewer = OpenSeadragon({
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
tileSources: tileSources
tileSources: tileSources,
crossOriginPolicy: 'Anonymous',
});
this.viewer.addHandler('open', function() {

View File

@ -109,14 +109,14 @@
opacity: getOpacity( layerName )
};
var addLayerHandler = function( event ) {
if ( event.options === options ) {
viewer.removeHandler( "add-layer", addLayerHandler );
layers[layerName] = event.drawer;
if ( event.item.source.levels[0].url.includes(layerName) ) {
viewer.world.removeHandler( "add-item", addLayerHandler );
layers[layerName] = event.item;
updateOrder();
}
};
viewer.addHandler( "add-layer", addLayerHandler );
viewer.addLayer( options );
viewer.world.addHandler( "add-item", addLayerHandler );
viewer.addTiledImage( options );
}
function left() {
@ -146,13 +146,15 @@
}
function updateOrder() {
var nbLayers = viewer.getLayersCount();
var nbLayers = viewer.world.getItemCount();
if ( nbLayers < 2 ) {
return;
}
$.each( $( "#used select option" ), function( index, value ) {
var layer = value.innerHTML;
viewer.setLayerLevel( layers[layer], nbLayers -1 - index );
if (layers[layer]) {
viewer.world.setItemIndex( layers[layer], nbLayers -1 - index );
}
} );
}

View File

@ -26,7 +26,7 @@
prefixUrl: "../../build/openseadragon/images/",
tileSources: "https://openseadragon.github.io/example-images/duomo/duomo.dzi",
showNavigator:true,
debugMode:true,
crossOriginPolicy: 'Anonymous',
maxTilesPerFrame:3,
});

View File

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenSeadragon Filtering Plugin Demo</title>
<script type="text/javascript" src='/build/openseadragon/openseadragon.js'></script>
<style>
textarea {
width: 900px;
padding: 10px;
font-size: 16px;
font-family: 'Courier New', Courier, monospace;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
background-color: #fff;
resize: vertical;
display: block;
max-width: 90%;
}
button {
margin-top: 10px;
padding: 10px 20px;
font-size: 16px;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
button:hover {
background-color: #0056b3;
display: block;
}
</style>
<script>
let _pA, _pB;
function executeScript() {
const scriptContent = document.getElementById("scriptInput").value;
try {
eval(scriptContent);
if (! (typeof window.pluginA === "function") || ! (typeof window.pluginB === "function")) {
alert("pluginA and pluginB functions must be defined!");
return;
}
if (_pA) {
viewer.removeHandler('tile-invalidated', _pA);
}
if (_pB) {
viewer.removeHandler('tile-invalidated', _pB);
}
_pA = window.pluginA;
_pB = window.pluginB;
viewer.addHandler('tile-invalidated', _pA, null, window.orderPluginA || 0);
viewer.addHandler('tile-invalidated', _pB, null, window.orderPluginB || 0);
viewer.requestInvalidate();
} catch (error) {
alert("Error executing script: " + error.message);
}
}
</script>
<!-- JQuery -->
<script src="/test/lib/jquery-1.9.1.min.js"></script>
<link rel="stylesheet" href="/test/lib/jquery-ui-1.10.2/css/smoothness/jquery-ui-1.10.2.min.css">
<script src="/test/lib/jquery-ui-1.10.2/js/jquery-ui-1.10.2.min.js"></script>
</head>
<body>
<section class="home home-title" id="title-banner">
<h1>OpenSeadragon plugin demo</h1>
<p>You should see two plugins interacting. You can change the order of plugins and the logics!</p>
</section>
<div style="display: flex; flex-direction: row; flex-wrap: wrap;">
<section class="demo" id="demo" style="width: 800px; height: 600px"></section>
<div>
<textarea id="scriptInput" rows="25" cols="120" placeholder="" style="height: 470px">
// window.pluginA must be defined! draw small gradient square
window.pluginA = async function(e) {
const ctx = await e.getData('context2d');
if (ctx) {
const gradient = ctx.createLinearGradient(0, 0, 50, 50);
gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.5, 'green');
gradient.addColorStop(1, 'red');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 50, 50);
await e.setData(ctx, 'context2d');
}
};
// window.pluginB must be defined! overlay with color opacity 40%
window.pluginB = async function(e) {
const ctx = await e.getData('context2d');
if (ctx) {
const canvas = ctx.canvas;
ctx.fillStyle = "rgba(156, 0, 26, 0.4)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
await e.setData(ctx, 'context2d');
}
};
// higher number = earlier execution
window.orderPluginA = 1;
window.orderPluginB = 0;
</textarea>
<textarea style="height: 120px; background-color: #e5e5e5" disabled>
// Application of the plugins done automatically:
viewer.addHandler('tile-invalidated', window.pluginA, null, window.orderPluginA);
viewer.addHandler('tile-invalidated', window.pluginB, null, window.orderPluginB);
viewer.requestInvalidate();
</textarea>
<button onclick="executeScript()">Apply</button>
</div>
</div>
<script>
const viewer = window.viewer = OpenSeadragon({
id: "demo",
prefixUrl: "../../build/openseadragon/images/",
tileSources: "https://openseadragon.github.io/example-images/duomo/duomo.dzi",
drawer: 'webgl',
crossOriginPolicy: 'Anonymous',
wrapHorizontal: true,
showNavigator: true,
});
</script>
</body>
</html>

View File

@ -25,8 +25,9 @@
// debugMode: true,
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
tileSources: "http://wellcomelibrary.org/iiif-img/b11768265-0/a6801943-b8b4-4674-908c-7d5b27e70569/info.json",
tileSources: "https://openseadragon.github.io/example-images/highsmith/highsmith.dzi",
showNavigator:true,
crossOriginPolicy: 'Anonymous',
timeout: 0
});

View File

@ -25,8 +25,9 @@
// debugMode: true,
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
tileSources: "http://wellcomelibrary.org/iiif-img/b11768265-0/a6801943-b8b4-4674-908c-7d5b27e70569/info.json",
tileSources: "https://openseadragon.github.io/example-images/highsmith/highsmith.dzi",
showNavigator:true,
crossOriginPolicy: 'Anonymous',
timeout: 1000 * 60 * 60 * 24
});

View File

@ -0,0 +1,76 @@
/**
* Ability to switch between different drawers.
* Usage: with two viewers, we would do
*
* const switcher = new DrawerSwitcher();
* switcher.addDrawerOption("drawer_left", "Select drawer for the left viewer", "canvas");
* switcher.addDrawerOption("drawer_right", "Select drawer for the right viewer", "webgl");
* const viewer1 = window.viewer1 = new OpenSeadragon({
* id: 'openseadragon',
* ...
* drawer:switcher.activeImplementation("drawer_left"),
* });
* $("#my-title-for-left-drawer").html(`Viewer using drawer ${switcher.activeName("drawer_left")}`);
* $("#container").html(switcher.render());
* // OR switcher.render("#container")
* // ..do the same for the second viewer
*/
class DrawerSwitcher {
url = new URL(window.location.href);
drawers = {
canvas: "Context2d drawer (default in OSD &lt;= 4.1.0)",
webgl: "New WebGL drawer"
};
_data = {}
addDrawerOption(urlQueryName, title="Select drawer:", defaultDrawerImplementation="canvas") {
const drawer = this.url.searchParams.get(urlQueryName) || defaultDrawerImplementation;
if (!this.drawers[drawer]) throw "Unsupported drawer implementation: " + drawer;
let context = this._data[urlQueryName] = {
query: urlQueryName,
implementation: drawer,
title: title
};
}
activeName(urlQueryName) {
return this.drawers[this.activeImplementation(urlQueryName)];
}
activeImplementation(urlQueryName) {
return this._data[urlQueryName].implementation;
}
_getFormData(useNewline=true) {
return Object.values(this._data).map(ctx => `${ctx.title}&nbsp;
<select name="${ctx.query}">
${Object.entries(this.drawers).map(([k, v]) => {
const selected = ctx.implementation === k ? "selected" : "";
return `<option value="${k}" ${selected}>${v}</option>`;
}).join("\n")}
</select>`).join(useNewline ? "<br>" : "");
}
_preserveOtherSeachParams() {
let res = [], registered = Object.keys(this._data);
for (let [k, v] of this.url.searchParams.entries()) {
if (!registered.includes(k)) {
res.push(`<input name="${k}" type="hidden" value=${v} />`);
}
}
return res.join('\n');
}
render(selector, useNewline=undefined) {
useNewline = typeof useNewline === "boolean" ? useNewline : Object.keys(this._data).length > 1;
const html = `<div>
<form method="get">
${this._preserveOtherSeachParams()}
${this._getFormData()}${useNewline ? "<br>":""}<button>Submit</button>
</form>
</div>`;
if (selector) $(selector).append(html);
return html;
}
}

95
test/helpers/mocks.js Normal file
View File

@ -0,0 +1,95 @@
// Test-wide mocks for more test stability: tests might require calling functions that expect
// presence of certain mock properties. It is better to include maintened mock props than to copy
// over all the place
window.MockSeadragon = {
/**
* Get mocked tile: loaded state, cutoff such that it is not kept in cache by force,
* level: 1, x: 0, y: 0, all coords: [x0 y0 w0 h0]
*
* Requires TiledImage referece (mock or real)
* @return {OpenSeadragon.Tile}
*/
getTile(url, tiledImage, props={}) {
const dummyRect = new OpenSeadragon.Rect(0, 0, 0, 0, 0);
//default cutoof = 0 --> use level 1 to not to keep caches from unloading (cutoff = navigator data, kept in cache)
const dummyTile = new OpenSeadragon.Tile(1, 0, 0, dummyRect, true, url,
undefined, true, null, dummyRect, null, url);
dummyTile.tiledImage = tiledImage;
//by default set as ready
dummyTile.loaded = true;
dummyTile.loading = false;
//override anything we need
OpenSeadragon.extend(tiledImage, props);
return dummyTile;
},
/**
* Get mocked viewer: it has not all props that might be required. If your
* tests fails because they do not find some props on a viewer, add them here.
*
* Requires a drawer reference (mock or real). Automatically created if not provided.
* @return {OpenSeadragon.Viewer}
*/
getViewer(drawer=null, props={}) {
drawer = drawer || this.getDrawer();
return OpenSeadragon.extend(new class extends OpenSeadragon.EventSource {
forceRedraw () {}
drawer = drawer
tileCache = new OpenSeadragon.TileCache()
}, props);
},
/**
* Get mocked viewer: it has not all props that might be required. If your
* tests fails because they do not find some props on a viewer, add them here.
* @return {OpenSeadragon.Viewer}
*/
getDrawer(props={}) {
return OpenSeadragon.extend({
getType: function () {
return "mock";
}
}, props);
},
/**
* Get mocked tiled image: it has not all props that might be required. If your
* tests fails because they do not find some props on a tiled image, add them here.
*
* Requires viewer reference (mock or real). Automatically created if not provided.
* @return {OpenSeadragon.TiledImage}
*/
getTiledImage(viewer=null, props={}) {
viewer = viewer || this.getViewer();
return OpenSeadragon.extend({
viewer: viewer,
source: OpenSeadragon.TileSource.prototype,
redraw: function() {},
_tileCache: viewer.tileCache
}, props);
},
/**
* Get mocked tile source
* @return {OpenSeadragon.TileSource}
*/
getTileSource(props={}) {
return new OpenSeadragon.TileSource(OpenSeadragon.extend({
width: 1500,
height: 1000,
tileWidth: 200,
tileHeight: 150,
tileOverlap: 0
}, props));
},
/**
* Get mocked cache record
* @return {OpenSeadragon.CacheRecord}
*/
getCacheRecord(props={}) {
return OpenSeadragon.extend(new OpenSeadragon.CacheRecord(), props);
}
};

View File

@ -180,12 +180,45 @@
}
};
// OSD has circular references, if a console log tries to serialize
// certain object, remove these references from a clone (do not delete prop
// on the original object).
// NOTE: this does not work if someone replaces the original class with
// a mock object! Try to mock functions only, or ensure mock objects
// do not hold circular references.
const circularOSDReferences = {
'Tile': 'tiledImage',
'CacheRecord': ['_tRef', '_tiles'],
'World': 'viewer',
'DrawerBase': ['viewer', 'viewport'],
'CanvasDrawer': ['viewer', 'viewport'],
'WebGLDrawer': ['viewer', 'viewport'],
'TiledImage': ['viewer', '_drawer'],
};
for ( var i in testLog ) {
if ( testLog.hasOwnProperty( i ) && testLog[i].push ) {
// Circular reference removal
const osdCircularStructureReplacer = function (key, value) {
for (let ClassType in circularOSDReferences) {
if (value instanceof OpenSeadragon[ClassType]) {
const instance = {};
Object.assign(instance, value);
let circProps = circularOSDReferences[ClassType];
if (!Array.isArray(circProps)) circProps = [circProps];
for (let prop of circProps) {
instance[prop] = '__circular_reference__';
}
return instance;
}
}
return value;
};
testConsole[i] = ( function ( arr ) {
return function () {
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, osdCircularStructureReplacer ) ); // Store as JSON to avoid tedious array-equality tests
};
} )( testLog[i] );
@ -207,5 +240,29 @@
};
OpenSeadragon.console = testConsole;
OpenSeadragon.getBuiltInDrawersForTest = function() {
const drawers = [];
for (let property in OpenSeadragon) {
const drawer = OpenSeadragon[ property ],
proto = drawer.prototype;
if( proto &&
proto instanceof OpenSeadragon.DrawerBase &&
$.isFunction( proto.getType )){
drawers.push(proto.getType.call( drawer ));
}
}
return drawers;
};
OpenSeadragon.Viewer.prototype.waitForFinishedJobsForTest = function () {
let finish;
let int = setInterval(() => {
if (this.imageLoader.jobsInProgress < 1) {
finish();
}
}, 50);
return new OpenSeadragon.Promise((resolve) => finish = resolve);
};
} )();

View File

@ -81,7 +81,7 @@
tileExists: function ( level, x, y ) {
return true;
}
},
});
var Loader = function(options) {
@ -97,7 +97,9 @@
OriginalLoader.prototype.addJob.apply(this, [options]);
} else {
//no ajax means we would wait for invalid image link to load, close - passed
viewer.close();
setTimeout(() => {
viewer.close();
});
}
}
});
@ -139,7 +141,9 @@
//first AJAX firing is the image info getter, second is the first tile request: can exit
ajaxCounter++;
if (ajaxCounter > 1) {
viewer.close();
setTimeout(() => {
viewer.close();
});
return null;
}
@ -184,33 +188,34 @@
});
var failHandler = function (event) {
testPostData(event.postData, "event: 'open-failed'");
viewer.removeHandler('open-failed', failHandler);
viewer.close();
ASSERT.ok(false, 'Open-failed shoud not be called. We have custom function of fetching the data that succeeds.');
};
viewer.addHandler('open-failed', failHandler);
var readyHandler = function (event) {
//relies on Tilesource contructor extending itself with options object
testPostData(event.postData, "event: 'ready'");
viewer.removeHandler('ready', readyHandler);
};
viewer.addHandler('ready', readyHandler);
var openHandlerCalled = false;
var openHandler = function(event) {
viewer.removeHandler('open', openHandler);
ASSERT.ok(true, 'Open event was sent');
openHandlerCalled = true;
};
var readyHandler = function (event) {
testPostData(event.item.source.getTilePostData(0, 0, 0), "event: 'add-item'");
viewer.world.removeHandler('add-item', readyHandler);
viewer.addHandler('close', closeHandler);
viewer.world.draw();
};
var closeHandler = function(event) {
ASSERT.ok(openHandlerCalled, 'Open event was sent.');
viewer.removeHandler('close', closeHandler);
$('#example').empty();
ASSERT.ok(true, 'Close event was sent');
timeWatcher.done();
};
//make sure we call add-item before the system default 0 priority, it fires download on tiles and removes
// which calls internally viewer.close
viewer.world.addHandler('add-item', readyHandler, null, Infinity);
viewer.addHandler('open', openHandler);
};

View File

@ -49,7 +49,8 @@
loadTilesWithAjax: true,
ajaxHeaders: {
'X-Viewer-Header': 'ViewerHeaderValue'
}
},
callTileLoadedWithCachedData: true
});
},
afterEach: function() {

View File

@ -223,50 +223,50 @@
viewer.open('/test/data/testpattern.dzi');
});
// TODO: can this be enabled without breaking tests due to lack of short-duration user interaction?
// QUnit.test('FullScreen', function(assert) {
// const done = assert.async();
// if (!OpenSeadragon.supportsFullScreen) {
// assert.expect(0);
// done();
// return;
// }
QUnit.test('FullScreen', function(assert) {
if (!OpenSeadragon.supportsFullScreen) {
const done = assert.async();
assert.expect(0);
done();
return;
}
var timeWatcher = Util.timeWatcher(assert, 7000);
// viewer.addHandler('open', function () {
// assert.ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');
viewer.addHandler('open', function () {
assert.ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');
// const checkEnteringPreFullScreen = (event) => {
// viewer.removeHandler('pre-full-screen', checkEnteringPreFullScreen);
// assert.ok(event.fullScreen, 'Switching to fullscreen');
// assert.ok(!OpenSeadragon.isFullScreen(), 'Not yet fullscreen');
// };
const checkEnteringPreFullScreen = (event) => {
viewer.removeHandler('pre-full-screen', checkEnteringPreFullScreen);
assert.ok(event.fullScreen, 'Switching to fullscreen');
assert.ok(!OpenSeadragon.isFullScreen(), 'Not yet fullscreen');
};
// const checkExitingFullScreen = (event) => {
// viewer.removeHandler('full-screen', checkExitingFullScreen);
// assert.ok(!event.fullScreen, 'Disabling fullscreen');
// assert.ok(!OpenSeadragon.isFullScreen(), 'Fullscreen disabled');
// done();
// }
const checkExitingFullScreen = (event) => {
viewer.removeHandler('full-screen', checkExitingFullScreen);
assert.ok(!event.fullScreen, 'Disabling fullscreen');
assert.ok(!OpenSeadragon.isFullScreen(), 'Fullscreen disabled');
timeWatcher.done();
}
// // The 'new' headless mode allows us to enter fullscreen, so verify
// // that we see the correct values returned. We will then close out
// // of fullscreen to check the same values when exiting.
// const checkAcquiredFullScreen = (event) => {
// viewer.removeHandler('full-screen', checkAcquiredFullScreen);
// viewer.addHandler('full-screen', checkExitingFullScreen);
// assert.ok(event.fullScreen, 'Acquired fullscreen');
// assert.ok(OpenSeadragon.isFullScreen(), 'Fullscreen enabled');
// viewer.setFullScreen(false);
// };
// The 'new' headless mode allows us to enter fullscreen, so verify
// that we see the correct values returned. We will then close out
// of fullscreen to check the same values when exiting.
const checkAcquiredFullScreen = (event) => {
viewer.removeHandler('full-screen', checkAcquiredFullScreen);
viewer.addHandler('full-screen', checkExitingFullScreen);
assert.ok(event.fullScreen, 'Acquired fullscreen');
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.addHandler('pre-full-screen', checkEnteringPreFullScreen);
viewer.addHandler('full-screen', checkAcquiredFullScreen);
viewer.setFullScreen(true);
});
// viewer.addHandler('pre-full-screen', checkEnteringPreFullScreen);
// viewer.addHandler('full-screen', checkAcquiredFullScreen);
// viewer.setFullScreen(true);
// });
// viewer.open('/test/data/testpattern.dzi');
// });
viewer.open('/test/data/testpattern.dzi');
});
QUnit.test('Close', function(assert) {
var done = assert.async();
@ -332,9 +332,10 @@
} ]
} );
viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas),
"Canvas should be tainted.");
done();
event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
assert.ok(OpenSeadragon.isCanvasTainted(context.canvas),
"Canvas should be tainted.")
).then(done);
});
} );
@ -352,9 +353,10 @@
} ]
} );
viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(!OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas),
"Canvas should not be tainted.");
done();
event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
assert.notOk(OpenSeadragon.isCanvasTainted(context.canvas),
"Canvas should be tainted.")
).then(done);
});
} );
@ -376,9 +378,10 @@
crossOriginPolicy : false
} );
viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas),
"Canvas should be tainted.");
done();
event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
assert.ok(OpenSeadragon.isCanvasTainted(context.canvas),
"Canvas should be tainted.")
).then(done);
});
} );
@ -400,9 +403,10 @@
}
} );
viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(!OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas),
"Canvas should not be tainted.");
done();
event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
assert.notOk(OpenSeadragon.isCanvasTainted(context.canvas),
"Canvas should be tainted.")
).then(done);
});
} );

View File

@ -0,0 +1,260 @@
/* global QUnit, testLog */
(function() {
let viewer;
QUnit.module(`Data Manipulation Across Drawers`, {
beforeEach: function () {
$('<div id="example"></div>').appendTo("#qunit-fixture");
testLog.reset();
},
afterEach: function () {
if (viewer && viewer.close) {
viewer.close();
}
viewer = null;
}
});
const PROMISE_REF_KEY = Symbol("_private_test_ref");
OpenSeadragon.getBuiltInDrawersForTest().forEach(testDrawer);
// If you want to debug a specific drawer, use instead:
// ['webgl'].forEach(testDrawer);
function getPluginCode(overlayColor = "rgba(0,0,255,0.5)") {
return async function(e) {
const ctx = await e.getData('context2d');
if (ctx) {
const canvas = ctx.canvas;
ctx.fillStyle = overlayColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
await e.setData(ctx, 'context2d');
}
};
}
function getResetTileDataCode() {
return async function(e) {
e.resetData();
};
}
function getTileDescription(t) {
return `${t.level}/${t.x}-${t.y}`;
}
function testDrawer(type) {
function whiteViewport() {
viewer = OpenSeadragon({
id: 'example',
prefixUrl: '/build/openseadragon/images/',
maxImageCacheCount: 200,
springStiffness: 100,
drawer: type
});
viewer.open({
width: 24,
height: 24,
tileSize: 24,
minLevel: 1,
// This is a crucial test feature: all tiles share the same URL, so there are plenty collisions
getTileUrl: (x, y, l) => "",
getTilePostData: () => "",
downloadTileStart: (context) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = context.tile.size.x;
canvas.height = context.tile.size.y;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, context.tile.size.x, context.tile.size.y);
context.finish(ctx, null, "context2d");
}
});
// Get promise reference to wait for tile ready
viewer.addHandler('tile-loaded', e => {
e.tile[PROMISE_REF_KEY] = e.promise;
});
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// we test middle of the canvas, so that we can test both tiles or the output canvas of canvas drawer :)
async function readTileData(tileRef = null) {
// Get some time for viewer to load data
await sleep(50);
// make sure at least one tile loaded
const tile = tileRef || viewer.world.getItemAt(0).getTilesToDraw()[0];
await tile[PROMISE_REF_KEY];
// Get some time for viewer to load data
await sleep(50);
if (type === "canvas") {
//test with the underlying canvas instead
const canvas = viewer.drawer.canvas;
return viewer.drawer.canvas.getContext("2d").getImageData(canvas.width/2, canvas.height/2, 1, 1);
}
//else incompatible drawer for data getting
const cache = tile.tile.getCache();
if (!cache || !cache.loaded) return null;
const ctx = await cache.getDataAs("context2d");
if (!ctx) return null;
return ctx.getImageData(ctx.canvas.width/2, ctx.canvas.height/2, 1, 1)
}
QUnit.test(type + ' drawer: basic scenario.', function(assert) {
whiteViewport();
const done = assert.async();
const fnA = getPluginCode("rgba(0,0,255,1)");
const fnB = getPluginCode("rgba(255,0,0,1)");
viewer.addHandler('tile-invalidated', fnA);
viewer.addHandler('tile-invalidated', fnB);
viewer.addHandler('open', async () => {
await viewer.waitForFinishedJobsForTest();
let data = await readTileData();
assert.equal(data.data[0], 255);
assert.equal(data.data[1], 0);
assert.equal(data.data[2], 0);
assert.equal(data.data[3], 255);
// Thorough testing of the cache state
for (let tile of viewer.tileCache._tilesLoaded) {
await tile[PROMISE_REF_KEY]; // to be sure all tiles has finished before checking
const caches = Object.entries(tile._caches);
assert.equal(caches.length, 2, `Tile ${getTileDescription(tile)} has only two caches - main & original`);
for (let [key, value] of caches) {
assert.ok(value.loaded, `Attached cache '${key}' is ready.`);
assert.notOk(value._destroyed, `Attached cache '${key}' is not destroyed.`);
assert.ok(value._tiles.includes(tile), `Attached cache '${key}' reference is bidirectional.`);
}
}
done();
});
});
QUnit.test(type + ' drawer: basic scenario with priorities + events addition.', function(assert) {
whiteViewport();
const done = assert.async();
// FNA gets applied last since it has low priority
const fnA = getPluginCode("rgba(0,0,255,1)");
const fnB = getPluginCode("rgba(255,0,0,1)");
viewer.addHandler('tile-invalidated', fnA);
viewer.addHandler('tile-invalidated', fnB, null, 1);
// const promise = viewer.requestInvalidate();
viewer.addHandler('open', async () => {
await viewer.waitForFinishedJobsForTest();
let data = await readTileData();
assert.equal(data.data[0], 0);
assert.equal(data.data[1], 0);
assert.equal(data.data[2], 255);
assert.equal(data.data[3], 255);
// Test swap
viewer.addHandler('tile-invalidated', fnB);
await viewer.requestInvalidate();
data = await readTileData();
// suddenly B is applied since it was added with same priority but later
assert.equal(data.data[0], 255);
assert.equal(data.data[1], 0);
assert.equal(data.data[2], 0);
assert.equal(data.data[3], 255);
// Now B gets applied last! Red
viewer.addHandler('tile-invalidated', fnB, null, -1);
await viewer.requestInvalidate();
// no change
data = await readTileData();
assert.equal(data.data[0], 255);
assert.equal(data.data[1], 0);
assert.equal(data.data[2], 0);
assert.equal(data.data[3], 255);
// Thorough testing of the cache state
for (let tile of viewer.tileCache._tilesLoaded) {
await tile[PROMISE_REF_KEY]; // to be sure all tiles has finished before checking
const caches = Object.entries(tile._caches);
assert.equal(caches.length, 2, `Tile ${getTileDescription(tile)} has only two caches - main & original`);
for (let [key, value] of caches) {
assert.ok(value.loaded, `Attached cache '${key}' is ready.`);
assert.notOk(value._destroyed, `Attached cache '${key}' is not destroyed.`);
assert.ok(value._tiles.includes(tile), `Attached cache '${key}' reference is bidirectional.`);
}
}
done();
});
});
QUnit.test(type + ' drawer: one calls tile restore.', function(assert) {
whiteViewport();
const done = assert.async();
const fnA = getPluginCode("rgba(0,255,0,1)");
const fnB = getResetTileDataCode();
viewer.addHandler('tile-invalidated', fnA);
viewer.addHandler('tile-invalidated', fnB, null, 1);
// const promise = viewer.requestInvalidate();
viewer.addHandler('open', async () => {
await viewer.waitForFinishedJobsForTest();
let data = await readTileData();
assert.equal(data.data[0], 0);
assert.equal(data.data[1], 255);
assert.equal(data.data[2], 0);
assert.equal(data.data[3], 255);
// Test swap - suddenly B applied since it was added later
viewer.addHandler('tile-invalidated', fnB);
await viewer.requestInvalidate();
data = await readTileData();
assert.equal(data.data[0], 255);
assert.equal(data.data[1], 255);
assert.equal(data.data[2], 255);
assert.equal(data.data[3], 255);
viewer.addHandler('tile-invalidated', fnB, null, -1);
await viewer.requestInvalidate();
data = await readTileData();
//Erased!
assert.equal(data.data[0], 255);
assert.equal(data.data[1], 255);
assert.equal(data.data[2], 255);
assert.equal(data.data[3], 255);
// Thorough testing of the cache state
for (let tile of viewer.tileCache._tilesLoaded) {
await tile[PROMISE_REF_KEY]; // to be sure all tiles has finished before checking
const caches = Object.entries(tile._caches);
assert.equal(caches.length, 1, `Tile ${getTileDescription(tile)} has only single, original cache`);
for (let [key, value] of caches) {
assert.ok(value.loaded, `Attached cache '${key}' is ready.`);
assert.notOk(value._destroyed, `Attached cache '${key}' is not destroyed.`);
assert.ok(value._tiles.includes(tile), `Attached cache '${key}' reference is bidirectional.`);
}
}
done();
});
});
}
}());

View File

@ -2,8 +2,7 @@
(function() {
var viewer;
const drawerTypes = ['webgl','canvas','html'];
drawerTypes.forEach(runDrawerTests);
OpenSeadragon.getBuiltInDrawersForTest().forEach(runDrawerTests);
function runDrawerTests(drawerType){

View File

@ -33,10 +33,14 @@
}
}
function runTest(e) {
function runTest(e, async=false) {
context.raiseEvent(eName, e);
}
function runTestAwaiting(e, async=false) {
context.raiseEventAwaiting(eName, e);
}
QUnit.module( 'EventSource', {
beforeEach: function () {
context = new OpenSeadragon.EventSource();
@ -82,4 +86,58 @@
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.'
});
});
} )();

View File

@ -2,6 +2,7 @@
(function () {
var viewer;
var sleep = time => new Promise(res => setTimeout(res, time));
QUnit.module( 'Events', {
beforeEach: function () {
@ -1222,11 +1223,12 @@
var tile = event.tile;
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() {
//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.ok( tile.loaded, "The tile should be marked as loaded.");
done();
}, 0);
});
}
viewer.addHandler( 'tile-loaded', tileLoaded);
@ -1238,72 +1240,85 @@
function tileLoaded ( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
var tile = event.tile;
var callback = event.getCompletionCallback();
assert.ok( tile.loading, "The tile should be marked as loading.");
assert.notOk( tile.loaded, "The tile should not be marked as loaded.");
assert.ok( callback, "The event should have a callback.");
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();
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();
}, 0);
});
}
viewer.addHandler( 'tile-loaded', tileLoaded);
viewer.open( '/test/data/testpattern.dzi' );
} );
QUnit.test( 'Viewer: tile-loaded event with 2 callbacks.', function (assert) {
var done = assert.async();
function tileLoaded ( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
var tile = event.tile;
var callback1 = event.getCompletionCallback();
var callback2 = event.getCompletionCallback();
QUnit.test( 'Viewer: asynchronous tile processing.', function (assert) {
var done = assert.async(),
handledOnce = false;
const tileLoaded1 = async (event) => {
assert.ok( handledOnce, "tileLoaded1 with priority 5 should be called second.");
const tile = event.tile;
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.");
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' );
} );
QUnit.test( 'Viewer: tile-unloaded event.', function(assert) {
var tiledImage;
var tile;
var tiles = [];
var done = assert.async();
function tileLoaded( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
tiledImage = event.tiledImage;
tile = event.tile;
setTimeout(function() {
tiledImage.reset();
}, 0);
tiles.push(event.tile);
if (tiles.length === 1) {
setTimeout(function() {
tiledImage.reset();
}, 0);
}
}
function tileUnloaded( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
viewer.removeHandler( 'tile-unloaded', tileUnloaded );
assert.equal( tile, event.tile,
"The unloaded tile should be the same than the loaded one." );
assert.equal( tiles.find(t => t === event.tile), event.tile,
"The unloaded tile should be one of the loaded tiles." );
assert.equal( tiledImage, event.tiledImage,
"The tiledImage of the unloaded tile should be the same than the one of the loaded one." );
done();

View File

@ -29,7 +29,7 @@
};
var testOpen = function(tileSource, assert) {
var timeWatcher = Util.timeWatcher(assert, 7000);
const done = assert.async();
viewer = OpenSeadragon({
id: 'example',
@ -56,7 +56,7 @@
viewer.removeHandler('close', closeHandler);
$('#example').empty();
assert.ok(true, 'Close event was sent');
timeWatcher.done();
done();
};
viewer.addHandler('open', openHandler);
};

View File

@ -201,7 +201,6 @@
var done = assert.async();
viewer.addHandler("open", function openHandler() {
viewer.removeHandler("open", openHandler);
viewer.world.addHandler('add-item', function itemAdded(event) {
viewer.world.removeHandler('add-item', itemAdded);
assert.equal(event.item.opacity, 0.5,
@ -221,17 +220,23 @@
var done = assert.async();
viewer.open('/test/data/testpattern.dzi');
var density = OpenSeadragon.pixelDensityRatio;
function getPixelFromViewerScreenCoords(x, y) {
const density = OpenSeadragon.pixelDensityRatio;
const imageData = viewer.drawer.context.getImageData(x * density, y * density, 1, 1);
return {
r: imageData.data[0],
g: imageData.data[1],
b: imageData.data[2],
a: imageData.data[3]
};
}
viewer.addHandler('open', function() {
var firstImage = viewer.world.getItemAt(0);
firstImage.addHandler('fully-loaded-change', function() {
viewer.addOnceHandler('update-viewport', function(){
var imageData = viewer.drawer.context.getImageData(0, 0,
500 * density, 500 * density);
// Pixel 250,250 will be in the hole of the A
var expectedVal = getPixelValue(imageData, 250 * density, 250 * density);
var expectedVal = getPixelFromViewerScreenCoords(250, 250);
assert.notEqual(expectedVal.r, 0, 'Red channel should not be 0');
assert.notEqual(expectedVal.g, 0, 'Green channel should not be 0');
@ -242,10 +247,9 @@
url: '/test/data/A.png',
success: function() {
var secondImage = viewer.world.getItemAt(1);
secondImage.addHandler('fully-loaded-change', function() {
viewer.addOnceHandler('update-viewport',function(){
var imageData = viewer.drawer.context.getImageData(0, 0, 500 * density, 500 * density);
var actualVal = getPixelValue(imageData, 250 * density, 250 * density);
secondImage.addHandler('fully-loaded-change', function() {
viewer.addOnceHandler('update-viewport', function(){
var actualVal = getPixelFromViewerScreenCoords(250, 250);
assert.equal(actualVal.r, expectedVal.r,
'Red channel should not change in transparent part of the A');
@ -256,10 +260,10 @@
assert.equal(actualVal.a, expectedVal.a,
'Alpha channel should not change in transparent part of the A');
var onAVal = getPixelValue(imageData, 333 * density, 250 * density);
assert.equal(onAVal.r, 0, 'Red channel should be null on the A');
assert.equal(onAVal.g, 0, 'Green channel should be null on the A');
assert.equal(onAVal.b, 0, 'Blue channel should be null on the A');
var onAVal = getPixelFromViewerScreenCoords(333 , 250);
assert.equal(onAVal.r, 0, 'Red channel should be 0 on the A');
assert.equal(onAVal.g, 0, 'Green channel should be 0 on the A');
assert.equal(onAVal.b, 0, 'Blue channel should be 0 on the A');
assert.equal(onAVal.a, 255, 'Alpha channel should be 255 on the A');
done();
@ -272,17 +276,6 @@
});
});
});
function getPixelValue(imageData, x, y) {
var offset = 4 * (y * imageData.width + x);
return {
r: imageData.data[offset],
g: imageData.data[offset + 1],
b: imageData.data[offset + 2],
a: imageData.data[offset + 3]
};
}
});
}
})();

View File

@ -1,61 +1,250 @@
/* global QUnit, testLog */
(function() {
const Convertor = OpenSeadragon.convertor,
T_A = "__TEST__typeA", T_B = "__TEST__typeB", T_C = "__TEST__typeC", T_D = "__TEST__typeD", T_E = "__TEST__typeE";
let viewer;
//we override jobs: remember original function
const originalJob = OpenSeadragon.ImageLoader.prototype.addJob;
//event awaiting
function waitFor(predicate) {
const time = setInterval(() => {
if (predicate()) {
clearInterval(time);
}
}, 20);
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
// other tests will interfere
let typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
//set all same costs to get easy testing, know which path will be taken
Convertor.learn(T_A, T_B, (tile, x) => {
typeAtoB++;
return x+1;
});
// Costly conversion to C simulation
Convertor.learn(T_B, T_C, async (tile, x) => {
typeBtoC++;
await sleep(5);
return x+1;
});
Convertor.learn(T_C, T_A, (tile, x) => {
typeCtoA++;
return x+1;
});
Convertor.learn(T_D, T_A, (tile, x) => {
typeDtoA++;
return x+1;
});
Convertor.learn(T_C, T_E, (tile, x) => {
typeCtoE++;
return x+1;
});
//'Copy constructors'
let copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
//also learn destructors
Convertor.learn(T_A, T_A,(tile, x) => {
copyA++;
return x+1;
});
Convertor.learn(T_B, T_B,(tile, x) => {
copyB++;
return x+1;
});
Convertor.learn(T_C, T_C,(tile, x) => {
copyC++;
return x-1;
});
Convertor.learn(T_D, T_D,(tile, x) => {
copyD++;
return x+1;
});
Convertor.learn(T_E, T_E,(tile, x) => {
copyE++;
return x+1;
});
let destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
//also learn destructors
Convertor.learnDestroy(T_A, () => {
destroyA++;
});
Convertor.learnDestroy(T_B, () => {
destroyB++;
});
Convertor.learnDestroy(T_C, () => {
destroyC++;
});
Convertor.learnDestroy(T_D, () => {
destroyD++;
});
Convertor.learnDestroy(T_E, () => {
destroyE++;
});
OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase {
constructor(opts) {
super(opts);
this.testEvents = new OpenSeadragon.EventSource();
}
getType() {
return "test-cache-drawer";
}
// Make test use private cache
get defaultOptions() {
return {
usePrivateCache: true
};
}
getSupportedDataFormats() {
return [T_C, T_E];
}
static isSupported() {
return true;
}
_createDrawingElement() {
return document.createElement("div");
}
draw(tiledImages) {
for (let image of tiledImages) {
const tilesDoDraw = image.getTilesToDraw().map(info => info.tile);
for (let tile of tilesDoDraw) {
const data = this.getDataToDraw(tile);
this.testEvents.raiseEvent('test-tile', {
tile: tile,
dataToDraw: data,
});
}
}
}
canRotate() {
return true;
}
destroy() {
//noop
}
setImageSmoothingEnabled(imageSmoothingEnabled){
//noop
}
drawDebuggingRect(rect) {
//noop
}
clear(){
//noop
}
}
OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource {
supports( data, url ){
return data && data.isTestSource;
}
configure( data, url, postData ){
return {
width: 512, /* width *required */
height: 512, /* height *required */
tileSize: 128, /* tileSize *required */
tileOverlap: 0, /* tileOverlap *required */
minLevel: 0, /* minLevel */
maxLevel: 3, /* maxLevel */
tilesUrl: "", /* tilesUrl */
fileFormat: "", /* fileFormat */
displayRects: null /* displayRects */
}
}
getTileUrl(level, x, y) {
return String(level); //treat each tile on level same to introduce cache overlaps
}
downloadTileStart(context) {
context.finish(0, null, T_A);
}
}
// ----------
QUnit.module('TileCache', {
beforeEach: function () {
$('<div id="example"></div>').appendTo("#qunit-fixture");
testLog.reset();
viewer = OpenSeadragon({
id: 'example',
prefixUrl: '/build/openseadragon/images/',
maxImageCacheCount: 200, //should be enough to fit test inside the cache
springStiffness: 100, // Faster animation = faster tests
drawer: 'test-cache-drawer',
});
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
// Reset counters
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
},
afterEach: function () {
if (viewer && viewer.close) {
viewer.close();
}
viewer = null;
}
});
// ----------
// TODO: this used to be async
QUnit.test('basics', function(assert) {
var done = assert.async();
var fakeViewer = {
raiseEvent: function() {}
};
var fakeTiledImage0 = {
viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
};
var fakeTiledImage1 = {
viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
};
const done = assert.async();
const fakeViewer = MockSeadragon.getViewer(
MockSeadragon.getDrawer({
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats() {
return [T_A, T_B, T_C, T_D, T_E];
}
})
);
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer);
var fakeTile0 = {
url: 'foo.jpg',
cacheKey: 'foo.jpg',
image: {},
unload: function() {}
};
const tile0 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0);
const tile1 = MockSeadragon.getTile('foo.jpg', fakeTiledImage1);
var fakeTile1 = {
url: 'foo.jpg',
cacheKey: 'foo.jpg',
image: {},
unload: function() {}
};
var cache = new OpenSeadragon.TileCache();
const cache = new OpenSeadragon.TileCache();
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
cache.cacheTile({
tile: fakeTile0,
tiledImage: fakeTiledImage0
tile0._caches[tile0.cacheKey] = cache.cacheTile({
tile: tile0,
tiledImage: fakeTiledImage0,
data: 3,
dataType: T_A
});
tile0._cacheSize++;
assert.equal(cache.numTilesLoaded(), 1, 'tile count after cache');
cache.cacheTile({
tile: fakeTile1,
tiledImage: fakeTiledImage1
tile1._caches[tile1.cacheKey] = cache.cacheTile({
tile: tile1,
tiledImage: fakeTiledImage1,
data: 55,
dataType: T_B
});
tile1._cacheSize++;
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
cache.clearTilesFor(fakeTiledImage0);
@ -71,64 +260,520 @@
// ----------
QUnit.test('maxImageCacheCount', function(assert) {
var done = assert.async();
var fakeViewer = {
raiseEvent: function() {}
};
var fakeTiledImage0 = {
viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
};
const done = assert.async();
const fakeViewer = MockSeadragon.getViewer(
MockSeadragon.getDrawer({
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats() {
return [T_A, T_B, T_C, T_D, T_E];
}
})
);
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
const tile0 = MockSeadragon.getTile('different.jpg', fakeTiledImage0);
const tile1 = MockSeadragon.getTile('same.jpg', fakeTiledImage0);
const tile2 = MockSeadragon.getTile('same.jpg', fakeTiledImage0);
var fakeTile0 = {
url: 'different.jpg',
cacheKey: 'different.jpg',
image: {},
unload: function() {}
};
var fakeTile1 = {
url: 'same.jpg',
cacheKey: 'same.jpg',
image: {},
unload: function() {}
};
var fakeTile2 = {
url: 'same.jpg',
cacheKey: 'same.jpg',
image: {},
unload: function() {}
};
var cache = new OpenSeadragon.TileCache({
const cache = new OpenSeadragon.TileCache({
maxImageCacheCount: 1
});
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
cache.cacheTile({
tile: fakeTile0,
tiledImage: fakeTiledImage0
tile0._caches[tile0.cacheKey] = cache.cacheTile({
tile: tile0,
tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
});
tile0._cacheSize++;
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add');
cache.cacheTile({
tile: fakeTile1,
tiledImage: fakeTiledImage0
tile1._caches[tile1.cacheKey] = cache.cacheTile({
tile: tile1,
tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
});
tile1._cacheSize++;
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add of second image');
cache.cacheTile({
tile: fakeTile2,
tiledImage: fakeTiledImage0
tile2._caches[tile2.cacheKey] = cache.cacheTile({
tile: tile2,
tiledImage: fakeTiledImage0,
data: 55,
dataType: T_B
});
tile2._cacheSize++;
assert.equal(cache.numTilesLoaded(), 2, 'tile count after additional same image');
done();
});
//Tile API and cache interaction
QUnit.test('Tile: basic rendering & test setup', function(test) {
const done = test.async();
const tileCache = viewer.tileCache;
const drawer = viewer.drawer;
let testTileCalled = false;
drawer.testEvents.addHandler('test-tile', e => {
testTileCalled = true;
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
});
viewer.addHandler('open', async () => {
await viewer.waitForFinishedJobsForTest();
await sleep(1); // necessary to make space for a draw call
test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A.");
test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A.");
test.ok(testTileCalled, "Drawer tested at least one tile.");
test.ok(typeAtoB > 1, "At least one conversion was triggered.");
test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer.");
for (let tile of tileCache._tilesLoaded) {
const cache = tile.getCache();
test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache.");
const internalCache = cache.getDataForRendering(drawer, tile);
test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E.");
test.ok(internalCache.loaded, "Internal cache ready.");
}
done();
});
viewer.open([
{isTestSource: true},
{isTestSource: true},
]);
});
QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) {
const done = test.async();
const tileCache = viewer.tileCache;
const drawer = viewer.drawer;
let testTileCalled = false;
let _currentTestVal = undefined;
let previousTestValue = undefined;
drawer.testEvents.addHandler('test-tile', e => {
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
if (_currentTestVal !== undefined) {
testTileCalled = true;
test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data.");
}
});
function testDrawingRoutine(value) {
_currentTestVal = value;
viewer.world.needsDraw();
viewer.world.draw();
previousTestValue = value;
_currentTestVal = undefined;
}
viewer.addHandler('open', async () => {
await viewer.waitForFinishedJobsForTest();
await sleep(1); // necessary to make space for a draw call
// Test simple data set -> creates main cache
let testHandler = async e => {
// data comes in as T_A
test.equal(typeDtoA, 0, "No conversion needed to get type A.");
test.equal(typeCtoA, 0, "No conversion needed to get type A.");
const data = await e.getData(T_A);
test.equal(data, 1, "Copy: creation of a working cache.");
e.tile.__TEST_PROCESSED = true;
// Test value 2 since we set T_C no need to convert
await e.setData(2, T_C);
test.notOk(e.outdated(), "Event is still valid.");
};
viewer.addHandler('tile-invalidated', testHandler);
await viewer.world.requestInvalidate(true);
await sleep(1); // necessary to make space for internal updates
testDrawingRoutine(2);
//test for each level only single cache was processed
const processedLevels = {};
for (let tile of tileCache._tilesLoaded) {
const level = tile.level;
if (tile.__TEST_PROCESSED) {
test.ok(!processedLevels[level], "Only single tile processed per level.");
processedLevels[level] = true;
delete tile.__TEST_PROCESSED;
}
const origCache = tile.getCache(tile.originalCacheKey);
test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache.");
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache.");
const cache = tile.getCache();
test.equal(cache.type, T_C, "Main Cache Updated (suite 1)");
test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)");
const internalCache = cache.getDataForRendering(drawer, tile);
test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E.");
test.ok(internalCache.loaded, "Internal cache ready.");
}
// Test that basic scenario with reset data false starts from the main cache data of previous round
const modificationConstant = 50;
viewer.removeHandler('tile-invalidated', testHandler);
testHandler = async e => {
const data = await e.getData(T_B);
test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened.");
await e.setData(data + modificationConstant, T_B);
console.log(data + modificationConstant);
test.notOk(e.outdated(), "Event is still valid.");
};
console.log(previousTestValue, modificationConstant)
viewer.addHandler('tile-invalidated', testHandler);
await viewer.world.requestInvalidate(false);
await sleep(1); // necessary to make space for a draw call
// We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round
let newValue = previousTestValue + modificationConstant + 3;
testDrawingRoutine(newValue);
newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less
for (let tile of tileCache._tilesLoaded) {
const cache = tile.getCache();
test.equal(cache.type, T_B, "Main Cache Updated (suite 2).");
test.equal(cache.data, newValue, "Main Cache Updated (suite 2).");
}
// Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration
viewer.removeHandler('tile-invalidated', testHandler);
testHandler = async e => {
const data = await e.getData(T_B);
test.equal(data, 1, "Copy: creation of a working cache.");
await e.setData(-8, T_E);
e.resetData();
};
viewer.addHandler('tile-invalidated', testHandler);
await viewer.world.requestInvalidate(true);
await sleep(1); // necessary to make space for a draw call
testDrawingRoutine(2); // Value +2 rendering from original data
for (let tile of tileCache._tilesLoaded) {
const origCache = tile.getCache(tile.originalCacheKey);
test.ok(tile.getCache() === origCache, "Main cache is now original cache.");
}
// Now force main cache creation that differs
viewer.removeHandler('tile-invalidated', testHandler);
testHandler = async e => {
await e.setData(41, T_B);
};
viewer.addHandler('tile-invalidated', testHandler);
await viewer.world.requestInvalidate(true);
// Now test whether data reset works, even with non-original data
viewer.removeHandler('tile-invalidated', testHandler);
testHandler = async e => {
const data = await e.getData(T_B);
test.equal(data, 42, "Copy: 41 + 1.");
await e.setData(data, T_E);
e.resetData();
};
viewer.addHandler('tile-invalidated', testHandler);
await viewer.world.requestInvalidate(false);
await sleep(1); // necessary to make space for a draw call
testDrawingRoutine(42);
for (let tile of tileCache._tilesLoaded) {
const origCache = tile.getCache(tile.originalCacheKey);
test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh.");
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh.");
}
test.ok(testTileCalled, "Drawer tested at least one tile.");
done();
});
viewer.open([
{isTestSource: true},
{isTestSource: true},
]);
});
//Tile API and cache interaction
QUnit.test('Tile API Cache Interaction', function(test) {
const done = test.async();
const fakeViewer = MockSeadragon.getViewer(
MockSeadragon.getDrawer({
// tile in safe mode inspects the supported formats upon cache set
getSupportedDataFormats() {
return [T_A, T_B, T_C, T_D, T_E];
}
})
);
const tileCache = fakeViewer.tileCache;
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer);
//load data
const tile00 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0);
tile00.addCache(tile00.cacheKey, 0, T_A, false, false);
const tile01 = MockSeadragon.getTile('foo2.jpg', fakeTiledImage0);
tile01.addCache(tile01.cacheKey, 0, T_B, false, false);
const tile10 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1);
tile10.addCache(tile10.cacheKey, 0, T_C, false, false);
const tile11 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1);
tile11.addCache(tile11.cacheKey, 0, T_C, false, false);
const tile12 = MockSeadragon.getTile('foo.jpg', fakeTiledImage1);
tile12.addCache(tile12.cacheKey, 0, T_A, false, false);
//test set/get data in async env
(async function() {
test.equal(tileCache.numTilesLoaded(), 5, "We loaded 5 tiles");
test.equal(tileCache.numCachesLoaded(), 3, "We loaded 3 cache objects - three different urls");
const c00 = tile00.getCache(tile00.cacheKey);
const c12 = tile12.getCache(tile12.cacheKey);
//now test multi-cache within tile
const theTileKey = tile00.cacheKey;
tile00.addCache(tile00.buildDistinctMainCacheKey(), 42, T_E, true, false);
test.notEqual(tile00.cacheKey, tile00.originalCacheKey, "New cache rendered.");
//now add artifically another record
tile00.addCache("my_custom_cache", 128, T_C);
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items (two new added already).");
test.equal(c00.getTileCount(), 2, "The cache still has only two tiles attached.");
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects (original data, main cache & custom.");
//related tile not affected
test.equal(tile12.cacheKey, tile12.originalCacheKey, "Original cache change not reflected on shared caches.");
test.equal(tile12.originalCacheKey, theTileKey, "Original cache key also preserved.");
test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached.");
//add and delete cache nothing changes (+1 destroy T_C)
tile00.addCache("my_custom_cache2", 128, T_C);
tile00.removeCache("my_custom_cache2");
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items.");
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
//delete cache as a zombie (+0 destroy)
tile00.addCache("my_custom_cache2", 17, T_D);
//direct access shoes correct value although we set key!
const myCustomCache2Data = tile00.getCache("my_custom_cache2").data;
test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene.");
test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6.");
//keep zombie
tile00.removeCache("my_custom_cache2", false);
test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
test.equal(tileCache._zombiesLoadedCount, 1, "One zombie.");
//revive zombie
tile01.addCache("my_custom_cache2", 18, T_D);
const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data;
test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived.");
test.equal(tileCache._cachesLoadedCount, 6, "Zombie revived, original state restored.");
test.equal(tileCache._zombiesLoadedCount, 0, "No zombies.");
//again, keep zombie
tile01.removeCache("my_custom_cache2", false);
//first create additional cache so zombie is not the youngest
tile01.addCache("some weird cache", 11, T_A);
test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys.");
//insertion aadditional cache clears the zombie first although it is not the youngest one
test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
test.equal(tileCache._cachesLoadedCount, 6, "New cache created -> 5+1.");
test.equal(tileCache._zombiesLoadedCount, 1, "One zombie remains.");
//Test CAP
tileCache._maxCacheItemCount = 7;
// Zombie destroyed before other caches (+1 destroy T_D)
tile12.addCache("someKey", 43, T_B);
test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
test.equal(tileCache._zombiesLoadedCount, 0, "One zombie sacrificed, preferred over living cache.");
test.notOk([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "All tiles sill loaded since zombie was sacrificed.");
// test destructors called as expected
test.equal(destroyA, 0, "No destructors for A called.");
test.equal(destroyB, 0, "No destructors for B called.");
test.equal(destroyC, 1, "One destruction for C called.");
test.equal(destroyD, 1, "One destruction for D called.");
test.equal(destroyE, 0, "No destructors for E called.");
//try to revive zombie will fail: the zombie was deleted, we will find new vaue there
tile01.addCache("my_custom_cache2", -849613, T_C);
const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because zombie was killed.");
test.equal(myCustomCache2RecreatedData, -849613, "Cache data is actually as set to 18.");
test.equal(tileCache.numCachesLoaded(), 7, "The cache has still 7 items.");
// some tile has been selected as a sacrifice since we triggered cap control
test.ok([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "One tile has been sacrificed.");
done();
})();
});
QUnit.test('Zombie Cache', function(test) {
const done = test.async();
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
let jobCounter = 0, coverage = undefined;
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
jobCounter++;
if (coverage) {
//old coverage of previous tiled image: if loaded, fail --> should be in cache
const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y];
test.ok(!coverageItem, "Attempt to add job for tile that should be already in memory.");
}
return originalJob.call(this, options);
};
let tilesFinished = 0;
const tileCounter = function (event) {tilesFinished++;}
const openHandler = function(event) {
event.item.allowZombieCache(true);
viewer.world.removeHandler('add-item', openHandler);
test.ok(jobCounter === 0, 'Initial state, no images loaded');
waitFor(() => {
if (tilesFinished === jobCounter && event.item._fullyLoaded) {
coverage = $.extend(true, {}, event.item.coverage);
viewer.world.removeAll();
return true;
}
return false;
});
};
let jobsAfterRemoval = 0;
const removalHandler = function (event) {
viewer.world.removeHandler('remove-item', removalHandler);
test.ok(jobCounter > 0, 'Tiled image removed after 100 ms, should load some images.');
jobsAfterRemoval = jobCounter;
viewer.world.addHandler('add-item', reopenHandler);
viewer.addTiledImage({
tileSource: '/test/data/testpattern.dzi'
});
}
const reopenHandler = function (event) {
event.item.allowZombieCache(true);
viewer.removeHandler('add-item', reopenHandler);
test.equal(jobCounter, jobsAfterRemoval, 'Reopening image does not fetch any tiles imemdiatelly.');
waitFor(() => {
if (event.item._fullyLoaded) {
viewer.removeHandler('tile-unloaded', unloadTileHandler);
viewer.removeHandler('tile-loaded', tileCounter);
coverage = undefined;
//console test needs here explicit removal to finish correctly
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
done();
return true;
}
return false;
});
};
const unloadTileHandler = function (event) {
test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!");
}
viewer.world.addHandler('add-item', openHandler);
viewer.world.addHandler('remove-item', removalHandler);
viewer.addHandler('tile-unloaded', unloadTileHandler);
viewer.addHandler('tile-loaded', tileCounter);
viewer.open('/test/data/testpattern.dzi');
});
QUnit.test('Zombie Cache Replace Item', function(test) {
const done = test.async();
let jobCounter = 0, coverage = undefined;
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
jobCounter++;
if (coverage) {
//old coverage of previous tiled image: if loaded, fail --> should be in cache
const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y];
if (!coverageItem) {
console.warn(coverage, coverage[options.tile.level][options.tile.x], options.tile);
}
test.ok(!coverageItem, "Attempt to add job for tile data that was previously loaded.");
}
return originalJob.call(this, options);
};
let tilesFinished = 0;
const tileCounter = function (event) {tilesFinished++;}
const openHandler = function(event) {
event.item.allowZombieCache(true);
viewer.world.removeHandler('add-item', openHandler);
viewer.world.addHandler('add-item', reopenHandler);
waitFor(() => {
if (tilesFinished === jobCounter && event.item._fullyLoaded) {
coverage = $.extend(true, {}, event.item.coverage);
viewer.addTiledImage({
tileSource: '/test/data/testpattern.dzi',
index: 0,
replace: true
});
return true;
}
return false;
});
};
const reopenHandler = function (event) {
event.item.allowZombieCache(true);
viewer.removeHandler('add-item', reopenHandler);
waitFor(() => {
if (event.item._fullyLoaded) {
viewer.removeHandler('tile-unloaded', unloadTileHandler);
viewer.removeHandler('tile-loaded', tileCounter);
//console test needs here explicit removal to finish correctly
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
done();
return true;
}
return false;
});
};
const unloadTileHandler = function (event) {
test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!");
}
viewer.world.addHandler('add-item', openHandler);
viewer.addHandler('tile-unloaded', unloadTileHandler);
viewer.addHandler('tile-loaded', tileCounter);
viewer.open('/test/data/testpattern.dzi');
});
})();

View File

@ -558,17 +558,17 @@
});
QUnit.test('_getCornerTiles without wrapping', function(assert) {
var tiledImageMock = {
var tiledImageMock = MockSeadragon.getTiledImage(null, {
wrapHorizontal: false,
wrapVertical: false,
source: new OpenSeadragon.TileSource({
source: MockSeadragon.getTileSource({
width: 1500,
height: 1000,
tileWidth: 200,
tileHeight: 150,
tileOverlap: 1,
}),
};
})
});
var _getCornerTiles = OpenSeadragon.TiledImage.prototype._getCornerTiles.bind(tiledImageMock);
function assertCornerTiles(topLeftBound, bottomRightBound,
@ -606,17 +606,13 @@
});
QUnit.test('_getCornerTiles with horizontal wrapping', function(assert) {
var tiledImageMock = {
var tiledImageMock = MockSeadragon.getTiledImage(null, {
wrapHorizontal: true,
wrapVertical: false,
source: new OpenSeadragon.TileSource({
width: 1500,
height: 1000,
tileWidth: 200,
tileHeight: 150,
tileOverlap: 1,
}),
};
source: MockSeadragon.getTileSource({
tileOverlap: 1
})
});
var _getCornerTiles = OpenSeadragon.TiledImage.prototype._getCornerTiles.bind(tiledImageMock);
function assertCornerTiles(topLeftBound, bottomRightBound,
@ -653,17 +649,13 @@
});
QUnit.test('_getCornerTiles with vertical wrapping', function(assert) {
var tiledImageMock = {
var tiledImageMock = MockSeadragon.getTiledImage(null, {
wrapHorizontal: false,
wrapVertical: true,
source: new OpenSeadragon.TileSource({
width: 1500,
height: 1000,
tileWidth: 200,
tileHeight: 150,
tileOverlap: 1,
}),
};
source: MockSeadragon.getTileSource({
tileOverlap: 1
})
});
var _getCornerTiles = OpenSeadragon.TiledImage.prototype._getCornerTiles.bind(tiledImageMock);
function assertCornerTiles(topLeftBound, bottomRightBound,

View File

@ -8,7 +8,7 @@
var DYNAMIC_URL = "";
var viewer = null;
var OriginalAjax = OpenSeadragon.makeAjaxRequest;
var OriginalTile = OpenSeadragon.Tile;
var OriginalTileGetUrl = OpenSeadragon.Tile.prototype.getUrl;
// These variables allow tracking when the first request for data has finished
var firstUrlPromise = null;
var isFirstUrlPromiseResolved = false;
@ -115,22 +115,15 @@
return request;
};
// Override Tile to ensure getUrl is called successfully.
var Tile = function(...params) {
OriginalTile.apply(this, params);
};
OpenSeadragon.extend( Tile.prototype, OpenSeadragon.Tile.prototype, {
getUrl: function() {
// if ASSERT is still truthy, call ASSERT.ok. If the viewer
// has already been destroyed and ASSERT has set to null, ignore this
if(ASSERT){
ASSERT.ok(true, 'Tile.getUrl called');
}
return OriginalTile.prototype.getUrl.apply(this);
// Override Tile::getUrl to ensure getUrl is called successfully.
OpenSeadragon.Tile.prototype.getUrl = function () {
// if ASSERT is still truthy, call ASSERT.ok. If the viewer
// has already been destroyed and ASSERT has set to null, ignore this
if (ASSERT) {
ASSERT.ok(true, 'Tile.getUrl called');
}
});
OpenSeadragon.Tile = Tile;
return OriginalTileGetUrl.apply(this, arguments);
};
},
afterEach: function () {
@ -143,7 +136,7 @@
viewer = null;
OpenSeadragon.makeAjaxRequest = OriginalAjax;
OpenSeadragon.Tile = OriginalTile;
OpenSeadragon.Tile.prototype.getUrl = OriginalTileGetUrl;
}
});

View File

@ -0,0 +1,389 @@
/* global QUnit, $, Util, testLog */
(function() {
const Convertor = OpenSeadragon.convertor;
let viewer;
//we override jobs: remember original function
const originalJob = OpenSeadragon.ImageLoader.prototype.addJob;
//event awaiting
function waitFor(predicate) {
const time = setInterval(() => {
if (predicate()) {
clearInterval(time);
}
}, 20);
}
//hijack conversion paths
//count jobs: how many items we process?
let jobCounter = 0;
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
jobCounter++;
return originalJob.call(this, options);
};
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
// other tests will interfere
// Note: this is not the same as in the production conversion, where CANVAS on its own does not exist
let imageToCanvas = 0, srcToImage = 0, context2DtoImage = 0, canvasToContext2D = 0, imageToUrl = 0,
canvasToUrl = 0;
//set all same costs to get easy testing, know which path will be taken
Convertor.learn("__TEST__canvas", "__TEST__url", (tile, canvas) => {
canvasToUrl++;
return canvas.toDataURL();
}, 1, 1);
Convertor.learn("__TEST__image", "__TEST__url", (tile,image) => {
imageToUrl++;
return image.url;
}, 1, 1);
Convertor.learn("__TEST__canvas", "__TEST__context2d", (tile,canvas) => {
canvasToContext2D++;
return canvas.getContext("2d");
}, 1, 1);
Convertor.learn("__TEST__context2d", "__TEST__canvas", (tile,context2D) => {
context2DtoImage++;
return context2D.canvas;
}, 1, 1);
Convertor.learn("__TEST__image", "__TEST__canvas", (tile,image) => {
imageToCanvas++;
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);
Convertor.learn("__TEST__url", "__TEST__image", (tile, url) => {
return new Promise((resolve, reject) => {
srcToImage++;
const img = new Image();
img.onerror = img.onabort = reject;
img.onload = () => resolve(img);
img.src = url;
});
}, 1, 1);
let canvasDestroy = 0, imageDestroy = 0, contex2DDestroy = 0, urlDestroy = 0;
//also learn destructors
Convertor.learnDestroy("__TEST__canvas", canvas => {
canvas.width = canvas.height = 0;
canvasDestroy++;
});
Convertor.learnDestroy("__TEST__image", () => {
imageDestroy++;
});
Convertor.learnDestroy("__TEST__context2d", () => {
contex2DDestroy++;
});
Convertor.learnDestroy("__TEST__url", () => {
urlDestroy++;
});
QUnit.module('TypeConversion', {
beforeEach: function () {
$('<div id="example"></div>').appendTo("#qunit-fixture");
testLog.reset();
viewer = OpenSeadragon({
id: 'example',
prefixUrl: '/build/openseadragon/images/',
maxImageCacheCount: 200, //should be enough to fit test inside the cache
springStiffness: 100 // Faster animation = faster tests
});
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
},
afterEach: function () {
if (viewer && viewer.close) {
viewer.close();
}
viewer = null;
imageToCanvas = 0; srcToImage = 0; context2DtoImage = 0;
canvasToContext2D = 0; imageToUrl = 0; canvasToUrl = 0;
canvasDestroy = 0; imageDestroy = 0; contex2DDestroy = 0; urlDestroy = 0;
}
});
QUnit.test('Conversion path deduction', function (test) {
const done = test.async();
test.ok(Convertor.getConversionPath("__TEST__url", "__TEST__image"),
"Type conversion ok between TEST types.");
test.ok(Convertor.getConversionPath("url", "context2d"),
"Type conversion ok between real types.");
test.equal(Convertor.getConversionPath("url", "__TEST__image"), undefined,
"Type conversion not possible between TEST and real types.");
test.equal(Convertor.getConversionPath("__TEST__canvas", "context2d"), undefined,
"Type conversion not possible between TEST and real types.");
done();
});
QUnit.test('Copy of build-in types', function (test) {
const done = test.async();
//prepare data
const URL = "/test/data/A.png";
const image = new Image();
image.onerror = image.onabort = () => {
test.ok(false, "Image data preparation failed to load!");
done();
};
const canvas = document.createElement( 'canvas' );
//test when ready
image.onload = async () => {
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d');
context.drawImage( image, 0, 0 );
//copy URL
const URL2 = await Convertor.copy(null, URL, "url");
//we cannot check if they are not the same object, strings are immutable (and we don't copy anyway :D )
test.equal(URL, URL2, "String copy is equal in data.");
test.equal(typeof URL, typeof URL2, "Type of copies equals.");
test.equal(URL.length, URL2.length, "Data length is also equal.");
//copy context
const context2 = await Convertor.copy(null, context, "context2d");
test.notEqual(context, context2, "Copy is not the same as original canvas.");
test.equal(typeof context, typeof context2, "Type of copies equals.");
test.equal(context.canvas.toDataURL(), context2.canvas.toDataURL(), "Data is equal.");
//copy image
const image2 = await Convertor.copy(null, image, "image");
test.notEqual(image, image2, "Copy is not the same as original image.");
test.equal(typeof image, typeof image2, "Type of copies equals.");
test.equal(image.src, image2.src, "Data is equal.");
done();
};
image.src = URL;
});
// ----------
QUnit.test('Manual Data Convertors: testing conversion, copies & destruction', function (test) {
const done = test.async();
//load image object: url -> image
Convertor.convert(null, "/test/data/A.png", "__TEST__url", "__TEST__image").then(i => {
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
test.equal(srcToImage, 1, "Conversion happened.");
test.equal(urlDestroy, 0, "Url destructor not called automatically.");
Convertor.destroy("/test/data/A.png", "__TEST__url");
test.equal(urlDestroy, 1, "Url destructor called.");
test.equal(imageDestroy, 0, "Image destructor not called.");
return Convertor.convert(null, i, "__TEST__image", "__TEST__canvas");
}).then(c => { //path image -> canvas
test.equal(OpenSeadragon.type(c), "canvas", "Got canvas object after conversion.");
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
test.equal(urlDestroy, 1, "Url destructor not called.");
test.equal(imageDestroy, 0, "Image destructor not called unless we ask it.");
return Convertor.convert(null, c, "__TEST__canvas", "__TEST__image");
}).then(i => { //path canvas, image: canvas -> url -> image
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
test.equal(imageToCanvas, 1, "Conversion image->canvas did not happened.");
test.equal(context2DtoImage, 0, "Conversion c2d->image did not happened.");
test.equal(canvasToContext2D, 0, "Conversion canvas->c2d did not happened.");
test.equal(canvasToUrl, 1, "Conversion canvas->url happened.");
test.equal(imageToUrl, 0, "Conversion image->url did not happened.");
test.equal(urlDestroy, 2, "Url destructor called.");
test.equal(imageDestroy, 0, "Image destructor not called.");
test.equal(canvasDestroy, 0, "Canvas destructor called.");
test.equal(contex2DDestroy, 0, "Image destructor not called.");
done();
});
});
QUnit.test('Data Convertors via Cache object: testing conversion & destruction', function (test) {
const done = test.async();
const dummyTile = MockSeadragon.getTile("", MockSeadragon.getTiledImage(), {cacheKey: "key"});
const cache = MockSeadragon.getCacheRecord();
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
//load image object: url -> image
cache.transformTo("__TEST__image").then(_ => {
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
test.equal(srcToImage, 1, "Conversion happened.");
test.equal(urlDestroy, 1, "Url destructor called.");
test.equal(imageDestroy, 0, "Image destructor not called.");
return cache.transformTo("__TEST__canvas");
}).then(_ => { //path image -> canvas
test.equal(OpenSeadragon.type(cache.data), "canvas", "Got canvas object after conversion.");
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
test.equal(urlDestroy, 1, "Url destructor not called.");
test.equal(imageDestroy, 1, "Image destructor called.");
return cache.transformTo("__TEST__image");
}).then(_ => { //path canvas, image: canvas -> url -> image
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
test.equal(imageToCanvas, 1, "Conversion image->canvas did not happened.");
test.equal(context2DtoImage, 0, "Conversion c2d->image did not happened.");
test.equal(canvasToContext2D, 0, "Conversion canvas->c2d did not happened.");
test.equal(canvasToUrl, 1, "Conversion canvas->url happened.");
test.equal(imageToUrl, 0, "Conversion image->url did not happened.");
test.equal(urlDestroy, 2, "Url destructor called.");
test.equal(imageDestroy, 1, "Image destructor not called.");
test.equal(canvasDestroy, 1, "Canvas destructor called.");
test.equal(contex2DDestroy, 0, "Image destructor not called.");
}).then(_ => {
cache.destroy();
test.equal(urlDestroy, 2, "Url destructor not called.");
test.equal(imageDestroy, 2, "Image destructor called.");
test.equal(canvasDestroy, 1, "Canvas destructor not called.");
test.equal(contex2DDestroy, 0, "Image destructor not called.");
done();
});
});
QUnit.test('Data Convertors via Cache object: testing set/get', function (test) {
const done = test.async();
const dummyTile = MockSeadragon.getTile("", MockSeadragon.getTiledImage(), {cacheKey: "key"});
const cache = MockSeadragon.getCacheRecord({
testGetSet: async function(type) {
const value = await cache.getDataAs(type, false);
await cache.setDataAs(value, type);
return value;
}
});
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
//load image object: url -> image
cache.testGetSet("__TEST__image").then(_ => {
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
test.equal(srcToImage, 1, "Conversion happened.");
test.equal(urlDestroy, 1, "Url destructor called.");
test.equal(imageDestroy, 0, "Image destructor not called.");
return cache.testGetSet("__TEST__canvas");
}).then(_ => { //path image -> canvas
test.equal(OpenSeadragon.type(cache.data), "canvas", "Got canvas object after conversion.");
test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
test.equal(urlDestroy, 1, "Url destructor not called.");
test.equal(imageDestroy, 1, "Image destructor called.");
return cache.testGetSet("__TEST__image");
}).then(_ => { //path canvas, image: canvas -> url -> image
test.equal(OpenSeadragon.type(cache.data), "image", "Got image object after conversion.");
test.equal(srcToImage, 2, "Conversion ulr->image happened.");
test.equal(imageToCanvas, 1, "Conversion image->canvas did not happened.");
test.equal(context2DtoImage, 0, "Conversion c2d->image did not happened.");
test.equal(canvasToContext2D, 0, "Conversion canvas->c2d did not happened.");
test.equal(canvasToUrl, 1, "Conversion canvas->url happened.");
test.equal(imageToUrl, 0, "Conversion image->url did not happened.");
test.equal(urlDestroy, 2, "Url destructor called.");
test.equal(imageDestroy, 1, "Image destructor not called.");
test.equal(canvasDestroy, 1, "Canvas destructor called.");
test.equal(contex2DDestroy, 0, "Image destructor not called.");
}).then(_ => {
cache.destroy();
test.equal(urlDestroy, 2, "Url destructor not called.");
test.equal(imageDestroy, 2, "Image destructor called.");
test.equal(canvasDestroy, 1, "Canvas destructor not called.");
test.equal(contex2DDestroy, 0, "Image destructor not called.");
done();
});
});
QUnit.test('Deletion cache after a copy was requested but not yet processed.', function (test) {
const done = test.async();
let conversionHappened = false;
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", (tile, value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
conversionHappened = true;
resolve("modified " + value);
}, 20);
});
}, 1, 1);
let longConversionDestroy = 0;
Convertor.learnDestroy("__TEST__longConversionProcessForTesting", _ => {
longConversionDestroy++;
});
const dummyTile = MockSeadragon.getTile("", MockSeadragon.getTiledImage(), {cacheKey: "key"});
const cache = MockSeadragon.getCacheRecord();
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
cache.getDataAs("__TEST__longConversionProcessForTesting").then(convertedData => {
test.equal(longConversionDestroy, 1, "Copy already destroyed.");
test.notOk(cache.loaded, "Cache was destroyed.");
test.equal(cache.data, undefined, "Already destroyed cache does not return data.");
test.equal(urlDestroy, 1, "Url was destroyed.");
test.ok(conversionHappened, "Conversion was fired.");
//destruction will likely happen after we finish current async callback
setTimeout(async () => {
test.equal(longConversionDestroy, 1, "Copy destroyed.");
done();
}, 25);
});
test.ok(cache.loaded, "Cache is still not loaded.");
test.equal(cache.data, "/test/data/A.png", "Get data does not override cache.");
test.equal(cache.type, "__TEST__url", "Cache did not change its type.");
cache.destroy();
test.notOk(cache.type, "Type erased immediatelly as the data copy is out.");
test.equal(urlDestroy, 1, "We destroyed cache before copy conversion finished.");
});
QUnit.test('Deletion cache while being in the conversion process', function (test) {
const done = test.async();
let conversionHappened = false;
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", (tile, value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
conversionHappened = true;
resolve("modified " + value);
}, 20);
});
}, 1, 1);
let destructionHappened = false;
Convertor.learnDestroy("__TEST__longConversionProcessForTesting", _ => {
destructionHappened = true;
});
const dummyTile = MockSeadragon.getTile("", MockSeadragon.getTiledImage(), {cacheKey: "key"});
const cache = MockSeadragon.getCacheRecord();
cache.addTile(dummyTile, "/test/data/A.png", "__TEST__url");
cache.transformTo("__TEST__longConversionProcessForTesting").then(_ => {
test.ok(conversionHappened, "Interrupted conversion finished.");
test.ok(cache.loaded, "Cache is loaded.");
test.equal(cache.data, "modified /test/data/A.png", "We got the correct data.");
test.equal(cache.type, "__TEST__longConversionProcessForTesting", "Cache declares new type.");
test.equal(urlDestroy, 1, "Url was destroyed.");
//destruction will likely happen after we finish current async callback
setTimeout(() => {
test.ok(destructionHappened, "Interrupted conversion finished.");
done();
}, 25);
});
test.ok(!cache.loaded, "Cache is still not loaded.");
test.equal(cache.data, undefined, "Cache is still not loaded.");
test.equal(cache.type, "__TEST__longConversionProcessForTesting", "Cache already declares new type.");
cache.destroy();
test.equal(cache.type, "__TEST__longConversionProcessForTesting",
"Type not erased immediatelly as we still process the data.");
test.ok(!conversionHappened, "We destroyed cache before conversion finished.");
});
})();

View File

@ -3,6 +3,21 @@
<head>
<meta charset="utf-8">
<title>OpenSeadragon QUnit</title>
<script type="text/javascript">
function isInteractiveMode() {
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const isHeadless = typeof navigator !== 'undefined' && navigator.webdriver;
return isBrowser && !isHeadless;
}
window.QUnit = {
config: {
//five seconds timeout due to problems with untrusted events (e.g. auto zoom) for non-interactive runs
//there is timeWatcher property but sometimes tests do not respect it, or they get stuck due to bugs
testTimeout: isInteractiveMode() ? 30000 : 10000
}
};
</script>
<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/helpers/test.css">
@ -16,6 +31,7 @@
<script src="/test/lib/jquery.simulate.js"></script>
<script src="/build/openseadragon/openseadragon.min.js"></script>
<script src="/test/helpers/legacy.mouse.shim.js"></script>
<script src="/test/helpers/mocks.js"></script>
<script src="/test/helpers/test.js"></script>
<script src="/test/helpers/touch.js"></script>
@ -25,6 +41,7 @@
<script src="/test/modules/event-source.js"></script>
<script src="/test/modules/viewerretrieval.js"></script>
<script src="/test/modules/basic.js"></script>
<script src="/test/modules/type-conversion.js"></script>
<script src="/test/modules/strings.js"></script>
<script src="/test/modules/formats.js"></script>
<script src="/test/modules/iiif.js"></script>
@ -49,6 +66,7 @@
<script src="/test/modules/ajax-post-data.js"></script>
<script src="/test/modules/imageloader.js"></script>
<script src="/test/modules/tilesource-dynamic-url.js"></script>
<script src="/test/modules/data-manipulation.js"></script>
<!--The navigator tests are the slowest (for now; hopefully they can be sped up)
so we put them last. -->
<script src="/test/modules/navigator.js"></script>