mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-02-22 17:53:13 +03:00
Merge pull request #2407 from RationAI/cache-overhaul-reviewed
Cache Overhaul for OpenSeadragon (reviewed).
This commit is contained in:
commit
64bb7e25c7
@ -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
|
||||
|
18
Gruntfile.js
18
Gruntfile.js
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
BIN
assets/logos/bbmri-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
@ -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:
|
||||
|
||||
|
@ -46,6 +46,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"prepare": "grunt build"
|
||||
"prepare": "grunt build",
|
||||
"build": "grunt build",
|
||||
"dev": "grunt dev"
|
||||
}
|
||||
}
|
||||
|
@ -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
488
src/datatypeconvertor.js
Normal 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));
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 ));
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
} );
|
||||
|
||||
|
@ -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));
|
||||
|
||||
|
||||
|
@ -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
362
src/priorityqueue.js
Normal 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));
|
525
src/tile.js
525
src/tile.js
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
1503
src/tilecache.js
1503
src/tilecache.js
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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 ));
|
||||
|
208
src/world.js
208
src/world.js
@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
300
style.css
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
33
test/demo/basic-html-drawer.html
Normal file
33
test/demo/basic-html-drawer.html
Normal 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>
|
@ -41,8 +41,9 @@
|
||||
constrainDuringPan: true,
|
||||
visibilityRatio: 1,
|
||||
prefixUrl: "../../build/openseadragon/images/",
|
||||
minZoomImageRatio: 1
|
||||
minZoomImageRatio: 1,
|
||||
crossOriginPolicy: 'Anonymous',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
843
test/demo/filtering-plugin/demo.js
Normal file
843
test/demo/filtering-plugin/demo.js
Normal 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 + '"> ? </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;
|
||||
});
|
82
test/demo/filtering-plugin/index.html
Normal file
82
test/demo/filtering-plugin/index.html
Normal 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>
|
337
test/demo/filtering-plugin/plugin.js
Normal file
337
test/demo/filtering-plugin/plugin.js
Normal 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);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
BIN
test/demo/filtering-plugin/static/minus.png
Normal file
BIN
test/demo/filtering-plugin/static/minus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 171 B |
BIN
test/demo/filtering-plugin/static/plus.png
Normal file
BIN
test/demo/filtering-plugin/static/plus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 240 B |
81
test/demo/filtering-plugin/style.css
Normal file
81
test/demo/filtering-plugin/style.css
Normal 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;
|
||||
}
|
@ -59,7 +59,8 @@
|
||||
this.viewer = OpenSeadragon({
|
||||
id: "contentDiv",
|
||||
prefixUrl: "../../build/openseadragon/images/",
|
||||
tileSources: tileSources
|
||||
tileSources: tileSources,
|
||||
crossOriginPolicy: 'Anonymous',
|
||||
});
|
||||
|
||||
this.viewer.addHandler('open', function() {
|
||||
|
@ -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 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
143
test/demo/plugin-data-modification-interaction.html
Normal file
143
test/demo/plugin-data-modification-interaction.html
Normal 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>
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
76
test/helpers/drawer-switcher.js
Normal file
76
test/helpers/drawer-switcher.js
Normal 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 <= 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}
|
||||
<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
95
test/helpers/mocks.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
} )();
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -49,7 +49,8 @@
|
||||
loadTilesWithAjax: true,
|
||||
ajaxHeaders: {
|
||||
'X-Viewer-Header': 'ViewerHeaderValue'
|
||||
}
|
||||
},
|
||||
callTileLoadedWithCachedData: true
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
} );
|
||||
|
260
test/modules/data-manipulation.js
Normal file
260
test/modules/data-manipulation.js
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}());
|
@ -2,8 +2,7 @@
|
||||
|
||||
(function() {
|
||||
var viewer;
|
||||
const drawerTypes = ['webgl','canvas','html'];
|
||||
drawerTypes.forEach(runDrawerTests);
|
||||
OpenSeadragon.getBuiltInDrawersForTest().forEach(runDrawerTests);
|
||||
|
||||
function runDrawerTests(drawerType){
|
||||
|
||||
|
@ -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.'
|
||||
});
|
||||
});
|
||||
} )();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
389
test/modules/type-conversion.js
Normal file
389
test/modules/type-conversion.js
Normal 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.");
|
||||
});
|
||||
})();
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user