mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-04-04 22:33:32 +03:00
Compare commits
166 commits
Author | SHA1 | Date | |
---|---|---|---|
|
99fcb408fe | ||
|
479ef0ec82 | ||
|
9e01a1e18b | ||
|
2e3f0923f5 | ||
|
3550dfafd3 | ||
|
12de02938a | ||
|
8f093f54df | ||
|
6bdd186219 | ||
|
c54c292873 | ||
|
4c7d9a2ffa | ||
|
595066abd1 | ||
|
b4e14d4804 | ||
|
be87b47469 | ||
|
62fa378f51 | ||
|
c386eca94d | ||
|
8c6d1f4f88 | ||
|
3cb5152905 | ||
|
5cc76737b4 | ||
|
f312bae614 | ||
|
97d388be7d | ||
|
fd27327737 | ||
|
5c60f669ec | ||
|
6ceac75b7a | ||
|
2387c3afd5 | ||
|
5184ed6c9f | ||
|
b22b689dae | ||
|
138215e22f | ||
|
22e766bd1a | ||
|
337af91373 | ||
|
ced1bde338 | ||
|
dbc72d4d98 | ||
|
994f0e25a4 | ||
|
71c3c8437a | ||
|
6e81bedc39 | ||
|
a9562fbb64 | ||
|
b4c7e75109 | ||
|
301191a0b8 | ||
|
edb422007c | ||
|
f8aad5f237 | ||
|
1286fa4549 | ||
|
03b7c5b9a6 | ||
|
7dfde9da3a | ||
|
ffe12888cf | ||
|
8b30e7a307 | ||
|
306adb9b6a | ||
|
226a44c498 | ||
|
b1feb367b8 | ||
|
cb06a5c0fb | ||
|
9b8b4c2728 | ||
|
d6740dab25 | ||
|
003dd3a9da | ||
|
a1fa790e19 | ||
|
3aaa8fa721 | ||
|
39671f4d78 | ||
|
426700b1c6 | ||
|
6315662078 | ||
|
e193557e56 | ||
|
b8b6a9656a | ||
|
f5f9eab200 | ||
|
3f22b6aa3a | ||
|
641e085259 | ||
|
9527f15f52 | ||
|
865c3884b2 | ||
|
56d0d982da | ||
|
58f4e6a36e | ||
|
ef0f14a03d | ||
|
d31562e34f | ||
|
ad943e5472 | ||
|
4c2b0715af | ||
|
a5c569ab5d | ||
|
8fdd639bf0 | ||
|
a0fcfc20ad | ||
|
15627ee18e | ||
|
33c3d4b380 | ||
|
74ffb66f40 | ||
|
55f53e8369 | ||
|
acb7563257 | ||
|
c06e719198 | ||
|
64bb7e25c7 | ||
|
c392f2d205 | ||
|
45bd3f48f9 | ||
|
bee3c243fb | ||
|
563c7674c7 | ||
|
fec033f9d2 | ||
|
e12a349f52 | ||
|
bee5cb2471 | ||
|
c8c7b481b8 | ||
|
039ffbd37a | ||
|
1a7a5ee1d6 | ||
|
d6bb8d3bd1 | ||
|
271f437568 | ||
|
85e8b381b8 | ||
|
ef7628f098 | ||
|
17f13885c7 | ||
|
6b4c0f873a | ||
|
8b16628950 | ||
|
f03f2a5d31 | ||
|
ce4b16616d | ||
|
af9bf9e07f | ||
|
1851405fcf | ||
|
0bc7deccd7 | ||
|
e24f7d1358 | ||
|
541fe2e4df | ||
|
e059b8982e | ||
|
3b1b2d6d23 | ||
|
535507568f | ||
|
cc7474ec9b | ||
|
9bfdd55b2e | ||
|
5fdeb382ea | ||
|
cf65f1a4f4 | ||
|
f127014f0f | ||
|
cd60aff5dc | ||
|
207bc88aab | ||
|
6cbe359398 | ||
|
20177116e7 | ||
|
e403e29312 | ||
|
82e1160508 | ||
|
d5cdf59993 | ||
|
3c6c7e0ab7 | ||
|
68f0ed8901 | ||
|
b3cdeabf02 | ||
|
0cd17abafd | ||
|
1e47bd6add | ||
|
bf25e2f069 | ||
|
06ac68d00e | ||
|
1b6fea72d8 | ||
|
0b63a943b6 | ||
|
f8e5cff117 | ||
|
b6693ee50d | ||
|
3d21ec897b | ||
|
2033814227 | ||
|
e3af370832 | ||
|
63180a1589 | ||
|
c04b6af937 | ||
|
29b01cf1bd | ||
|
cba40f4db8 | ||
|
1b6f79661b | ||
|
999ff30e74 | ||
|
0a035afc2d | ||
|
cdb89ff5ad | ||
|
e0f442209b | ||
|
e2c633a23b | ||
|
47419a090a | ||
|
52ef8156c0 | ||
|
a9b50a8fdb | ||
|
135fa76fde | ||
|
360f0d6796 | ||
|
63f0adbc15 | ||
|
d91df0126b | ||
|
cae6ec6bee | ||
|
a97fe34d74 | ||
|
9ef2d46e75 | ||
|
fcf20be8ea | ||
|
3fa13570ef | ||
|
3d6eb1b91c | ||
|
cf2413e0c9 | ||
|
a690b50eee | ||
|
90ce0669c5 | ||
|
2c67860c61 | ||
|
2a1090ffa8 | ||
|
219049976c | ||
|
c3ab9a08e7 | ||
|
023a864a36 | ||
|
f796925ae5 | ||
|
750d45be81 | ||
|
f01a7a4b3c |
69 changed files with 8343 additions and 1581 deletions
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
10
README.md
10
README.md
|
@ -27,3 +27,13 @@ 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="BBMRI ERIC Logo" src="assets/logos/bbmri-logo.png" height="70" /></a>
|
||||
|
||||
<a href="https://www.pitt.edu/"><img alt="University of Pittsburgh Logo" src="assets/logos/pitt-logo.png" height="70" /></a>
|
||||
|
||||
<a href="https://www.stanford.edu/"><img alt="Stanford University Logo" src="assets/logos/stanford-logo.png" height="70" /></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 |
BIN
assets/logos/pitt-logo.png
Normal file
BIN
assets/logos/pitt-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
BIN
assets/logos/stanford-logo.png
Normal file
BIN
assets/logos/stanford-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
|
@ -1,6 +1,40 @@
|
|||
OPENSEADRAGON CHANGELOG
|
||||
=======================
|
||||
|
||||
6.0.0: (in progress...)
|
||||
|
||||
* NEW BEHAVIOR: OpenSeadragon Data Pipeline Overhaul (#2407, #2643 @Aiosa)
|
||||
* 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.
|
||||
* New option: loadDestinationTilesOnAnimation. With it on, during animations, OSD loads tiles in the destination region, rather than the areas passed through on the way to the destination. This new feature is on by default. (#2686, #2690 @MichaelWGibson)
|
||||
* Overlay wrapper elements now have a "openseadragon-overlay-wrapper" class. If the overlay element has an ID, the wrapper gets a variant on that ID, but if the overlay element does not have an ID, we no longer give the wrapper an ID. (#2698 @lokaesshwar)
|
||||
* The functions the viewer uses to operate the zoom in and zoom out buttons are now accessible to be called programatically (#2702 @achu1998)
|
||||
* Improved how OpenSeadragon is imported in various environments (#2644 @Aiosa)
|
||||
* Improved documentation (#2676 @bennlich)
|
||||
* Improved unit tests (#2640 @harshkg23)
|
||||
* Fixed: Transparency detection didn't always work properly (#2636 @pcram-techcyte)
|
||||
* Fixed: MouseTracker's hasGestureHandlers and hasScrollHandler values were not getting updated upon dynamically adding/removing handlers (#2649 @Seafret)
|
||||
* Fixed: Sometimes images wouldn't update when you changed their opacity (#2652 @pearcetm)
|
||||
* Fixed: Possible MouseTracker hash collision (#2657 @cff29546)
|
||||
|
||||
5.0.1:
|
||||
|
||||
* Improved overlay handling so it plays better with other libraries (#2582 @BeebBenjamin)
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -5217,4 +5217,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,21 @@
|
|||
|
||||
(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 custom type conversion atop
|
||||
* what cache performs. In that case, drawer must implement internalCacheCreate() which gets data in one
|
||||
* of formats the drawer declares as supported. This method must return object to be used during drawing.
|
||||
* You should probably implement also internalCacheFree() to provide cleanup logics.
|
||||
*
|
||||
* @property {boolean} [preloadCache=true]
|
||||
* When internalCacheCreate is used, it can be applied offline (asynchronously) during data processing = preloading,
|
||||
* or just in time before rendering (if necessary). Preloading supports
|
||||
*/
|
||||
|
||||
const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
|
||||
/**
|
||||
* @class OpenSeadragon.DrawerBase
|
||||
* @classdesc Base class for Drawers that handle rendering of tiles for an {@link OpenSeadragon.Viewer}.
|
||||
|
@ -51,10 +65,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 +92,42 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||
this.container.style.textAlign = "left";
|
||||
this.container.appendChild( this.canvas );
|
||||
|
||||
this._checkForAPIOverrides();
|
||||
this._checkInterfaceImplementation();
|
||||
this.setInternalCacheNeedsRefresh(); // initializes stamp
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
preloadCache: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 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 +137,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.
|
||||
|
@ -140,6 +216,15 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||
$.console.error('Drawer.destroy must be implemented by child class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy internal cache. Should be called within destroy() when
|
||||
* usePrivateCache is set to true. Ensures cleanup of anything created
|
||||
* by internalCacheCreate(...).
|
||||
*/
|
||||
destroyInternalCache() {
|
||||
this.viewer.tileCache.clearDrawerInternalCache(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TiledImage} tiledImage the tiled image that is calling the function
|
||||
* @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
|
||||
|
@ -149,7 +234,6 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
|
||||
|
@ -174,6 +258,31 @@ OpenSeadragon.DrawerBase = class DrawerBase{
|
|||
$.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.');
|
||||
}
|
||||
|
||||
/**
|
||||
* If options.usePrivateCache is true, this method MUST RETURN the private cache content
|
||||
* @param {OpenSeadragon.CacheRecord} cache
|
||||
* @param {OpenSeadragon.Tile} tile
|
||||
* @return any
|
||||
*/
|
||||
internalCacheCreate(cache, tile) {}
|
||||
|
||||
/**
|
||||
* It is possible to perform any necessary cleanup on internal cache, necessary if you
|
||||
* need to clean up some memory (e.g. destroy canvas by setting with & height to 0).
|
||||
* @param {*} data object returned by internalCacheCreate(...)
|
||||
*/
|
||||
internalCacheFree(data) {}
|
||||
|
||||
/**
|
||||
* Call to invalidate internal cache. It will be rebuilt. With synchronous converions,
|
||||
* it will be rebuilt immediatelly. With asynchronous, it will be rebuilt once invalidation
|
||||
* routine happens, e.g. you should call also requestInvalidate() if you need to happen
|
||||
* it as soon as possible.
|
||||
*/
|
||||
setInternalCacheNeedsRefresh() {
|
||||
this._dataNeedsRefresh = $.now();
|
||||
}
|
||||
|
||||
// Private functions
|
||||
|
||||
/**
|
||||
|
@ -183,20 +292,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 +411,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;
|
||||
}
|
||||
} );
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
this.hash = Math.random(); // An unique hash for this tracker.
|
||||
this.hash = uniqueHash(); // An unique hash for this tracker.
|
||||
/**
|
||||
* The element for which pointer events are being monitored.
|
||||
* @member {Element} element
|
||||
|
@ -277,13 +277,6 @@
|
|||
sentDragEvent: false
|
||||
};
|
||||
|
||||
this.hasGestureHandlers = !!( this.pressHandler || this.nonPrimaryPressHandler ||
|
||||
this.releaseHandler || this.nonPrimaryReleaseHandler ||
|
||||
this.clickHandler || this.dblClickHandler ||
|
||||
this.dragHandler || this.dragEndHandler ||
|
||||
this.pinchHandler );
|
||||
this.hasScrollHandler = !!this.scrollHandler;
|
||||
|
||||
if ( $.MouseTracker.havePointerEvents ) {
|
||||
$.setElementPointerEvents( this.element, 'auto' );
|
||||
}
|
||||
|
@ -391,6 +384,30 @@
|
|||
return count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do we currently have any assigned gesture handlers.
|
||||
* @returns {Boolean} Do we currently have any assigned gesture handlers.
|
||||
*/
|
||||
get hasGestureHandlers() {
|
||||
return !!(this.pressHandler ||
|
||||
this.nonPrimaryPressHandler ||
|
||||
this.releaseHandler ||
|
||||
this.nonPrimaryReleaseHandler ||
|
||||
this.clickHandler ||
|
||||
this.dblClickHandler ||
|
||||
this.dragHandler ||
|
||||
this.dragEndHandler ||
|
||||
this.pinchHandler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Do we currently have a scroll handler.
|
||||
* @returns {Boolean} Do we currently have a scroll handler.
|
||||
*/
|
||||
get hasScrollHandler() {
|
||||
return !!this.scrollHandler;
|
||||
},
|
||||
|
||||
/**
|
||||
* Implement or assign implementation to these handlers during or after
|
||||
* calling the constructor.
|
||||
|
@ -3764,4 +3781,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @private
|
||||
* @inner
|
||||
*/
|
||||
function uniqueHash( ) {
|
||||
let uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
while (uniqueId in THIS) {
|
||||
// rehash when not unique
|
||||
uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
return uniqueId;
|
||||
}
|
||||
|
||||
}(OpenSeadragon));
|
||||
|
|
|
@ -355,6 +355,9 @@
|
|||
* Specifies the animation duration per each {@link OpenSeadragon.Spring}
|
||||
* which occur when the image is dragged, zoomed or rotated.
|
||||
*
|
||||
* @property {Boolean} [loadDestinationTilesOnAnimation=true]
|
||||
* If true, tiles are loaded only at the destination of an animation.
|
||||
* If false, tiles are loaded along the animation path during the animation.
|
||||
* @property {OpenSeadragon.GestureSettings} [gestureSettingsMouse]
|
||||
* Settings for gestures generated by a mouse pointer device. (See {@link OpenSeadragon.GestureSettings})
|
||||
* @property {Boolean} [gestureSettingsMouse.dragToPan=true] - Pan on drag gesture
|
||||
|
@ -722,6 +725,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 +769,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 +876,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 +1083,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 +1256,7 @@ function OpenSeadragon( options ){
|
|||
loadTilesWithAjax: false,
|
||||
ajaxHeaders: {},
|
||||
splitHashDataForPost: false,
|
||||
callTileLoadedWithCachedData: false,
|
||||
|
||||
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
|
||||
panHorizontal: true,
|
||||
|
@ -1252,6 +1278,7 @@ function OpenSeadragon( options ){
|
|||
dblClickDistThreshold: 20,
|
||||
springStiffness: 6.5,
|
||||
animationTime: 1.2,
|
||||
loadDestinationTilesOnAnimation: true,
|
||||
gestureSettingsMouse: {
|
||||
dragToPan: true,
|
||||
scrollToZoom: true,
|
||||
|
@ -2296,29 +2323,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 +2371,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 +2387,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 +2408,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 +2636,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,21 +2908,143 @@ 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));
|
||||
|
||||
|
||||
// Universal Module Definition, supports CommonJS, AMD and simple script tag
|
||||
(function (root, factory) {
|
||||
(function (root, $) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// expose as amd module
|
||||
define([], factory);
|
||||
define([], function () {
|
||||
return $;
|
||||
});
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// expose as commonjs module
|
||||
module.exports = factory();
|
||||
module.exports = $;
|
||||
} else {
|
||||
if (!root) {
|
||||
root = typeof window === 'object' && window;
|
||||
if (!root) {
|
||||
$.console.error("OpenSeadragon must run in browser environment!");
|
||||
}
|
||||
}
|
||||
// expose as window.OpenSeadragon
|
||||
root.OpenSeadragon = factory();
|
||||
root.OpenSeadragon = $;
|
||||
}
|
||||
}(this, function () {
|
||||
return OpenSeadragon;
|
||||
}));
|
||||
}(this, 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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -133,11 +133,12 @@
|
|||
this.elementWrapper.appendChild(this.element);
|
||||
|
||||
if (this.element.id) {
|
||||
this.elementWrapper.id = "overlay-wrapper-" + this.element.id;
|
||||
} else {
|
||||
this.elementWrapper.id = "overlay-wrapper";
|
||||
this.elementWrapper.id = "overlay-wrapper-" + this.element.id; // Unique ID if element has one
|
||||
}
|
||||
|
||||
// Always add a class for styling & selection
|
||||
this.elementWrapper.classList.add("openseadragon-overlay-wrapper");
|
||||
|
||||
this.style = this.elementWrapper.style;
|
||||
this._init(options);
|
||||
};
|
||||
|
|
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));
|
527
src/tile.js
527
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,59 @@ $.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];
|
||||
},
|
||||
|
||||
this.element = null;
|
||||
this.imgElement = null;
|
||||
/**
|
||||
* 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.loaded = false;
|
||||
this.loading = false;
|
||||
this._cKey = this._ocKey;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
1563
src/tilecache.js
1563
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?
|
||||
|
@ -175,6 +176,7 @@ $.TiledImage = function( options ) {
|
|||
wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
|
||||
wrapVertical: $.DEFAULT_SETTINGS.wrapVertical,
|
||||
immediateRender: $.DEFAULT_SETTINGS.immediateRender,
|
||||
loadDestinationTilesOnAnimation: $.DEFAULT_SETTINGS.loadDestinationTilesOnAnimation,
|
||||
blendTime: $.DEFAULT_SETTINGS.blendTime,
|
||||
alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend,
|
||||
minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio,
|
||||
|
@ -188,7 +190,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 +232,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 +281,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 +345,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 +374,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 +925,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();
|
||||
|
@ -980,6 +998,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
|
||||
this._opacity = opacity;
|
||||
this._needsDraw = true;
|
||||
this._needsUpdate = true;
|
||||
/**
|
||||
* Raised when the TiledImage's opacity is changed.
|
||||
* @event opacity-change
|
||||
|
@ -1066,6 +1085,19 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
return drawArea;
|
||||
},
|
||||
|
||||
getLoadArea: function() {
|
||||
var loadArea = this._viewportToTiledImageRectangle(
|
||||
this.viewport.getBoundsWithMargins(false));
|
||||
|
||||
if (!this.wrapHorizontal && !this.wrapVertical) {
|
||||
var tiledImageBounds = this._viewportToTiledImageRectangle(
|
||||
this.getClippedBounds(false));
|
||||
loadArea = loadArea.intersection(tiledImageBounds);
|
||||
}
|
||||
|
||||
return loadArea;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Array} Array of Tiles that make up the current view
|
||||
|
@ -1162,7 +1194,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 +1259,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);
|
||||
|
@ -1317,6 +1361,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
var highestLevel = levelsInterval.highestLevel; // the highest level we should draw at our current zoom
|
||||
var bestTiles = [];
|
||||
var drawArea = this.getDrawArea();
|
||||
var loadArea = drawArea;
|
||||
|
||||
if (this.loadDestinationTilesOnAnimation) {
|
||||
loadArea = this.getLoadArea();
|
||||
}
|
||||
var currentTime = $.now();
|
||||
|
||||
// reset each tile's beingDrawn flag
|
||||
|
@ -1404,6 +1453,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
levelOpacity,
|
||||
levelVisibility,
|
||||
drawArea,
|
||||
loadArea,
|
||||
currentTime,
|
||||
bestTiles
|
||||
);
|
||||
|
@ -1433,20 +1483,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
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1560,16 +1606,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
* @param {Number} levelOpacity
|
||||
* @param {Number} levelVisibility
|
||||
* @param {OpenSeadragon.Rect} drawArea
|
||||
* @param {OpenSeadragon.Rect} loadArea
|
||||
* @param {Number} currentTime
|
||||
* @param {OpenSeadragon.Tile[]} best Array of the current best tiles
|
||||
* @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile - the current "best" tiles to draw, updatedTiles: OpenSeadragon.Tile) - the updated tiles}.
|
||||
*/
|
||||
_updateLevel: function(level, levelOpacity,
|
||||
levelVisibility, drawArea, currentTime, best) {
|
||||
|
||||
var topLeftBound = drawArea.getBoundingBox().getTopLeft();
|
||||
var bottomRightBound = drawArea.getBoundingBox().getBottomRight();
|
||||
levelVisibility, drawArea, loadArea, currentTime, best) {
|
||||
|
||||
var drawTopLeftBound = drawArea.getBoundingBox().getTopLeft();
|
||||
var drawBottomRightBound = drawArea.getBoundingBox().getBottomRight();
|
||||
if (this.viewer) {
|
||||
/**
|
||||
* <em>- Needs documentation -</em>
|
||||
|
@ -1597,24 +1643,44 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
opacity: levelOpacity,
|
||||
visibility: levelVisibility,
|
||||
drawArea: drawArea,
|
||||
topleft: topLeftBound,
|
||||
bottomright: bottomRightBound,
|
||||
topleft: drawTopLeftBound,
|
||||
bottomright: drawBottomRightBound,
|
||||
currenttime: currentTime,
|
||||
best: best
|
||||
});
|
||||
}
|
||||
|
||||
this._resetCoverage(this.coverage, level);
|
||||
this._resetCoverage(this.loadingCoverage, level);
|
||||
var updatedTiles = this._updateDrawArea(level,
|
||||
levelVisibility, drawArea, currentTime);
|
||||
|
||||
if (loadArea) {
|
||||
best = this._updateLoadArea(level, loadArea, currentTime, best);
|
||||
}
|
||||
|
||||
return {
|
||||
bestTiles: best,
|
||||
updatedTiles: updatedTiles
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Visit all tiles in an a given area on a given level.
|
||||
* @private
|
||||
* @param {Number} level
|
||||
* @param {OpenSeadragon.Rect} area
|
||||
* @param {Function} callback - TiledImage, x, y, total
|
||||
*/
|
||||
_visitTiles: function(level, area, callback) {
|
||||
var topLeftBound = area.getBoundingBox().getTopLeft();
|
||||
var bottomRightBound = area.getBoundingBox().getBottomRight();
|
||||
|
||||
var drawCornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound);
|
||||
var drawTopLeftTile = drawCornerTiles.topLeft;
|
||||
var drawBottomRightTile = drawCornerTiles.bottomRight;
|
||||
|
||||
|
||||
//OK, a new drawing so do your calculations
|
||||
var cornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound);
|
||||
var topLeftTile = cornerTiles.topLeft;
|
||||
var bottomRightTile = cornerTiles.bottomRight;
|
||||
var numberOfTiles = this.source.getNumTiles(level);
|
||||
|
||||
var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter());
|
||||
|
||||
if (this.getFlip()) {
|
||||
// The right-most tile can be narrower than the others. When flipped,
|
||||
// this tile is now on the left. Because it is narrower than the normal
|
||||
|
@ -1622,16 +1688,15 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
// fill the viewport. Fix this by rendering an extra column of tiles. If we
|
||||
// are not wrapping, make sure we never render more than the number of tiles
|
||||
// in the image.
|
||||
bottomRightTile.x += 1;
|
||||
drawBottomRightTile.x += 1;
|
||||
if (!this.wrapHorizontal) {
|
||||
bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
|
||||
drawBottomRightTile.x = Math.min(drawBottomRightTile.x, numberOfTiles.x - 1);
|
||||
}
|
||||
}
|
||||
var numTiles = Math.max(0, (bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y));
|
||||
var tiles = new Array(numTiles);
|
||||
var tileIndex = 0;
|
||||
for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) {
|
||||
for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) {
|
||||
var numTiles = Math.max(0, (drawBottomRightTile.x - drawTopLeftTile.x) * (drawBottomRightTile.y - drawTopLeftTile.y));
|
||||
|
||||
for (var x = drawTopLeftTile.x; x <= drawBottomRightTile.x; x++) {
|
||||
for (var y = drawTopLeftTile.y; y <= drawBottomRightTile.y; y++) {
|
||||
|
||||
var flippedX;
|
||||
if (this.getFlip()) {
|
||||
|
@ -1641,30 +1706,83 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
flippedX = x;
|
||||
}
|
||||
|
||||
if (drawArea.intersection(this.getTileBounds(level, flippedX, y)) === null) {
|
||||
// This tile is outside of the viewport, no need to draw it
|
||||
if (area.intersection(this.getTileBounds(level, flippedX, y)) === null) {
|
||||
// This tile is not in the draw area
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = this._updateTile(
|
||||
flippedX, y,
|
||||
level,
|
||||
levelVisibility,
|
||||
viewportCenter,
|
||||
numberOfTiles,
|
||||
currentTime,
|
||||
best
|
||||
);
|
||||
best = result.bestTiles;
|
||||
tiles[tileIndex] = result.tile;
|
||||
tileIndex += 1;
|
||||
callback(this, flippedX, y, numTiles);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bestTiles: best,
|
||||
updatedTiles: tiles
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates draw information for all tiles at a given level in the area
|
||||
* @private
|
||||
* @param {Number} level
|
||||
* @param {Number} levelOpacity
|
||||
* @param {Number} levelVisibility
|
||||
* @param {OpenSeadragon.Rect} drawArea
|
||||
* @param {Number} currentTime
|
||||
* @param {OpenSeadragon.Tile[]} best Array of the current best tiles
|
||||
* @returns {OpenSeadragon.Tile[]} Updated tiles
|
||||
*/
|
||||
_updateDrawArea: function(level,
|
||||
levelVisibility, drawArea, currentTime) {
|
||||
var numberOfTiles = this.source.getNumTiles(level);
|
||||
var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter());
|
||||
this._resetCoverage(this.coverage, level);
|
||||
|
||||
var tiles = null;
|
||||
var tileIndex = 0;
|
||||
|
||||
this._visitTiles(level, drawArea, function(tiledImage, x, y, total) {
|
||||
if (!tiles) {
|
||||
tiles = new Array(total);
|
||||
}
|
||||
tiles[tileIndex] = tiledImage._updateTile(
|
||||
x, y,
|
||||
level,
|
||||
levelVisibility,
|
||||
viewportCenter,
|
||||
numberOfTiles,
|
||||
currentTime
|
||||
);
|
||||
|
||||
tileIndex += 1;
|
||||
|
||||
});
|
||||
|
||||
return tiles;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates load information for all tiles at a given level in the area
|
||||
* @private
|
||||
* @param {Number} level
|
||||
* @param {OpenSeadragon.Rect} loadArea
|
||||
* @param {Number} currentTime
|
||||
* @param {OpenSeadragon.Tile[]} best Array of the current best tiles to load
|
||||
* @returns {OpenSeadragon.Tile[]} The new best tiles to load
|
||||
*/
|
||||
_updateLoadArea: function(level, loadArea, currentTime, best) {
|
||||
this._resetCoverage(this.loadingCoverage, level);
|
||||
var numberOfTiles = this.source.getNumTiles(level);
|
||||
|
||||
this._visitTiles(level, loadArea, function(tiledImage, x, y, _) {
|
||||
best = tiledImage._considerTileForLoad(
|
||||
x, y,
|
||||
level,
|
||||
numberOfTiles,
|
||||
currentTime,
|
||||
best
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
return best;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1730,18 +1848,18 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
* @param {OpenSeadragon.Point} viewportCenter
|
||||
* @param {Number} numberOfTiles
|
||||
* @param {Number} currentTime
|
||||
* @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
|
||||
* @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile[] - the current best tiles, tile: OpenSeadragon.Tile the current tile}
|
||||
* @returns {OpenSeadragon.Tile} the updated Tile
|
||||
*/
|
||||
_updateTile: function( x, y, level,
|
||||
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
|
||||
levelVisibility, viewportCenter, numberOfTiles, currentTime){
|
||||
|
||||
var tile = this._getTile(
|
||||
const tile = this._getTile(
|
||||
x, y,
|
||||
level,
|
||||
currentTime,
|
||||
numberOfTiles
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
if( this.viewer ){
|
||||
/**
|
||||
|
@ -1763,19 +1881,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
|
||||
this._setCoverage( this.coverage, level, x, y, false );
|
||||
|
||||
var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y);
|
||||
this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
|
||||
|
||||
if ( !tile.exists ) {
|
||||
return {
|
||||
bestTiles: best,
|
||||
tile: tile
|
||||
};
|
||||
return tile;
|
||||
}
|
||||
if (tile.loaded && tile.opacity === 1){
|
||||
this._setCoverage( this.coverage, level, x, y, true );
|
||||
}
|
||||
|
||||
this._positionTile(
|
||||
tile,
|
||||
this.source.tileOverlap,
|
||||
|
@ -1784,28 +1895,55 @@ $.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());
|
||||
}
|
||||
}
|
||||
return tile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Consider a tile for loading
|
||||
* @private
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
* @param {Number} level
|
||||
* @param {Number} numberOfTiles
|
||||
* @param {Number} currentTime
|
||||
* @param {OpenSeadragon.Tile[]} best - The current "best" tiles to draw.
|
||||
* @returns {OpenSeadragon.Tile[]} - The updated "best" tiles to draw.
|
||||
*/
|
||||
_considerTileForLoad: function( x, y, level, numberOfTiles, currentTime, best){
|
||||
|
||||
const tile = this._getTile(
|
||||
x, y,
|
||||
level,
|
||||
currentTime,
|
||||
numberOfTiles
|
||||
);
|
||||
|
||||
|
||||
var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y);
|
||||
this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
|
||||
|
||||
|
||||
if ( !tile.exists ) {
|
||||
return best;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
bestTiles: best,
|
||||
tile: tile
|
||||
};
|
||||
return best;
|
||||
},
|
||||
|
||||
// private
|
||||
|
@ -1850,6 +1988,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 +2030,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
urlOrGetter,
|
||||
post,
|
||||
ajaxHeaders,
|
||||
context2D,
|
||||
tile,
|
||||
tilesMatrix = this.tilesMatrix,
|
||||
tileSource = this.source;
|
||||
|
@ -1905,9 +2061,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 +2068,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
|
|||
bounds,
|
||||
exists,
|
||||
urlOrGetter,
|
||||
context2D,
|
||||
undefined,
|
||||
this.loadTilesWithAjax,
|
||||
ajaxHeaders,
|
||||
sourceBounds,
|
||||
|
@ -1957,7 +2110,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 +2120,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 +2153,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 +2191,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 +2245,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 +2369,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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
170
src/viewer.js
170
src/viewer.js
|
@ -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({
|
||||
|
@ -1716,6 +1748,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||
wrapHorizontal: _this.wrapHorizontal,
|
||||
wrapVertical: _this.wrapVertical,
|
||||
maxTilesPerFrame: _this.maxTilesPerFrame,
|
||||
loadDestinationTilesOnAnimation: _this.loadDestinationTilesOnAnimation,
|
||||
immediateRender: _this.immediateRender,
|
||||
blendTime: _this.blendTime,
|
||||
alwaysBlend: _this.alwaysBlend,
|
||||
|
@ -1727,7 +1760,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) {
|
||||
|
@ -1971,11 +2005,11 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||
//////////////////////////////////////////////////////////////////////////
|
||||
// Navigation Controls
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
var beginZoomingInHandler = $.delegate( this, beginZoomingIn ),
|
||||
endZoomingHandler = $.delegate( this, endZooming ),
|
||||
doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ),
|
||||
beginZoomingOutHandler = $.delegate( this, beginZoomingOut ),
|
||||
doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ),
|
||||
var beginZoomingInHandler = $.delegate( this, this.startZoomInAction ),
|
||||
endZoomingHandler = $.delegate( this, this.endZoomAction ),
|
||||
doSingleZoomInHandler = $.delegate( this, this.singleZoomInAction ),
|
||||
beginZoomingOutHandler = $.delegate( this, this.startZoomOutAction ),
|
||||
doSingleZoomOutHandler = $.delegate( this, this.singleZoomOutAction ),
|
||||
onHomeHandler = $.delegate( this, onHome ),
|
||||
onFullScreenHandler = $.delegate( this, onFullScreen ),
|
||||
onRotateLeftHandler = $.delegate( this, onRotateLeft ),
|
||||
|
@ -2189,8 +2223,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||
/**
|
||||
* Adds an html element as an overlay to the current viewport. Useful for
|
||||
* highlighting words or areas of interest on an image or other zoomable
|
||||
* interface. The overlays added via this method are removed when the viewport
|
||||
* is closed which include when changing page.
|
||||
* interface. Unless the viewer has been configured with the preserveOverlays
|
||||
* option, overlays added via this method are removed when the viewport
|
||||
* is closed (including in sequence mode when changing page).
|
||||
* @method
|
||||
* @param {Element|String|Object} element - A reference to an element or an id for
|
||||
* the element which will be overlaid. Or an Object specifying the configuration for the overlay.
|
||||
|
@ -2590,6 +2625,69 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
|
|||
isAnimating: function () {
|
||||
return THIS[ this.hash ].animating;
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts continuous zoom-in animation (typically bound to mouse-down on the zoom-in button).
|
||||
* @function
|
||||
* @memberof OpenSeadragon.Viewer.prototype
|
||||
*/
|
||||
startZoomInAction: function () {
|
||||
THIS[ this.hash ].lastZoomTime = $.now();
|
||||
THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
|
||||
THIS[ this.hash ].zooming = true;
|
||||
scheduleZoom( this );
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts continuous zoom-out animation (typically bound to mouse-down on the zoom-out button).
|
||||
* @function
|
||||
* @memberof OpenSeadragon.Viewer.prototype
|
||||
*/
|
||||
startZoomOutAction: function () {
|
||||
THIS[ this.hash ].lastZoomTime = $.now();
|
||||
THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond;
|
||||
THIS[ this.hash ].zooming = true;
|
||||
scheduleZoom( this );
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops any continuous zoom animation (typically bound to mouse-up/leave events on a button).
|
||||
* @function
|
||||
* @memberof OpenSeadragon.Viewer.prototype
|
||||
*/
|
||||
endZoomAction: function () {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs single-step zoom-in operation (typically bound to click/enter on the zoom-in button).
|
||||
* @function
|
||||
* @memberof OpenSeadragon.Viewer.prototype
|
||||
*/
|
||||
singleZoomInAction: function () {
|
||||
if ( this.viewport ) {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
this.viewport.zoomBy(
|
||||
this.zoomPerClick / 1.0
|
||||
);
|
||||
this.viewport.applyConstraints();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs single-step zoom-out operation (typically bound to click/enter on the zoom-out button).
|
||||
* @function
|
||||
* @memberof OpenSeadragon.Viewer.prototype
|
||||
*/
|
||||
singleZoomOutAction: function () {
|
||||
if ( this.viewport ) {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
this.viewport.zoomBy(
|
||||
1.0 / this.zoomPerClick
|
||||
);
|
||||
this.viewport.applyConstraints();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
@ -2697,7 +2795,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);
|
||||
}
|
||||
});
|
||||
|
@ -3922,28 +4020,6 @@ function resolveUrl( prefix, url ) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
function beginZoomingIn() {
|
||||
THIS[ this.hash ].lastZoomTime = $.now();
|
||||
THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
|
||||
THIS[ this.hash ].zooming = true;
|
||||
scheduleZoom( this );
|
||||
}
|
||||
|
||||
|
||||
function beginZoomingOut() {
|
||||
THIS[ this.hash ].lastZoomTime = $.now();
|
||||
THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond;
|
||||
THIS[ this.hash ].zooming = true;
|
||||
scheduleZoom( this );
|
||||
}
|
||||
|
||||
|
||||
function endZooming() {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
}
|
||||
|
||||
|
||||
function scheduleZoom( viewer ) {
|
||||
$.requestAnimationFrame( $.delegate( viewer, doZoom ) );
|
||||
}
|
||||
|
@ -3967,28 +4043,6 @@ function doZoom() {
|
|||
}
|
||||
|
||||
|
||||
function doSingleZoomIn() {
|
||||
if ( this.viewport ) {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
this.viewport.zoomBy(
|
||||
this.zoomPerClick / 1.0
|
||||
);
|
||||
this.viewport.applyConstraints();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function doSingleZoomOut() {
|
||||
if ( this.viewport ) {
|
||||
THIS[ this.hash ].zooming = false;
|
||||
this.viewport.zoomBy(
|
||||
1.0 / this.zoomPerClick
|
||||
);
|
||||
this.viewport.applyConstraints();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function lightUp() {
|
||||
if (this.buttonGroup) {
|
||||
this.buttonGroup.emulateEnter();
|
||||
|
|
|
@ -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,21 @@
|
|||
this._setupCanvases();
|
||||
this._setupRenderer();
|
||||
|
||||
this._supportedFormats = ["context2d", "image"];
|
||||
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,
|
||||
preloadCache: false,
|
||||
};
|
||||
}
|
||||
|
||||
getSupportedDataFormats() {
|
||||
return this._supportedFormats;
|
||||
}
|
||||
|
||||
// Public API required by all Drawer implementations
|
||||
/**
|
||||
|
@ -137,8 +139,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 +156,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;
|
||||
|
||||
|
@ -174,6 +169,8 @@
|
|||
this.viewer.drawer = null;
|
||||
}
|
||||
|
||||
this.destroyInternalCache();
|
||||
|
||||
// set our destroyed flag to true
|
||||
this._destroyed = true;
|
||||
}
|
||||
|
@ -204,7 +201,7 @@
|
|||
|
||||
/**
|
||||
*
|
||||
* @returns 'webgl'
|
||||
* @returns {string} 'webgl'
|
||||
*/
|
||||
getType(){
|
||||
return 'webgl';
|
||||
|
@ -243,6 +240,8 @@
|
|||
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 = this.getDataToDraw.bind(this);
|
||||
}
|
||||
|
||||
return this._backupCanvasDrawer;
|
||||
|
@ -314,7 +313,7 @@
|
|||
tiledImage.debugMode
|
||||
);
|
||||
|
||||
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.hasTransparency;
|
||||
let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.tile.hasTransparency;
|
||||
|
||||
// using the context2d pipeline requires a clean rendering (back) buffer to start
|
||||
if(useContext2dPipeline){
|
||||
|
@ -379,23 +378,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 && textureInfo.texture) {
|
||||
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 +463,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
if(renderingBufferHasImageData){
|
||||
|
@ -491,8 +479,8 @@
|
|||
setImageSmoothingEnabled(enabled){
|
||||
if( this._imageSmoothingEnabled !== enabled ){
|
||||
this._imageSmoothingEnabled = enabled;
|
||||
this._unloadTextures();
|
||||
this.viewer.world.draw();
|
||||
this.setInternalCacheNeedsRefresh();
|
||||
this.viewer.forceRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -830,7 +818,6 @@
|
|||
//bind the frame buffer to the new texture
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0);
|
||||
|
||||
}
|
||||
|
||||
// private
|
||||
|
@ -876,6 +863,98 @@
|
|||
this.viewer.addHandler("resize", this._resizeHandler);
|
||||
}
|
||||
|
||||
internalCacheCreate(cache, tile) {
|
||||
let tiledImage = tile.tiledImage;
|
||||
let gl = this._gl;
|
||||
let texture;
|
||||
let position;
|
||||
|
||||
let data = cache.data;
|
||||
|
||||
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.');
|
||||
this.setInternalCacheNeedsRefresh();
|
||||
} 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);
|
||||
// TextureInfo stored in the cache
|
||||
return {
|
||||
texture: texture,
|
||||
position: position,
|
||||
};
|
||||
} 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.');
|
||||
this.setInternalCacheNeedsRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data instanceof Image) {
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
canvas.width = data.width;
|
||||
canvas.height = data.height;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage( data, 0, 0 );
|
||||
data = context;
|
||||
}
|
||||
if (data instanceof CanvasRenderingContext2D) {
|
||||
return data;
|
||||
}
|
||||
$.console.error("Unsupported data used for WebGL Drawer - probably a bug!");
|
||||
return {};
|
||||
}
|
||||
|
||||
internalCacheFree(data) {
|
||||
if (data && data.texture) {
|
||||
this._gl.deleteTexture(data.texture);
|
||||
data.texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// private
|
||||
_makeQuadVertexBuffer(left, right, top, bottom){
|
||||
return new Float32Array([
|
||||
|
@ -887,92 +966,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 +982,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 +1295,7 @@
|
|||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}( OpenSeadragon ));
|
||||
|
|
205
src/world.js
205
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,206 @@ $.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 drawer = this.viewer.drawer;
|
||||
|
||||
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(drawer).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(drawer).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(drawer).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 +462,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);
|
||||
}
|
||||
};
|
||||
|
|
@ -59,31 +59,31 @@
|
|||
},
|
||||
|
||||
// ----------
|
||||
equalsWithVariance: function ( value1, value2, variance ) {
|
||||
return Math.abs( value1 - value2 ) <= variance;
|
||||
equalsWithVariance: function (actual, expected, variance) {
|
||||
return Math.abs(actual - expected) <= variance;
|
||||
},
|
||||
|
||||
// ----------
|
||||
assessNumericValue: function ( assert, value1, value2, variance, message ) {
|
||||
assert.ok( Util.equalsWithVariance( value1, value2, variance ), message + " Expected:" + value1 + " Found: " + value2 + " Variance: " + variance );
|
||||
assessNumericValue: function (assert, actual, expected, variance, message) {
|
||||
assert.ok(
|
||||
Util.equalsWithVariance(actual, expected, variance),
|
||||
message + " Actual: " + actual + " Expected: " + expected + " Variance: " + variance
|
||||
);
|
||||
},
|
||||
|
||||
// ----------
|
||||
assertPointsEquals: function (assert, pointA, pointB, precision, message) {
|
||||
Util.assessNumericValue(assert, pointA.x, pointB.x, precision, message + " x: ");
|
||||
Util.assessNumericValue(assert, pointA.y, pointB.y, precision, message + " y: ");
|
||||
assertPointsEquals: function (assert, actualPoint, expectedPoint, precision, message) {
|
||||
Util.assessNumericValue(assert, actualPoint.x, expectedPoint.x, precision, message + " x: ");
|
||||
Util.assessNumericValue(assert, actualPoint.y, expectedPoint.y, precision, message + " y: ");
|
||||
},
|
||||
|
||||
// ----------
|
||||
assertRectangleEquals: function (assert, rectA, rectB, precision, message) {
|
||||
Util.assessNumericValue(assert, rectA.x, rectB.x, precision, message + " x: ");
|
||||
Util.assessNumericValue(assert, rectA.y, rectB.y, precision, message + " y: ");
|
||||
Util.assessNumericValue(assert, rectA.width, rectB.width, precision,
|
||||
message + " width: ");
|
||||
Util.assessNumericValue(assert, rectA.height, rectB.height, precision,
|
||||
message + " height: ");
|
||||
Util.assessNumericValue(assert, rectA.degrees, rectB.degrees, precision,
|
||||
message + " degrees: ");
|
||||
assertRectangleEquals: function (assert, actualRect, expectedRect, precision, message) {
|
||||
Util.assessNumericValue(assert, actualRect.x, expectedRect.x, precision, message + " x: ");
|
||||
Util.assessNumericValue(assert, actualRect.y, expectedRect.y, precision, message + " y: ");
|
||||
Util.assessNumericValue(assert, actualRect.width, expectedRect.width, precision, message + " width: ");
|
||||
Util.assessNumericValue(assert, actualRect.height, expectedRect.height, precision, message + " height: ");
|
||||
Util.assessNumericValue(assert, actualRect.degrees, expectedRect.degrees, precision, message + " degrees: ");
|
||||
},
|
||||
|
||||
// ----------
|
||||
|
@ -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 () {
|
||||
|
@ -332,10 +333,10 @@
|
|||
assert.equal( quickClick, expected.quickClick, expected.description + 'clickHandler event.quick matches expected (' + expected.quickClick + ')' );
|
||||
}
|
||||
if ('speed' in expected) {
|
||||
Util.assessNumericValue(expected.speed, speed, 1.0, expected.description + 'Drag speed ');
|
||||
Util.assessNumericValue(assert, speed, expected.speed, 1.0, expected.description + 'Drag speed');
|
||||
}
|
||||
if ('direction' in expected) {
|
||||
Util.assessNumericValue(expected.direction, direction, 0.2, expected.description + 'Drag direction ');
|
||||
Util.assessNumericValue(assert, direction, expected.direction, 0.2, expected.description + 'Drag direction');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -867,7 +868,7 @@
|
|||
simulateDblTap();
|
||||
|
||||
var zoom = viewer.viewport.getZoom();
|
||||
Util.assessNumericValue(assert, originalZoom, zoom, epsilon,
|
||||
Util.assessNumericValue(assert, zoom, originalZoom, epsilon,
|
||||
"Zoom on double tap should be prevented");
|
||||
|
||||
// Reset event handler to original
|
||||
|
@ -877,7 +878,7 @@
|
|||
originalZoom *= viewer.zoomPerClick;
|
||||
|
||||
zoom = viewer.viewport.getZoom();
|
||||
Util.assessNumericValue(assert, originalZoom, zoom, epsilon,
|
||||
Util.assessNumericValue(assert, zoom, originalZoom, epsilon,
|
||||
"Zoom on double tap should not be prevented");
|
||||
|
||||
|
||||
|
@ -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]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
@ -52,15 +52,15 @@
|
|||
|
||||
var assessNavigatorLocation = function (assert, expectedX, expectedY) {
|
||||
navigatorElement = navigatorElement || $(".navigator");
|
||||
Util.assessNumericValue(assert, expectedX, navigatorElement.offset().left, 10, ' Navigator x Position');
|
||||
Util.assessNumericValue(assert, expectedY, navigatorElement.offset().top, 10, ' Navigator y Position');
|
||||
Util.assessNumericValue(assert, navigatorElement.offset().left, expectedX, 10, ' Navigator x Position');
|
||||
Util.assessNumericValue(assert, navigatorElement.offset().top, expectedY, 10, ' Navigator y Position');
|
||||
};
|
||||
|
||||
var assessNavigatorSize = function (assert, expectedWidth, expectedHeight, msg) {
|
||||
msg = msg || "";
|
||||
navigatorElement = navigatorElement || $(".navigator");
|
||||
Util.assessNumericValue(assert, expectedWidth, navigatorElement.width(), 2, ' Navigator Width ' + msg);
|
||||
Util.assessNumericValue(assert, expectedHeight, navigatorElement.height(), 2, ' Navigator Height ' + msg);
|
||||
Util.assessNumericValue(assert, navigatorElement.width(), expectedWidth, 2, ' Navigator Width ' + msg);
|
||||
Util.assessNumericValue(assert, navigatorElement.height(), expectedHeight, 2, ' Navigator Height ' + msg);
|
||||
};
|
||||
|
||||
var assessNavigatorAspectRatio = function (assert, expectedAspectRatio, variance, msg) {
|
||||
|
@ -68,8 +68,8 @@
|
|||
navigatorElement = navigatorElement || $(".navigator");
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedAspectRatio,
|
||||
navigatorElement.width() / navigatorElement.height(),
|
||||
expectedAspectRatio,
|
||||
variance,
|
||||
' Navigator Aspect Ratio ' + msg
|
||||
);
|
||||
|
@ -80,13 +80,14 @@
|
|||
navigatorElement = navigatorElement || $(".navigator");
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedArea,
|
||||
navigatorElement.width() * navigatorElement.height(),
|
||||
expectedArea,
|
||||
Math.max(navigatorElement.width(), navigatorElement.height()),
|
||||
' Navigator Area ' + msg
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var navigatorRegionBoundsInPoints = function () {
|
||||
var regionBoundsInPoints,
|
||||
expectedDisplayRegionWidth,
|
||||
|
@ -142,22 +143,23 @@
|
|||
var expectedBounds = navigatorRegionBoundsInPoints();
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedBounds.width,
|
||||
displayRegion.width() + viewer.navigator.totalBorderWidths.x,
|
||||
expectedBounds.width,
|
||||
2,
|
||||
status + ' Width synchronization'
|
||||
);
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedBounds.height,
|
||||
displayRegion.height() + viewer.navigator.totalBorderWidths.y,
|
||||
expectedBounds.height,
|
||||
2,
|
||||
status + ' Height synchronization'
|
||||
);
|
||||
Util.assessNumericValue(assert, expectedBounds.x, displayRegion.position().left, 2, status + ' Left synchronization');
|
||||
Util.assessNumericValue(assert, expectedBounds.y, displayRegion.position().top, 2, status + ' Top synchronization');
|
||||
Util.assessNumericValue(assert, displayRegion.position().left, expectedBounds.x, 2, status + ' Left synchronization');
|
||||
Util.assessNumericValue(assert, displayRegion.position().top, expectedBounds.y, 2, status + ' Top synchronization');
|
||||
};
|
||||
|
||||
|
||||
var waitForViewer = function () {
|
||||
return function (assert, handler, count, lastDisplayRegionLeft, lastDisplayWidth) {
|
||||
var viewerAndNavigatorDisplayReady = false,
|
||||
|
@ -271,14 +273,15 @@
|
|||
}
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
1 / viewer.source.aspectRatio / 2,
|
||||
viewer.viewport.getCenter().y,
|
||||
1 / viewer.source.aspectRatio / 2,
|
||||
yPositionVariance,
|
||||
' Viewer at center, y coord'
|
||||
);
|
||||
Util.assessNumericValue(assert, 0.5, viewer.viewport.getCenter().x, 0.4, ' Viewer at center, x coord');
|
||||
Util.assessNumericValue(assert, viewer.viewport.getCenter().x, 0.5, 0.4, ' Viewer at center, x coord');
|
||||
};
|
||||
|
||||
|
||||
var assessViewerInCorner = function (theContentCorner, assert) {
|
||||
return function () {
|
||||
var expectedXCoordinate, expectedYCoordinate;
|
||||
|
@ -301,8 +304,8 @@
|
|||
if (viewer.viewport.getBounds().width < 1) {
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedXCoordinate,
|
||||
viewer.viewport.getBounds().x,
|
||||
expectedXCoordinate,
|
||||
0.04,
|
||||
' Viewer at ' + theContentCorner + ', x coord'
|
||||
);
|
||||
|
@ -310,8 +313,8 @@
|
|||
if (viewer.viewport.getBounds().height < 1 / viewer.source.aspectRatio) {
|
||||
Util.assessNumericValue(
|
||||
assert,
|
||||
expectedYCoordinate,
|
||||
viewer.viewport.getBounds().y,
|
||||
expectedYCoordinate,
|
||||
0.04,
|
||||
' Viewer at ' + theContentCorner + ', y coord'
|
||||
);
|
||||
|
|
|
@ -22,37 +22,26 @@
|
|||
assert.strictEqual(rect.degrees, 0, 'rect.degrees should be 0');
|
||||
|
||||
rect = new OpenSeadragon.Rect(0, 0, 1, 2, -405);
|
||||
Util.assessNumericValue(assert, Math.sqrt(2) / 2, rect.x, precision,
|
||||
'rect.x should be sqrt(2)/2');
|
||||
Util.assessNumericValue(assert, -Math.sqrt(2) / 2, rect.y, precision,
|
||||
'rect.y should be -sqrt(2)/2');
|
||||
Util.assessNumericValue(assert, 2, rect.width, precision,
|
||||
'rect.width should be 2');
|
||||
Util.assessNumericValue(assert, 1, rect.height, precision,
|
||||
'rect.height should be 1');
|
||||
assert.strictEqual(45, rect.degrees, 'rect.degrees should be 45');
|
||||
Util.assessNumericValue(assert, rect.x, Math.sqrt(2) / 2, precision, 'rect.x should be sqrt(2)/2');
|
||||
Util.assessNumericValue(assert, rect.y, -Math.sqrt(2) / 2, precision, 'rect.y should be -sqrt(2)/2');
|
||||
Util.assessNumericValue(assert, rect.width, 2, precision, 'rect.width should be 2');
|
||||
Util.assessNumericValue(assert, rect.height, 1, precision, 'rect.height should be 1');
|
||||
assert.strictEqual(rect.degrees, 45, 'rect.degrees should be 45');
|
||||
|
||||
rect = new OpenSeadragon.Rect(0, 0, 1, 2, 135);
|
||||
Util.assessNumericValue(assert, -Math.sqrt(2), rect.x, precision,
|
||||
'rect.x should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, -Math.sqrt(2), rect.y, precision,
|
||||
'rect.y should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, 2, rect.width, precision,
|
||||
'rect.width should be 2');
|
||||
Util.assessNumericValue(assert, 1, rect.height, precision,
|
||||
'rect.height should be 1');
|
||||
assert.strictEqual(45, rect.degrees, 'rect.degrees should be 45');
|
||||
Util.assessNumericValue(assert, rect.x, -Math.sqrt(2), precision, 'rect.x should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, rect.y, -Math.sqrt(2), precision, 'rect.y should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, rect.width, 2, precision, 'rect.width should be 2');
|
||||
Util.assessNumericValue(assert, rect.height, 1, precision, 'rect.height should be 1');
|
||||
assert.strictEqual(rect.degrees, 45, 'rect.degrees should be 45');
|
||||
|
||||
rect = new OpenSeadragon.Rect(0, 0, 1, 1, 585);
|
||||
Util.assessNumericValue(assert, 0, rect.x, precision,
|
||||
'rect.x should be 0');
|
||||
Util.assessNumericValue(assert, -Math.sqrt(2), rect.y, precision,
|
||||
'rect.y should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, 1, rect.width, precision,
|
||||
'rect.width should be 1');
|
||||
Util.assessNumericValue(assert, 1, rect.height, precision,
|
||||
'rect.height should be 1');
|
||||
assert.strictEqual(45, rect.degrees, 'rect.degrees should be 45');
|
||||
Util.assessNumericValue(assert, rect.x, 0, precision, 'rect.x should be 0');
|
||||
Util.assessNumericValue(assert, rect.y, -Math.sqrt(2), precision, 'rect.y should be -sqrt(2)');
|
||||
Util.assessNumericValue(assert, rect.width, 1, precision, 'rect.width should be 1');
|
||||
Util.assessNumericValue(assert, rect.height, 1, precision, 'rect.height should be 1');
|
||||
assert.strictEqual(rect.degrees, 45, 'rect.degrees should be 45');
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('getTopLeft', function(assert) {
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
now = 500;
|
||||
spring.update();
|
||||
Util.assessNumericValue(assert, 5.5, spring.current.value, 0.00001, 'current value after first update');
|
||||
Util.assessNumericValue(assert, spring.current.value, 5.5, 0.00001, 'current value after first update');
|
||||
assert.equal(spring.target.value, 6, 'target value after first update');
|
||||
|
||||
now = 1000;
|
||||
|
@ -65,7 +65,7 @@
|
|||
|
||||
now = 500;
|
||||
spring.update();
|
||||
Util.assessNumericValue(assert, 1.41421, spring.current.value, 0.00001, 'current value after first update');
|
||||
Util.assessNumericValue(assert, spring.current.value, 1.41421, 0.00001, 'current value after first update');
|
||||
assert.equal(spring.target.value, 2, 'target value after first update');
|
||||
|
||||
now = 1000;
|
||||
|
|
|
@ -1,61 +1,291 @@
|
|||
/* 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++;
|
||||
});
|
||||
|
||||
// ----------
|
||||
QUnit.module('TileCache', {
|
||||
beforeEach: function () {
|
||||
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
||||
|
||||
testLog.reset();
|
||||
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
||||
|
||||
// Reset counters
|
||||
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
||||
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
||||
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
||||
|
||||
OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
this.testEvents = new OpenSeadragon.EventSource();
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internalCacheFree(data) {
|
||||
this.testEvents.raiseEvent('free-data');
|
||||
}
|
||||
|
||||
canRotate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyInternalCache();
|
||||
}
|
||||
|
||||
setImageSmoothingEnabled(imageSmoothingEnabled){
|
||||
//noop
|
||||
}
|
||||
|
||||
drawDebuggingRect(rect) {
|
||||
//noop
|
||||
}
|
||||
|
||||
clear(){
|
||||
//noop
|
||||
}
|
||||
}
|
||||
|
||||
OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer {
|
||||
|
||||
getType() {
|
||||
return "test-cache-drawer-sync";
|
||||
}
|
||||
|
||||
getSupportedDataFormats() {
|
||||
return [T_C, T_E];
|
||||
}
|
||||
|
||||
// Make test use private cache
|
||||
get defaultOptions() {
|
||||
return {
|
||||
usePrivateCache: true,
|
||||
preloadCache: false,
|
||||
};
|
||||
}
|
||||
|
||||
internalCacheCreate(cache, tile) {
|
||||
this.testEvents.raiseEvent('create-data');
|
||||
return cache.data;
|
||||
}
|
||||
}
|
||||
|
||||
OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer {
|
||||
|
||||
getType() {
|
||||
return "test-cache-drawer-async";
|
||||
}
|
||||
|
||||
getSupportedDataFormats() {
|
||||
return [T_A];
|
||||
}
|
||||
|
||||
// Make test use private cache
|
||||
get defaultOptions() {
|
||||
return {
|
||||
usePrivateCache: true,
|
||||
preloadCache: true,
|
||||
};
|
||||
}
|
||||
|
||||
internalCacheCreate(cache, tile) {
|
||||
this.testEvents.raiseEvent('create-data');
|
||||
return cache.getDataAs(T_C, true);
|
||||
}
|
||||
|
||||
internalCacheFree(data) {
|
||||
super.internalCacheFree(data);
|
||||
// Be nice and truly destroy the data copy
|
||||
OpenSeadragon.convertor.destroy(data, T_C);
|
||||
}
|
||||
}
|
||||
|
||||
OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
afterEach: function () {
|
||||
if (viewer && viewer.close) {
|
||||
viewer.close();
|
||||
}
|
||||
|
||||
// Some tests test all drawers - remove test drawers to avoid collision with other tests
|
||||
OpenSeadragon.EmptyTestT_ATileSource = null;
|
||||
OpenSeadragon.AsnycInternalCacheDrawer = null;
|
||||
OpenSeadragon.SyncInternalCacheDrawer = null;
|
||||
OpenSeadragon.TestCacheDrawer = null;
|
||||
|
||||
viewer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ----------
|
||||
// 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 +301,569 @@
|
|||
|
||||
// ----------
|
||||
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 (sync drawer)', function(test) {
|
||||
const done = test.async();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100, // Faster animation = faster tests
|
||||
drawer: 'test-cache-drawer-sync',
|
||||
});
|
||||
|
||||
const tileCache = viewer.tileCache;
|
||||
const drawer = viewer.drawer;
|
||||
|
||||
let testTileCalled = false;
|
||||
let countFreeCalled = 0;
|
||||
let countCreateCalled = 0;
|
||||
drawer.testEvents.addHandler('test-tile', e => {
|
||||
testTileCalled = true;
|
||||
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
|
||||
});
|
||||
drawer.testEvents.addHandler('create-data', e => {
|
||||
countCreateCalled++;
|
||||
});
|
||||
drawer.testEvents.addHandler('free-data', e => {
|
||||
countFreeCalled++;
|
||||
});
|
||||
|
||||
viewer.addHandler('open', async () => {
|
||||
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_C, "Cache data was affected, the drawer supports only T_C since there is no way to get to T_E.");
|
||||
|
||||
const internalCache = cache.getDataForRendering(drawer, tile);
|
||||
test.equal(internalCache.type, viewer.drawer.getId(), "Sync conversion routine means T_C is also internal since dataCreate only creates data. However, internal cache keeps type of the drawer ID.");
|
||||
test.ok(internalCache.loaded, "Internal cache ready.");
|
||||
}
|
||||
|
||||
test.ok(countCreateCalled > 0, "Internal cache creation called.");
|
||||
viewer.drawer.destroyInternalCache();
|
||||
test.equal(countCreateCalled, countFreeCalled, "Free called as many times as create.");
|
||||
|
||||
done();
|
||||
});
|
||||
viewer.open([
|
||||
{isTestSource: true},
|
||||
{isTestSource: true},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) {
|
||||
const done = test.async();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100, // Faster animation = faster tests
|
||||
drawer: 'test-cache-drawer-async',
|
||||
});
|
||||
const tileCache = viewer.tileCache;
|
||||
const 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();
|
||||
_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);
|
||||
|
||||
//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_A, "Main Cache Converted T_C -> T_A (drawer supports type A) (suite 1)");
|
||||
test.equal(cache.data, 3, "Conversion step increases plugin-stored value 2 to 3");
|
||||
|
||||
const internalCache = cache.getDataForRendering(drawer, tile);
|
||||
test.equal(internalCache.type, viewer.drawer.getId(), "Internal cache has type of the drawer ID.");
|
||||
test.ok(internalCache.loaded, "Internal cache ready.");
|
||||
}
|
||||
// Internal cache will have value 5: main cache is 3, type is T_A,
|
||||
testDrawingRoutine(5); // internal cache transforms to T_C: two steps, TA->TB->TC 3+2
|
||||
|
||||
// Test that basic scenario with reset data false starts from the main cache data of previous round
|
||||
const modificationConstant = 50;
|
||||
viewer.removeHandler('tile-invalidated', testHandler);
|
||||
testHandler = async e => {
|
||||
const data = await e.getData(T_B);
|
||||
test.equal(data, 4, "A -> B conversion happened, we started from value 3 in the main cache.");
|
||||
await e.setData(data + modificationConstant, T_B);
|
||||
test.notOk(e.outdated(), "Event is still valid.");
|
||||
};
|
||||
|
||||
viewer.addHandler('tile-invalidated', testHandler);
|
||||
await viewer.world.requestInvalidate(false);
|
||||
|
||||
// We set data as TB - there is required T_A: T_B -> T_C -> T_A conversion round on the main cache
|
||||
let newValue = modificationConstant + 4 + 2;
|
||||
// and there is still requirement of T_C on internal data, +2 steps
|
||||
testDrawingRoutine(newValue + 2);
|
||||
|
||||
for (let tile of tileCache._tilesLoaded) {
|
||||
const cache = tile.getCache();
|
||||
test.equal(cache.type, T_A, "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, 44, "Copy: 41 +2 (previous request invalidate ends at T_A) + 1 (we request type B).");
|
||||
await e.setData(data, T_E); // there is no way to convert T_E -> T_A, this would throw an error
|
||||
e.resetData(); // reset data will revert to original cache
|
||||
};
|
||||
viewer.addHandler('tile-invalidated', testHandler);
|
||||
|
||||
// The data will be 45 since no change has been made:
|
||||
// last main cache set was 41 T_B, supported T_A = +2
|
||||
// and internal requirement T_C = +2
|
||||
const checkNotCalled = e => {
|
||||
test.ok(false, "Create data must not be called when there is no change!");
|
||||
};
|
||||
drawer.testEvents.addHandler('create-data', checkNotCalled);
|
||||
|
||||
await viewer.world.requestInvalidate(false);
|
||||
testDrawingRoutine(45);
|
||||
|
||||
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.");
|
||||
viewer.destroy();
|
||||
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();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100, // Faster animation = faster tests
|
||||
drawer: 'test-cache-drawer-sync',
|
||||
});
|
||||
|
||||
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
|
||||
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();
|
||||
|
||||
viewer = OpenSeadragon({
|
||||
id: 'example',
|
||||
prefixUrl: '/build/openseadragon/images/',
|
||||
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
||||
springStiffness: 100, // Faster animation = faster tests
|
||||
drawer: 'test-cache-drawer-sync',
|
||||
});
|
||||
|
||||
let jobCounter = 0, coverage = undefined;
|
||||
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…
Add table
Reference in a new issue