Compare commits

...

204 commits

Author SHA1 Message Date
Ian Gilman
99fcb408fe Changelog for #2702 2025-04-02 09:36:33 -07:00
Ian Gilman
479ef0ec82
Merge pull request #2702 from achu1998/expose-nav-button
Exposed Zoom In and Out methods for end users
2025-04-02 09:33:44 -07:00
achu1998
9e01a1e18b Made changes according to review. 2025-04-01 11:04:04 +10:30
achu1998
2e3f0923f5 Explicitly bind 'this' to the Viewer instance 2025-03-29 14:17:54 +10:30
achu1998
3550dfafd3 Refactored code to minimise the changes required 2025-03-29 13:47:02 +10:30
achu1998
12de02938a Exposed Zoom In and Out methods for end users 2025-03-29 13:33:25 +10:30
Ian Gilman
8f093f54df Changelog and minor fixes for #2698 2025-03-26 09:44:50 -07:00
Ian Gilman
6bdd186219
Merge pull request #2698 from lokaesshwar/duplicate_ids_overlay_html_2682
Duplicate ids overlay html 2682
2025-03-26 09:36:17 -07:00
lokaesshwar
c54c292873 Fix #2682: Ensure unique ID for overlay wrapper to prevent duplicate IDs 2025-03-26 19:38:02 +05:30
lokaesshwar
4c7d9a2ffa Resolved Duplicate IDs in overlay HTML 2025-03-14 00:35:22 +05:30
Ian Gilman
595066abd1 Changelog for #2690 2025-03-12 09:32:08 -07:00
Ian Gilman
b4e14d4804
Merge pull request #2690 from MichaelWGibson/load-area-fixes
Make load code more robust to out of image panning
2025-03-12 09:31:08 -07:00
Michael Gibson
be87b47469 Make load code more robust to out of image panning 2025-03-10 11:19:44 -06:00
Ian Gilman
62fa378f51 Changelog for #2686 2025-03-10 10:04:26 -07:00
Ian Gilman
c386eca94d
Merge pull request #2686 from MichaelWGibson/animation-dest-tile-loading
During animations, load tiles in the destination region
2025-03-10 09:59:12 -07:00
Michael Gibson
8c6d1f4f88 Update documentation to match new flag behavior 2025-03-07 21:21:40 -07:00
Michael Gibson
3cb5152905 Change flag name 2025-03-07 21:18:42 -07:00
Michael Gibson
5cc76737b4 Fix flag type 2025-02-28 15:37:13 -07:00
Michael Gibson
f312bae614 Document flag 2025-02-28 15:35:25 -07:00
Michael Gibson
97d388be7d Add loadTilesOnAnimationPath flag 2025-02-28 11:30:30 -07:00
Michael Gibson
fd27327737 Factor out common traversal code 2025-02-28 11:10:29 -07:00
Michael Gibson
5c60f669ec Clean up 2025-02-28 09:33:49 -07:00
Michael Gibson
6ceac75b7a During animations, load tiles in the destination region 2025-02-28 09:17:41 -07:00
Ian Gilman
2387c3afd5 Stopped sponsor images from wrapping 2025-01-31 09:51:18 -08:00
Ian Gilman
5184ed6c9f Changelog for #2676 2025-01-31 09:40:00 -08:00
Ian Gilman
b22b689dae
Merge pull request #2676 from bennlich/patch-1
Update viewer.addOverlay doc
2025-01-31 09:38:38 -08:00
Benny Lichtner
138215e22f
delete trailing whitepsace 2025-01-30 20:12:05 +01:00
Ian Gilman
22e766bd1a Trimming sponsor logos 2025-01-30 09:37:34 -08:00
Benny Lichtner
337af91373
Update viewer.addOverlay doc
Mention the preserveOverlays option
2025-01-30 14:45:30 +01:00
Ian Gilman
ced1bde338 Changelog for #2643 2025-01-23 09:45:25 -08:00
Ian Gilman
dbc72d4d98
Merge pull request #2643 from RationAI/fix/fallback-canvas-drawer
Flexible support for drawers to modify cache
2025-01-23 09:37:52 -08:00
Aiosa
994f0e25a4 Fix renaming also in tests. 2025-01-14 12:26:28 +01:00
Aiosa
71c3c8437a Rename new drawer base api to reflect that it deals with internal cache. 2025-01-14 12:20:39 +01:00
Aiosa
6e81bedc39 Improve docs, change order of calls in prepareForRendering for better code stability. 2025-01-14 12:14:28 +01:00
Ian Gilman
a9562fbb64
Improved sponsor logo styling 2025-01-13 09:57:54 -08:00
Ian Gilman
b4c7e75109 Merge branch 'master' of github.com:openseadragon/openseadragon 2025-01-13 09:47:20 -08:00
Ian Gilman
301191a0b8 Actually adding the logos 2025-01-13 09:46:57 -08:00
Ian Gilman
edb422007c
Merge pull request #2661 from openseadragon/ian
Additional sponsor logos
2025-01-13 09:45:09 -08:00
Ian Gilman
f8aad5f237 Additional sponsor logos 2025-01-13 09:44:18 -08:00
Aiosa
1286fa4549 Test drawers collide with drawer render testing - remove after cache tests. 2025-01-10 22:13:16 +01:00
Aiosa
03b7c5b9a6 Update tests, fix async cache bug - first convert to supported format, then process. Record drawer ID in the internal cache type. 2025-01-10 22:05:16 +01:00
Ian Gilman
7dfde9da3a Changelog for #2644 2025-01-10 09:53:09 -08:00
Ian Gilman
ffe12888cf
Merge pull request #2644 from RationAI/feat/import
Import Openseadragon Dynamically
2025-01-10 09:51:54 -08:00
Ian Gilman
8b30e7a307 Changelog for #2657 2025-01-10 09:34:25 -08:00
Ian Gilman
306adb9b6a
Merge pull request #2657 from cff29546/master
use unique hash for MouseTracker
2025-01-10 09:32:12 -08:00
Aiosa
226a44c498 Fix docs. 2025-01-10 14:57:41 +01:00
Aiosa
b1feb367b8 Fix typo. 2025-01-10 14:49:59 +01:00
Aiosa
cb06a5c0fb Debug fallback to canvas drawer, small bugfixes, add support for drawer cleanup in cache. 2025-01-10 14:46:52 +01:00
Ian Gilman
9b8b4c2728 Changelog for #2652 2025-01-09 10:06:05 -08:00
Ian Gilman
d6740dab25 Merge branch 'master' of github.com:openseadragon/openseadragon 2025-01-09 10:04:58 -08:00
Ian Gilman
003dd3a9da
Merge pull request #2652 from pearcetm/fix-2651
Mark tiled image as needing update when opacity changes
2025-01-09 10:02:35 -08:00
Ian Gilman
a1fa790e19 Changelog for #2649 2025-01-09 09:45:33 -08:00
Ian Gilman
3aaa8fa721
Merge pull request #2649 from Seafret/master
Fixed issue with MouseTracker where hasGestureHandlers and hasScrollHandler values were not getting updated upon dynamically adding/removing handlers.
2025-01-09 09:42:51 -08:00
Min Xiang
39671f4d78 use unique hash for MouseTracker 2025-01-09 07:53:05 +08:00
Aiosa
426700b1c6 Temporarily disable tests - discussing design. 2025-01-07 19:57:37 +01:00
Aiosa
6315662078 DrawerBase API for internal cache. 2025-01-07 19:56:21 +01:00
Ian Gilman
e193557e56 Changelog for #2636 2025-01-06 09:45:36 -08:00
Ian Gilman
b8b6a9656a
Merge pull request #2636 from pcram-techcyte/v5.0.1-hasTransparency-fix
Typo in hasTransparency check in webgldrawer
2025-01-06 09:42:55 -08:00
Tom
f5f9eab200 mark tiled image as needing an update when opacity changes 2025-01-06 09:40:23 -05:00
Tom
3f22b6aa3a mark tiled image as needing an update when opacity changes 2025-01-06 09:39:28 -05:00
Jessica R.
641e085259
Updated MouseTracker's hasGestureHandlers and hasScrollHandler to be getters 2025-01-02 23:07:13 -07:00
Jessica R.
9527f15f52
Changed hasScrollHandler to be a function to better work with dynamically added handlers
Previously, we were setting hasScrollHandler once in the constructor and never re-assigning it after the initial creation. Changing hasScrollHandler to a function better accommodates dynamically added/removed handlers. See previous commit for change to hasGestureHandlers for further details and reasoning.
2025-01-01 22:45:12 -07:00
Jessica R.
865c3884b2
Changed hasGestureHandlers to be a function to better work with dynamically added handlers
Previously, we were setting hasGestureHandlers once in the constructor and never re-assigning it after the initial creation. This resulted in MouseTrackers created without gesture handlers never updating its hasGestureHandlers values, even when gesture handlers were dynamically added after creation. By changing this to a function, our MouseTrackers' hasGestureHandlers value should always be in the proper state, even if handlers are dynamically created or removed.
2025-01-01 22:38:48 -07:00
Ian Gilman
56d0d982da Changelog for #2640 2024-12-20 09:32:24 -08:00
Ian Gilman
58f4e6a36e
Merge pull request #2640 from harshkg23/master
Refactor assessNumericValue for improved clarity and debugging
2024-12-20 09:28:11 -08:00
Paul Cram
ef0f14a03d Revert "Add useTwoPassRendering as an option to the webgl drawer"
This reverts commit 55f53e8369.
2024-12-19 15:32:00 -05:00
Harsh Kumar Gupta
d31562e34f Fix the function arguments for the required file 2024-12-20 00:31:40 +05:30
Aiosa
ad943e5472 feat: ability to attach OSD to window reference if available 2024-12-19 16:22:50 +01:00
Aiosa
4c2b0715af fix: typo 2024-12-19 16:20:50 +01:00
Aiosa
a5c569ab5d fix: reference to the cpuData on webgldrawer 2024-12-19 16:11:53 +01:00
Harsh Kumar Gupta
8fdd639bf0 Rename parameters in helper functions for clarity and consistency 2024-12-19 16:30:04 +05:30
Harsh Kumar Gupta
a0fcfc20ad Correct function names in test helpers for consistency 2024-12-19 16:12:27 +05:30
Harsh Kumar Gupta
15627ee18e Bump version to 5.0.1 and update argument names for clarity in helper functions 2024-12-19 15:52:14 +05:30
Harsh Kumar Gupta
33c3d4b380 Corrected arguments name and fixed the identation issue 2024-12-19 15:24:34 +05:30
Harsh Kumar Gupta
74ffb66f40 Refactor test helper functions for improved readability and consistency 2024-12-18 00:53:44 +05:30
Paul Cram
55f53e8369 Add useTwoPassRendering as an option to the webgl drawer 2024-12-17 10:36:31 -05:00
pcram-techcyte
acb7563257
Merge branch 'openseadragon:master' into v5.0.1-hasTransparency-fix 2024-12-17 09:01:36 -05:00
Ian Gilman
c06e719198 Changelog for #2407 2024-12-16 09:30:47 -08:00
Ian Gilman
64bb7e25c7
Merge pull request #2407 from RationAI/cache-overhaul-reviewed
Cache Overhaul for OpenSeadragon (reviewed).
2024-12-16 09:26:13 -08:00
Aiosa
c392f2d205 Change location of logos, add link 2024-12-12 19:28:58 +01:00
pcram-techcyte
45bd3f48f9
Typo in hasTransparency check 2024-12-10 09:12:39 -05:00
Aiosa
bee3c243fb Delete demos with old plugins. 2024-12-10 14:38:34 +01:00
Aiosa
563c7674c7 Add sponsors section to README. 2024-12-10 14:37:30 +01:00
Aiosa
fec033f9d2 Clear up few todos, introduce tile processing promise to await single tiles. 2024-12-10 14:16:19 +01:00
Aiosa
e12a349f52 Merge branch 'temporary-merge' into cache-overhaul-reviewed 2024-12-10 13:51:56 +01:00
Aiosa
bee5cb2471 Merge conflicts from master. 2024-12-10 13:50:41 +01:00
Ian Gilman
c8c7b481b8 Started on version 6 2024-12-09 09:37:19 -08:00
Ian Gilman
480de92d0a Version 5.0.1 2024-12-09 09:31:27 -08:00
Ian Gilman
e9dd9e9dfc Changelog for #2627 2024-12-06 09:32:44 -08:00
Ian Gilman
07502e6d58
Merge pull request #2627 from pearcetm/pixel-density-change
Fix #2623
2024-12-06 09:30:41 -08:00
Tom
1843bbd89c update docs 2024-12-04 16:42:25 -05:00
Tom
a5987db8d5 fix #2623 2024-12-04 16:41:24 -05:00
Aiosa
039ffbd37a Fix typo 2024-12-04 09:16:04 +01:00
Aiosa
1a7a5ee1d6 Respect tainted() when uploading a texture. 2024-12-04 09:08:00 +01:00
Ian Gilman
9b96707dec Changelog for #2620 2024-12-02 09:51:24 -08:00
Ian Gilman
3ce307f309
Merge pull request #2620 from sbarex/master
Update webgldrawer.js
2024-12-02 09:48:24 -08:00
Aiosa
d6bb8d3bd1 Fix typo. Remove tileSource prototype extension - adds implementation for methods that throw exceptions. 2024-11-30 10:03:39 +01:00
Aiosa
271f437568 Force prototype extension on inlined tile source. Ensure zombies are processed with invalidation before restored in the system. WebGL drawer can use canvas drawer. Add changelog updates. 2024-11-30 09:52:27 +01:00
sbarex
5438fd65e1
Update webgldrawer.js
Fixed a bug that caused a completely white image to be rendered when two-pass rendering mode was requested (required when multiple images, using transparency, or in debug mode).
2024-11-26 23:38:13 +01:00
Aiosa
85e8b381b8 Add cache clear on viewer.clear(). Add tile and viewer state checks to invalidation. Fix IIF test (timer is now built-in). 2024-11-26 15:24:51 +01:00
Aiosa
ef7628f098 Fix bug: webgl 'manual instanced rendering' return -> continue, do not count as drawn index. 2024-11-26 14:08:32 +01:00
Aiosa
17f13885c7 Add back overlapFraction logic for webgl drawer. 2024-11-26 00:48:43 +01:00
Aiosa
6b4c0f873a Add maxTilesPerFrame degradation, fix tests. 2024-11-22 13:03:16 +01:00
Aiosa
8b16628950 Write documentation fo cache/data overhaul. Rename url type to be more explicit. 2024-11-22 12:43:14 +01:00
Ian Gilman
04211fbb27 Changelog for #2619 2024-11-21 09:58:12 -08:00
Ian Gilman
3885fa7bfc
Merge pull request #2619 from jbakarich/fix/2612/incorrect-display-region-rotation
Remove displayRegion transform
2024-11-21 09:53:08 -08:00
Aiosa
f03f2a5d31 Add fixes for working smoothing in the webgl renderer. 2024-11-21 16:51:38 +01:00
Aiosa
ce4b16616d Merge branch 'openseadragon-master' into cache-overhaul-reviewed 2024-11-21 16:13:36 +01:00
Aiosa
af9bf9e07f Resolve conflicts, implement setImageSmoothingEnabled on webgl drawer by invalidation routine. 2024-11-21 16:12:52 +01:00
Aiosa
1851405fcf Remove warning from cache attaching if the tile has already reference. 2024-11-21 15:48:11 +01:00
Aiosa
0bc7deccd7 Fixed tests & bugs for the new design of invalidation inside events. 2024-11-21 15:35:27 +01:00
Jacob Bakarich
2dc0e29647 Remove displayRegion transform
ref #2612

- Remove display region transform in `setFlip()` as it is not needed for flipping and was overriding existing rotation transforms.
2024-11-20 14:07:26 -07:00
Ian Gilman
0e3abe16f4 Changelog for #2615 2024-11-15 09:32:07 -08:00
Ian Gilman
cd9e4e8c4e
Merge pull request #2615 from pearcetm/webgl-glnearest
Add support for imageSmoothingEnabled to WebGLDrawer
2024-11-15 09:27:18 -08:00
Tom
02805226d4 only redraw after setImageSmoothingEnabled() when changed 2024-11-14 13:13:13 -05:00
Tom
4163a46a24 Update docs 2024-11-13 16:49:54 -05:00
Tom
16dbfc62be Add support for imageSmoothingEnabled to WebGLDrawer 2024-11-13 16:43:46 -05:00
Aiosa
e24f7d1358 Update demo plugins to the new api design 2024-11-13 15:04:09 +01:00
Aiosa
541fe2e4df Redesign working cache: it is now owned by the event, not a tile. Tests are not yet updated. 2024-11-13 14:35:50 +01:00
Aiosa
e059b8982e Add try-catch for a plugin 2024-11-07 12:22:18 +01:00
Aiosa
3b1b2d6d23 Fix: reference the correct drawer in invalidation routine. 2024-11-07 12:01:02 +01:00
Aiosa
535507568f Fix docs syntax. 2024-11-05 12:06:59 +01:00
Aiosa
cc7474ec9b
Merge branch 'openseadragon:master' into cache-overhaul-reviewed 2024-11-05 18:59:34 +09:00
Aiosa
9bfdd55b2e Make tile-invalidated event before tile-loaded. Try to fix behavior of maxTilesperFrame 2024-11-05 10:58:41 +01:00
Aiosa
5fdeb382ea Increase the test timeout: it seems that 5 seconds is not enough, maybe reason for tests failing. 2024-11-01 22:20:15 +01:00
Aiosa
cf65f1a4f4 Add ability to run only a specific module from CLI. 2024-11-01 22:06:18 +01:00
Aiosa
f127014f0f Design of separated events: drop update data support for tile-loaded, use only invalidated event. 2024-11-01 21:43:12 +01:00
Ian Gilman
e1e0ec6b24 Changelog for #2606 2024-10-28 09:17:59 -07:00
Ian Gilman
faf6d19db5
Merge pull request #2606 from pearcetm/viewerMargins
Fix problem with asymmetric viewport margins in webgl drawer
2024-10-28 09:14:09 -07:00
Tom
fab607584c fix problem with asymmetric viewport margins 2024-10-25 11:15:52 -06:00
Aiosa
cd60aff5dc Fix demo further: clear tile processing logics, perform locking on tile level, delete tile if drawn process encountered _destroyed state. 2024-10-23 15:54:31 +02:00
Aiosa
207bc88aab Fix demo further: _queuedInvalidateTiles was not being used, simplify how tile cache updates are being called (but without priorities). 2024-10-23 14:37:43 +02:00
Aiosa
6cbe359398 Fix demo filtering plugin: certain guards made rendering not being properly updated. 2024-10-23 10:07:16 +02:00
Ian Gilman
82314dd4bf Changelog for #2600 2024-10-22 09:43:43 -07:00
Ian Gilman
12e4607a30
Merge pull request #2600 from pearcetm/viewerMargins
Make webgl drawer account for viewport margins
2024-10-22 09:41:40 -07:00
Aiosa
20177116e7 Integration tests: bugfixing of manipulation of tiles that share data: when tiles are loaded, when tiles are processed, also await async data preparation befre finishing the invalidation event. 2024-10-22 17:25:02 +02:00
Aiosa
e403e29312 Fix tests: bug in type equality checking 2024-10-21 12:59:21 +02:00
Aiosa
82e1160508 Clean up outdated TODO comments. 2024-10-21 11:00:23 +02:00
Aiosa
d5cdf59993 Fix styling on the plugin demo. 2024-10-21 10:00:53 +02:00
Aiosa
3c6c7e0ab7 Add plugin interaction demo. 2024-10-21 09:55:23 +02:00
Aiosa
68f0ed8901 Do not spit out warns on invalid tile unload (preemtive working cache deletion), do not ignore working cache even if __restore=true. 2024-10-21 09:00:24 +02:00
Aiosa
b3cdeabf02 Flexible timeout for tests. 2024-10-18 14:56:05 +02:00
Aiosa
0cd17abafd Clean up old comments in a test. 2024-10-18 14:40:54 +02:00
Aiosa
1e47bd6add Add cache tests, add more robust CacheRecord creation/deletion logics. Zombies now do not replace data, prevents also potential memory leak. 2024-10-18 14:38:04 +02:00
Tom
6c20101c8b make webgl drawer account for viewport margins 2024-10-17 15:06:04 -04:00
Aiosa
bf25e2f069 Minor documentation and test cleanup. 2024-10-17 13:24:11 +02:00
Aiosa
06ac68d00e Html drawer add canvas to supported formats 2024-10-17 12:17:49 +02:00
Aiosa
1b6fea72d8 Add assets for fallback compatibility filtering plugin demo. 2024-10-17 12:17:24 +02:00
Aiosa
0b63a943b6 Tests & Bugfixes: new cache tests, working cache preemptively deleted when restore() called, zombie cache had bug (restored cache had no attached tile reference and restoration failed since we relied on any existing tile on the cache to inherit state), deprecated old HTMLDrawer props on tile, rewritten HTMLDrawer to work also with cache API. 2024-10-17 12:10:04 +02:00
Aiosa
f8e5cff117 Feature/Optimization: cache can be created by a callback (async or sync), to avoid premature data creation 2024-10-16 16:31:08 +02:00
Aiosa
b6693ee50d Fixed outdated demo pages. 2024-10-16 11:12:20 +02:00
Ian Gilman
8bdc0a4146 Changelog for #2590 2024-10-08 09:26:56 -07:00
Ian Gilman
3e0e39900e
Merge pull request #2590 from pearcetm/fix-setClip
Bug fix in TiledImage.setClip
2024-10-08 09:24:41 -07:00
Tom
263a664e58 add _needsUpdate during tiledImage.setClip() 2024-10-07 18:19:38 -04:00
Aiosa
3d21ec897b Set fully loaded for reset() call on tiled image to false. Add old plugins demo to see how they behave. Remove basic2 demo as it was added by accident. 2024-10-07 11:18:36 +02:00
Aiosa
2033814227 Update documentation and minor cleanup. 2024-10-05 11:50:21 +02:00
Ian Gilman
17c34a7985 Changelog for #2582 2024-09-24 09:50:10 -07:00
Ian Gilman
53e56932bd
Merge pull request #2582 from BeebBenjamin/2576-overlay-wrapping
Fixed: removeChild on Node
2024-09-24 09:43:56 -07:00
Richard Benjamin Allen
2b010a9a59 Fixed: JQuery cannot be used to get position with wrapper 2024-09-19 21:13:28 +01:00
Richard Benjamin Allen
4d4ccb99ba Fixed: Removed unused style attributes 2024-09-18 00:16:41 +01:00
Richard Benjamin Allen
c0eae951c6 Fixed: Removed unnecessary complexity 2024-09-17 23:38:48 +01:00
Richard Benjamin Allen
b28b77fdbd FIxed: Removed unnecessary references in selectors
We can test the overlay dimensions still
2024-09-17 23:30:06 +01:00
Richard Benjamin Allen
3b99665b32 Fixed: removeChild on Node
The overlay.js code has been changed to apply a div wrapper to the overlay element as opposed to its innerHTML.
2024-09-17 12:16:00 +01:00
Ian Gilman
50d7db595b
Merge pull request #2581 from openseadragon/dependabot/npm_and_yarn/serve-static-1.16.2
Bump serve-static from 1.14.2 to 1.16.2
2024-09-16 09:47:18 -07:00
dependabot[bot]
fe7e9a3315
Bump serve-static from 1.14.2 to 1.16.2
Bumps [serve-static](https://github.com/expressjs/serve-static) from 1.14.2 to 1.16.2.
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.14.2...v1.16.2)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-16 13:29:14 +00:00
Aiosa
e3af370832 Merge branch 'cache-overhaul-reviewed' of github.com:RationAI/openseadragon into cache-overhaul-reviewed 2024-08-24 10:00:09 +02:00
Aiosa
63180a1589 Simplify filtering plugin demo. 2024-08-24 09:59:18 +02:00
Aiosa
c04b6af937
Merge branch 'master' into cache-overhaul-reviewed 2024-08-24 09:53:23 +02:00
Aiosa
29b01cf1bd First visually correct design: tile invalidation event manages three caches that are shared among equal tiles (based on cache key). Works with both latest drawers and shared caches. 2024-08-24 09:49:16 +02:00
Ian Gilman
a10b9086ab Changelog for #2570 2024-08-16 09:39:04 -07:00
Ian Gilman
a9f81190dd
Merge pull request #2570 from pearcetm/resize-after-drawer-swap
Remove resize handler from viewer when webgl drawer is destroyed
2024-08-16 09:36:49 -07:00
Tom
f3b92e0562 remove resize handler when webgl drawer is destroyed 2024-08-15 17:51:06 -04:00
Aiosa
cba40f4db8 Merge branch 'cache-overhaul-reviewed' of github.com:RationAI/openseadragon into cache-overhaul-reviewed 2024-08-15 12:58:07 +02:00
Aiosa
1b6f79661b Commit before merging master v5.0 2024-08-15 12:58:01 +02:00
Aiosa
999ff30e74
Merge branch 'master' into cache-overhaul-reviewed 2024-08-15 11:53:50 +01:00
Ian Gilman
d97ba581b0 Started 5.0.1 2024-08-14 09:37:49 -07:00
Aiosa
0a035afc2d Merge master 2024-06-01 16:37:12 +02:00
Aiosa
cdb89ff5ad Add job queue full event. 2024-06-01 15:02:31 +02:00
Aiosa
e0f442209b Fix black viewport with testing filtering demo on webgl renderer. Introduce managed mock getters for tests. 2024-03-05 10:48:07 +01:00
Aiosa
e2c633a23b Small bugfixes, rename some properties. Add more redraw calls. 2024-03-04 19:23:47 +01:00
Aiosa
47419a090a Fix circular references in JSON test log serialization. 2024-03-04 10:49:05 +01:00
Aiosa
52ef8156c0 Fixed: internal cache not allways calling destructor, refresh handler was not called on internal cache. Polish code. Improve filtering demo. 2024-03-03 17:59:39 +01:00
Aiosa
a9b50a8fdb Test fixes (except gl null reference error - test fails sometimes). 2024-03-03 16:39:15 +01:00
Aiosa
135fa76fde Merge webgl PR - problems with tained canvas and texture upload. 2024-03-03 15:17:54 +01:00
Aiosa
360f0d6796 Fix docs, commit before upstream merge. 2024-03-03 14:50:01 +01:00
Aiosa
63f0adbc15 Fix issues with tile reference in cache: keep the 'most fresh' ref. 2024-02-11 17:18:03 +01:00
Aiosa
d91df0126b Add base drawer options and fix docs. Implement 'simple internal cache' for drawer data, optional to use. 2024-02-11 11:27:02 +01:00
Aiosa
cae6ec6bee Revert weird tiledImage check - tests worked now also without. Add generic drawerswitcher utility for demos. 2024-02-08 13:11:10 +01:00
Aiosa
a97fe34d74 Remove merge marks. 2024-02-05 09:47:10 +01:00
Aiosa
9ef2d46e75 Fix tests: always fetch up-to-date pixel values, prevent adding loaded tile to the 'bestTiles' array. Enforce _needsDraw check to be based on lastDrawn - we are async now. 2024-02-05 09:42:26 +01:00
Aiosa
fcf20be8ea Drawers now use new cache API to draw onto a canvas. The type conversion now requires also the tile argument so that conversion can rely on the tile metadata. 2024-02-04 18:48:25 +01:00
Aiosa
3fa13570ef Resolve Merge with First WebGL PR: code is merged, but not operable [drawer refactoring needs interaction implemented with cache system]. 2024-02-04 15:41:05 +01:00
Aiosa
3d6eb1b91c Fix broken tests (bad logics in event handling). 2023-12-10 17:58:50 +01:00
Aiosa
cf2413e0c9 Fix test for the preload hack (and fix the parentheses to always call updateMulti). 2023-12-10 16:49:56 +01:00
Aiosa
a690b50eee Add external execution pipeline (proof of concept implementation, needs polishing). Add filtering plugin live demo for testing. Fix issues with tile cache access outside its lifespan. Add custom css for the static page renderer and differentiate folder icons. Remove some old deprecations. 2023-12-10 16:34:42 +01:00
Aiosa
90ce0669c5 Add auto recognition of the need for tiledImage draw call. Fix ajax-headers test: did not finish because we don't call tile-loaded on cached tiles by default. 2023-11-27 12:12:54 +01:00
Aiosa
2c67860c61 Implement cache manipulation strategy: default copy on access if tile in the rendering process, remove 'canvas' type support, many bugfixes and new tests. 2023-11-26 21:32:26 +01:00
Aiosa
2a1090ffa8 Fix wrong test comparison. Add equality comparator to TileSource API. Return
deprecated support for getCompletionCallback. Turn on zombie cache if sources replaced & equal.
2023-11-19 16:14:28 +01:00
Aiosa
219049976c Add tests for zombie and data type conversion, ensure destructors are called.
Fix bugs (zombie was disabled on item replace, fix zombie cache system by separating to its own cache array). Fix CacheRecord destructor & dijkstra. Deduce cache only from originalCacheKey. Force explicit type declaration with types on users.
2023-11-18 20:16:35 +01:00
Aiosa
c3ab9a08e7 Merge branch 'cache-overhaul-reviewed' of github.com:RationAI/openseadragon into cache-overhaul-reviewed 2023-11-17 20:22:13 +01:00
Aiosa
023a864a36
Merge branch 'openseadragon:master' into cache-overhaul-reviewed 2023-11-17 20:21:24 +01:00
Aiosa
f796925ae5 Remove irrelevant code and comments. 2023-09-25 08:52:45 +02:00
Aiosa
750d45be81 Implement asynchronous tile processing logic wrt. tile cache conversion. 2023-09-24 22:30:28 +02:00
Aiosa
f01a7a4b3c Cache Overhaul for OpenSeadragon (areas to discuss marked with FIXME). 2023-09-08 08:47:43 +02:00
73 changed files with 8546 additions and 1673 deletions

View file

@ -35,5 +35,5 @@ keywords:
- high-resolution
- iiif
license: BSD-3-Clause
version: 5.0.0
date-released: 2024-08-14
version: 5.0.1
date-released: 2024-11-09

View file

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

View file

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

View file

@ -27,3 +27,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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.pitt.edu/"><img alt="University of Pittsburgh Logo" src="assets/logos/pitt-logo.png" height="70" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,6 +1,51 @@
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)
* WebGLDrawer now supports the imageSmoothingEnabled option (#2615 @pearcetm)
* Fixed: If you switched from WebGL drawer to canvas drawer, it didn't clean up properly (#2570 @pearcetm)
* Fixed: TiledImage.setClip would sometimes leave tiles unloaded (#2590 @pearcetm)
* Fixed: The WebGL drawer didn't support viewportMargins (#2600, #2606 @pearcetm)
* Fixed: If you set viewport rotation before flip, the navigator display region would be drawn wrong (#2619 @jbakarich)
* Fixed: In some cases, the WebGL drawer would draw the image in white (#2620 @sbarex)
* Fixed: If the user changed the page zoom or moved the window a different monitor, the image would disappear (#2627 @pearcetm)
5.0.0:
* BREAKING CHANGE: Dropped support for IE11 (#2300, #2361, #2553 @AndrewADev, @msalsbery)

109
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "openseadragon",
"version": "4.1.1",
"version": "5.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openseadragon",
"version": "4.1.1",
"version": "5.0.0",
"license": "BSD-3-Clause",
"devDependencies": {
"eslint-plugin-compat": "^4.1.2",
@ -998,9 +998,14 @@
}
},
"node_modules/destroy": {
"version": "1.0.4",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"license": "MIT"
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-file": {
"version": "1.0.0",
@ -1600,8 +1605,9 @@
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -1873,8 +1879,9 @@
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -3512,8 +3519,9 @@
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
},
@ -4202,8 +4210,9 @@
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -4357,60 +4366,77 @@
}
},
"node_modules/send": {
"version": "0.17.2",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/depd": {
"version": "1.1.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/http-errors": {
"version": "1.8.1",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "~1.1.2",
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.6"
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/send/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"license": "MIT"
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
"node_modules/send/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "ISC"
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-index": {
"version": "1.9.1",
@ -4430,19 +4456,29 @@
}
},
"node_modules/serve-static": {
"version": "1.14.2",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.2"
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"dev": true,
@ -4731,8 +4767,9 @@
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
}
@ -5180,4 +5217,4 @@
}
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "openseadragon",
"version": "5.0.0",
"version": "5.0.1",
"description": "Provides a smooth, zoomable user interface for HTML/Javascript.",
"keywords": [
"image",
@ -46,6 +46,8 @@
},
"scripts": {
"test": "grunt test",
"prepare": "grunt build"
"prepare": "grunt build",
"build": "grunt build",
"dev": "grunt dev"
}
}

View file

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

488
src/datatypeconvertor.js Normal file
View file

@ -0,0 +1,488 @@
/*
* OpenSeadragon.convertor (static property)
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function($){
/**
* modified from https://gist.github.com/Prottoy2938/66849e04b0bac459606059f5f9f3aa1a
* @private
*/
class WeightedGraph {
constructor() {
this.adjacencyList = {};
this.vertices = {};
}
/**
* Add vertex to graph
* @param vertex unique vertex ID
* @return {boolean} true if inserted, false if exists (no-op)
*/
addVertex(vertex) {
if (!this.vertices[vertex]) {
this.vertices[vertex] = new $.PriorityQueue.Node(0, vertex);
this.adjacencyList[vertex] = [];
return true;
}
return false;
}
/**
* Add edge to graph
* @param vertex1 id, must exist by calling addVertex()
* @param vertex2 id, must exist by calling addVertex()
* @param weight
* @param transform function that transforms on path vertex1 -> vertex2
* @return {boolean} true if new edge, false if replaced existing
*/
addEdge(vertex1, vertex2, weight, transform) {
if (weight < 0) {
$.console.error("WeightedGraph: negative weights will make for invalid shortest path computation!");
}
const outgoingPaths = this.adjacencyList[vertex1],
replacedEdgeIndex = outgoingPaths.findIndex(edge => edge.target === this.vertices[vertex2]),
newEdge = { target: this.vertices[vertex2], origin: this.vertices[vertex1], weight, transform };
if (replacedEdgeIndex < 0) {
this.adjacencyList[vertex1].push(newEdge);
return true;
}
this.adjacencyList[vertex1][replacedEdgeIndex] = newEdge;
return false;
}
/**
* @return {{path: ConversionStep[], cost: number}|undefined} cheapest path from start to finish
*/
dijkstra(start, finish) {
let path = []; //to return at end
if (start === finish) {
return {path: path, cost: 0};
}
const nodes = new OpenSeadragon.PriorityQueue();
let smallestNode;
//build up initial state
for (let vertex in this.vertices) {
vertex = this.vertices[vertex];
if (vertex.value === start) {
vertex.key = 0; //keys are known distances
nodes.insertNode(vertex);
} else {
vertex.key = Infinity;
delete vertex.index;
}
vertex._previous = null;
}
// as long as there is something to visit
while (nodes.getCount() > 0) {
smallestNode = nodes.remove();
if (smallestNode.value === finish) {
break;
}
const neighbors = this.adjacencyList[smallestNode.value];
for (let neighborKey in neighbors) {
let edge = neighbors[neighborKey];
//relax node
let newCost = smallestNode.key + edge.weight;
let nextNeighbor = edge.target;
if (newCost < nextNeighbor.key) {
nextNeighbor._previous = smallestNode;
//key change
nodes.decreaseKey(nextNeighbor, newCost);
}
}
}
if (!smallestNode || !smallestNode._previous || smallestNode.value !== finish) {
return undefined; //no path
}
let finalCost = smallestNode.key; //final weight last node
// done, build the shortest path
while (smallestNode._previous) {
//backtrack
const to = smallestNode.value,
parent = smallestNode._previous,
from = parent.value;
path.push(this.adjacencyList[from].find(x => x.target.value === to));
smallestNode = parent;
}
return {
path: path.reverse(),
cost: finalCost
};
}
}
/**
* Edge.transform function on the conversion path in OpenSeadragon.converter.getConversionPath().
* It can be also conversion to undefined if used as destructor implementation.
*
* @callback TypeConvertor
* @memberof OpenSeadragon
* @param {OpenSeadragon.Tile} tile reference tile that owns the data
* @param {any} data data in the input format
* @returns {any} data in the output format
*/
/**
* Destructor called every time a data type is to be destroyed or converted to another type.
*
* @callback TypeDestructor
* @memberof OpenSeadragon
* @param {any} data data in the format the destructor is registered for
* @returns {any} can return any value that is carried over to the caller if desirable.
* Note: not used by the OSD cache system.
*/
/**
* Node on the conversion path in OpenSeadragon.converter.getConversionPath().
*
* @typedef {Object} ConversionStep
* @memberof OpenSeadragon
* @param {OpenSeadragon.PriorityQueue.Node} target - Target node of the conversion step.
* Its value is the target format.
* @param {OpenSeadragon.PriorityQueue.Node} origin - Origin node of the conversion step.
* Its value is the origin format.
* @param {number} weight cost of the conversion
* @param {TypeConvertor} transform the conversion itself
*/
/**
* Class that orchestrates automated data types conversion. Do not instantiate
* this class, use OpenSeadragon.convertor - a global instance, instead.
* @class DataTypeConvertor
* @memberOf OpenSeadragon
*/
$.DataTypeConvertor = class {
constructor() {
this.graph = new WeightedGraph();
this.destructors = {};
this.copyings = {};
// Teaching OpenSeadragon built-in conversions:
const imageCreator = (tile, url) => new $.Promise((resolve, reject) => {
if (!$.supportsAsync) {
throw "Not supported in sync mode!";
}
const img = new Image();
img.onerror = img.onabort = reject;
img.onload = () => resolve(img);
img.src = url;
});
const canvasContextCreator = (tile, imageData) => {
const canvas = document.createElement( 'canvas' );
canvas.width = imageData.width;
canvas.height = imageData.height;
const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage( imageData, 0, 0 );
return context;
};
this.learn("context2d", "webImageUrl", (tile, ctx) => ctx.canvas.toDataURL(), 1, 2);
this.learn("image", "webImageUrl", (tile, image) => image.url);
this.learn("image", "context2d", canvasContextCreator, 1, 1);
this.learn("url", "image", imageCreator, 1, 1);
//Copies
this.learn("image", "image", (tile, image) => imageCreator(tile, image.src), 1, 1);
this.learn("url", "url", (tile, url) => url, 0, 1); //strings are immutable, no need to copy
this.learn("context2d", "context2d", (tile, ctx) => canvasContextCreator(tile, ctx.canvas));
/**
* Free up canvas memory
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
* and Safari keeps canvas until its height and width will be set to 0).
*/
this.learnDestroy("context2d", ctx => {
ctx.canvas.width = 0;
ctx.canvas.height = 0;
});
}
/**
* Unique identifier (unlike toString.call(x)) to be guessed
* from the data value. This type guess is more strict than
* OpenSeadragon.type() implementation, but for most type recognition
* this test relies on the output of OpenSeadragon.type().
*
* Note: although we try to implement the type guessing, do
* not rely on this functionality! Prefer explicit type declaration.
*
* @function guessType
* @param x object to get unique identifier for
* - can be array, in that case, alphabetically-ordered list of inner unique types
* is returned (null, undefined are ignored)
* - if $.isPlainObject(x) is true, then the object can define
* getType function to specify its type
* - otherwise, toString.call(x) is applied to get the parameter description
* @return {string} unique variable descriptor
*/
guessType( x ) {
if (Array.isArray(x)) {
const types = [];
for (let item of x) {
if (item === undefined || item === null) {
continue;
}
const type = this.guessType(item);
if (!types.includes(type)) {
types.push(type);
}
}
types.sort();
return `Array [${types.join(",")}]`;
}
const guessType = $.type(x);
if (guessType === "dom-node") {
//distinguish nodes
return guessType.nodeName.toLowerCase();
}
if (guessType === "object") {
if ($.isFunction(x.getType)) {
return x.getType();
}
}
return guessType;
}
/**
* Teach the system to convert data type 'from' -> 'to'
* @param {string} from unique ID of the data item 'from'
* @param {string} to unique ID of the data item 'to'
* @param {OpenSeadragon.TypeConvertor} callback convertor that takes two arguments: a tile reference, and
* a data object of a type 'from'; and converts this data object to type 'to'. It can return also the value
* wrapped in a Promise (returned in resolve) or it can be async function.
* @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7.
* Should reflect the actual cost of the conversion:
* - if nothing must be done and only reference is retrieved (or a constant operation done),
* return 0 (default)
* - if a linear amount of work is necessary,
* return 1
* ... and so on, basically the number in O() complexity power exponent (for simplification)
* @param {Number} [costMultiplier=1] multiplier of the cost class, e.g. O(3n^2) would
* use costPower=2, costMultiplier=3; can be between 1 and 10^5
*/
learn(from, to, callback, costPower = 0, costMultiplier = 1) {
$.console.assert(costPower >= 0 && costPower <= 7, "[DataTypeConvertor] Conversion costPower must be between <0, 7>.");
$.console.assert($.isFunction(callback), "[DataTypeConvertor:learn] Callback must be a valid function!");
if (from === to) {
this.copyings[to] = callback;
} else {
//we won't know if somebody added multiple edges, though it will choose some edge anyway
costPower++;
costMultiplier = Math.min(Math.max(costMultiplier, 1), 10 ^ 5);
this.graph.addVertex(from);
this.graph.addVertex(to);
this.graph.addEdge(from, to, costPower * 10 ^ 5 + costMultiplier, callback);
this._known = {}; //invalidate precomputed paths :/
}
}
/**
* Teach the system to destroy data type 'type'
* for example, textures loaded to GPU have to be also manually removed when not needed anymore.
* Needs to be defined only when the created object has extra deletion process.
* @param {string} type
* @param {OpenSeadragon.TypeDestructor} callback destructor, receives the object created,
* it is basically a type conversion to 'undefined' - thus the type.
*/
learnDestroy(type, callback) {
this.destructors[type] = callback;
}
/**
* Convert data item x of type 'from' to any of the 'to' types, chosen is the cheapest known conversion.
* Data is destroyed upon conversion. For different behavior, implement your conversion using the
* path rules obtained from getConversionPath().
* Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion).
* It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these
* manually if these should be destroyed.
* @param {OpenSeadragon.Tile} tile
* @param {any} data data item to convert
* @param {string} from data item type
* @param {string} to desired type(s)
* @return {OpenSeadragon.Promise<?>} promise resolution with type 'to' or undefined if the conversion failed
*/
convert(tile, data, from, ...to) {
const conversionPath = this.getConversionPath(from, to);
if (!conversionPath) {
$.console.error(`[OpenSeadragon.convertor.convert] Conversion ${from} ---> ${to} cannot be done!`);
return $.Promise.resolve();
}
const stepCount = conversionPath.length,
_this = this;
const step = (x, i, destroy = true) => {
if (i >= stepCount) {
return $.Promise.resolve(x);
}
let edge = conversionPath[i];
let y = edge.transform(tile, x);
if (y === undefined) {
$.console.error(`[OpenSeadragon.convertor.convert] data mid result undefined value (while converting using %s)`, edge);
return $.Promise.resolve();
}
//node.value holds the type string
if (destroy) {
_this.destroy(x, edge.origin.value);
}
const result = $.type(y) === "promise" ? y : $.Promise.resolve(y);
return result.then(res => step(res, i + 1));
};
//destroy only mid-results, but not the original value
return step(data, 0, false);
}
/**
* Copy the data item given.
* @param {OpenSeadragon.Tile} tile
* @param {any} data data item to convert
* @param {string} type data type
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor
*/
copy(tile, data, type) {
const copyTransform = this.copyings[type];
if (copyTransform) {
const y = copyTransform(tile, data);
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
}
$.console.warn(`[OpenSeadragon.convertor.copy] is not supported with type %s`, type);
return $.Promise.resolve(undefined);
}
/**
* Destroy the data item given.
* @param {string} type data type
* @param {any} data
* @return {OpenSeadragon.Promise<any>|undefined} promise resolution with data passed from constructor, or undefined
* if not such conversion exists
*/
destroy(data, type) {
const destructor = this.destructors[type];
if (destructor) {
const y = destructor(data);
return $.type(y) === "promise" ? y : $.Promise.resolve(y);
}
return undefined;
}
/**
* Get possible system type conversions and cache result.
* @param {string} from data item type
* @param {string|string[]} to array of accepted types
* @return {ConversionStep[]|undefined} array of required conversions (returns empty array
* for from===to), or undefined if the system cannot convert between given types.
* Each object has 'transform' function that converts between neighbouring types, such
* that x = arr[i].transform(x) is valid input for convertor arr[i+1].transform(), e.g.
* arr[i+1].transform(arr[i].transform( ... )) is a valid conversion procedure.
*
* Note: if a function is returned, it is a callback called once the data is ready.
*/
getConversionPath(from, to) {
let bestConvertorPath, selectedType;
let knownFrom = this._known[from];
if (!knownFrom) {
this._known[from] = knownFrom = {};
}
if (Array.isArray(to)) {
$.console.assert(to.length > 0, "[getConversionPath] conversion 'to' type must be defined.");
let bestCost = Infinity;
//FIXME: pre-compute all paths in 'to' array? could be efficient for multiple
// type system, but overhead for simple use cases... now we just use the first type if costs unknown
selectedType = to[0];
for (const outType of to) {
const conversion = knownFrom[outType];
if (conversion && bestCost > conversion.cost) {
bestConvertorPath = conversion;
bestCost = conversion.cost;
selectedType = outType;
}
}
} else {
$.console.assert(typeof to === "string", "[getConversionPath] conversion 'to' type must be defined.");
bestConvertorPath = knownFrom[to];
selectedType = to;
}
if (!bestConvertorPath) {
bestConvertorPath = this.graph.dijkstra(from, selectedType);
this._known[from][selectedType] = bestConvertorPath;
}
return bestConvertorPath ? bestConvertorPath.path : undefined;
}
/**
* Return a list of known conversion types
* @return {string[]}
*/
getKnownTypes() {
return Object.keys(this.graph.vertices);
}
/**
* Check whether given type is known to the convertor
* @param {string} type type to test
* @return {boolean}
*/
existsType(type) {
return !!this.graph.vertices[type];
}
};
/**
* Static convertor available throughout OpenSeadragon.
*
* Built-in conversions include types:
* - context2d canvas 2d context
* - image HTMLImage element
* - url url string carrying or pointing to 2D raster data
* - canvas HTMLCanvas element
*
* @type OpenSeadragon.DataTypeConvertor
* @memberOf OpenSeadragon
*/
$.convertor = new $.DataTypeConvertor();
}(OpenSeadragon));

View file

@ -34,7 +34,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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,268 +31,235 @@
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function ($) {
/**
* @class ImageTileSource
* @classdesc The ImageTileSource allows a simple image to be loaded
* into an OpenSeadragon Viewer.
* There are 2 ways to open an ImageTileSource:
* 1. viewer.open({type: 'image', url: fooUrl});
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
*
* With the first syntax, the crossOriginPolicy, ajaxWithCredentials and
* useCanvas options are inherited from the viewer if they are not
* specified directly in the options object.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.TileSource
* @param {Object} options Options object.
* @param {String} options.url URL of the image
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
* pyramid will be built internally to provide a better downsampling.
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
* 'Anonymous', 'use-credentials', and false. If false, image requests will
* not use CORS preventing internal pyramid building for images from other
* domains.
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
* @param {Boolean} [options.useCanvas=true] Set to false to prevent any use
* of the canvas API.
*/
$.ImageTileSource = class extends $.TileSource {
/**
* @class ImageTileSource
* @classdesc The ImageTileSource allows a simple image to be loaded
* into an OpenSeadragon Viewer.
* There are 2 ways to open an ImageTileSource:
* 1. viewer.open({type: 'image', url: fooUrl});
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
*
* With the first syntax, the crossOriginPolicy and ajaxWithCredentials
* options are inherited from the viewer if they are not
* specified directly in the options object.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.TileSource
* @param {Object} options Options object.
* @param {String} options.url URL of the image
* @param {Boolean} [options.buildPyramid=true] If set to true (default), a
* pyramid will be built internally to provide a better downsampling.
* @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are
* 'Anonymous', 'use-credentials', and false. If false, image requests will
* not use CORS preventing internal pyramid building for images from other
* domains.
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
*/
$.ImageTileSource = function (options) {
options = $.extend({
constructor(props) {
super($.extend({
buildPyramid: true,
crossOriginPolicy: false,
ajaxWithCredentials: false
}, options);
$.TileSource.apply(this, [options]);
ajaxWithCredentials: false,
}, props));
}
};
/**
* Determine if the data and/or url imply the image service is supported by
* this tile source.
* @function
* @param {Object|Array} data
* @param {String} url - optional
*/
supports(data, url) {
return data.type && data.type === "image";
}
/**
*
* @function
* @param {Object} options - the options
* @param {String} dataUrl - the url the image was retrieved from, if any.
* @param {String} postData - HTTP POST data in k=v&k2=v2... form or null
* @returns {Object} options - A dictionary of keyword arguments sufficient
* to configure this tile sources constructor.
*/
configure(options, dataUrl, postData) {
return options;
}
/**
* Responsible for retrieving, and caching the
* image metadata pertinent to this TileSources implementation.
* @function
* @param {String} url
* @throws {Error}
*/
getImageInfo(url) {
const image = new Image(),
_this = this;
$.extend($.ImageTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ImageTileSource.prototype */{
/**
* Determine if the data and/or url imply the image service is supported by
* this tile source.
* @function
* @param {Object|Array} data
* @param {String} optional - url
*/
supports: function (data, url) {
return data.type && data.type === "image";
},
/**
*
* @function
* @param {Object} options - the options
* @param {String} dataUrl - the url the image was retrieved from, if any.
* @param {String} postData - HTTP POST data in k=v&k2=v2... form or null
* @returns {Object} options - A dictionary of keyword arguments sufficient
* to configure this tile sources constructor.
*/
configure: function (options, dataUrl, postData) {
return options;
},
/**
* Responsible for retrieving, and caching the
* image metadata pertinent to this TileSources implementation.
* @function
* @param {String} url
* @throws {Error}
*/
getImageInfo: function (url) {
var image = this._image = new Image();
var _this = this;
if (this.crossOriginPolicy) {
image.crossOrigin = this.crossOriginPolicy;
}
if (this.ajaxWithCredentials) {
image.useCredentials = this.ajaxWithCredentials;
}
if (this.crossOriginPolicy) {
image.crossOrigin = this.crossOriginPolicy;
}
if (this.ajaxWithCredentials) {
image.useCredentials = this.ajaxWithCredentials;
}
$.addEvent(image, 'load', function () {
_this.width = image.naturalWidth;
_this.height = image.naturalHeight;
_this.aspectRatio = _this.width / _this.height;
_this.dimensions = new $.Point(_this.width, _this.height);
_this._tileWidth = _this.width;
_this._tileHeight = _this.height;
_this.tileOverlap = 0;
_this.minLevel = 0;
_this.image = image;
_this.levels = _this._buildLevels(image);
_this.maxLevel = _this.levels.length - 1;
$.addEvent(image, 'load', function () {
_this.width = image.naturalWidth;
_this.height = image.naturalHeight;
_this.aspectRatio = _this.width / _this.height;
_this.dimensions = new $.Point(_this.width, _this.height);
_this._tileWidth = _this.width;
_this._tileHeight = _this.height;
_this.tileOverlap = 0;
_this.minLevel = 0;
_this.levels = _this._buildLevels();
_this.maxLevel = _this.levels.length - 1;
_this.ready = true;
_this.ready = true;
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('ready', {tileSource: _this});
});
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('ready', {tileSource: _this});
$.addEvent(image, 'error', function () {
_this.image = null;
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('open-failed', {
message: "Error loading image at " + url,
source: url
});
});
$.addEvent(image, 'error', function () {
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('open-failed', {
message: "Error loading image at " + url,
source: url
});
});
image.src = url;
}
/**
* @function
* @param {Number} level
*/
getLevelScale(level) {
let levelScale = NaN;
if (level >= this.minLevel && level <= this.maxLevel) {
levelScale =
this.levels[level].width /
this.levels[this.maxLevel].width;
}
return levelScale;
}
/**
* @function
* @param {Number} level
*/
getNumTiles(level) {
if (this.getLevelScale(level)) {
return new $.Point(1, 1);
}
return new $.Point(0, 0);
}
/**
* Retrieves a tile url
* @function
* @param {Number} level Level of the tile
* @param {Number} x x coordinate of the tile
* @param {Number} y y coordinate of the tile
*/
getTileUrl(level, x, y) {
if (level === this.maxLevel) {
return this.url; //for original image, preserve url
}
//make up url by positional args
return `${this.url}?l=${level}&x=${x}&y=${y}`;
}
image.src = url;
},
/**
* @function
* @param {Number} level
*/
getLevelScale: function (level) {
var levelScale = NaN;
if (level >= this.minLevel && level <= this.maxLevel) {
levelScale =
this.levels[level].width /
this.levels[this.maxLevel].width;
}
return levelScale;
},
/**
* @function
* @param {Number} level
*/
getNumTiles: function (level) {
var scale = this.getLevelScale(level);
if (scale) {
return new $.Point(1, 1);
} else {
return new $.Point(0, 0);
}
},
/**
* Retrieves a tile url
* @function
* @param {Number} level Level of the tile
* @param {Number} x x coordinate of the tile
* @param {Number} y y coordinate of the tile
*/
getTileUrl: function (level, x, y) {
var url = null;
if (level >= this.minLevel && level <= this.maxLevel) {
url = this.levels[level].url;
}
return url;
},
/**
* Retrieves a tile context 2D
* @function
* @param {Number} level Level of the tile
* @param {Number} x x coordinate of the tile
* @param {Number} y y coordinate of the tile
*/
getContext2D: function (level, x, y) {
var context = null;
if (level >= this.minLevel && level <= this.maxLevel) {
context = this.levels[level].context2D;
}
return context;
},
/**
* Destroys ImageTileSource
* @function
* @param {OpenSeadragon.Viewer} viewer the viewer that is calling
* destroy on the ImageTileSource
*/
destroy: function (viewer) {
this._freeupCanvasMemory(viewer);
},
/**
* Equality comparator
*/
equals(otherSource) {
return this.url === otherSource.url;
}
// private
//
// Builds the different levels of the pyramid if possible
// (i.e. if canvas API enabled and no canvas tainting issue).
_buildLevels: function () {
var levels = [{
url: this._image.src,
width: this._image.naturalWidth,
height: this._image.naturalHeight
}];
getTilePostData(level, x, y) {
return {level: level, x: x, y: y};
}
if (!this.buildPyramid || !$.supportsCanvas) {
// We don't need the image anymore. Allows it to be GC.
delete this._image;
return levels;
}
/**
* Retrieves a tile context 2D
* @deprecated
*/
getContext2D(level, x, y) {
$.console.error('Using [TiledImage.getContext2D] (for plain images only) is deprecated. ' +
'Use overridden downloadTileStart (https://openseadragon.github.io/examples/advanced-data-model/) instead.');
return this._createContext2D();
}
var currentWidth = this._image.naturalWidth;
var currentHeight = this._image.naturalHeight;
downloadTileStart(job) {
const tileData = job.postData;
if (tileData.level === this.maxLevel) {
job.finish(this.image, null, "image");
return;
}
if (tileData.level >= this.minLevel && tileData.level <= this.maxLevel) {
const levelData = this.levels[tileData.level];
const context = this._createContext2D(this.image, levelData.width, levelData.height);
job.finish(context, null, "context2d");
return;
}
job.fail(`Invalid level ${tileData.level} for plain image source. Did you forget to set buildPyramid=true?`);
}
var bigCanvas = document.createElement("canvas");
var bigContext = bigCanvas.getContext("2d");
downloadTileAbort(job) {
//no-op
}
bigCanvas.width = currentWidth;
bigCanvas.height = currentHeight;
bigContext.drawImage(this._image, 0, 0, currentWidth, currentHeight);
// We cache the context of the highest level because the browser
// is a lot faster at downsampling something it already has
// downsampled before.
levels[0].context2D = bigContext;
// We don't need the image anymore. Allows it to be GC.
delete this._image;
// private
//
// Builds the different levels of the pyramid if possible
// (i.e. if canvas API enabled and no canvas tainting issue).
_buildLevels(image) {
const levels = [{
url: image.src,
width: image.naturalWidth,
height: image.naturalHeight
}];
if ($.isCanvasTainted(bigCanvas)) {
// If the canvas is tainted, we can't compute the pyramid.
return levels;
}
// We build smaller levels until either width or height becomes
// 1 pixel wide.
while (currentWidth >= 2 && currentHeight >= 2) {
currentWidth = Math.floor(currentWidth / 2);
currentHeight = Math.floor(currentHeight / 2);
var smallCanvas = document.createElement("canvas");
var smallContext = smallCanvas.getContext("2d");
smallCanvas.width = currentWidth;
smallCanvas.height = currentHeight;
smallContext.drawImage(bigCanvas, 0, 0, currentWidth, currentHeight);
levels.splice(0, 0, {
context2D: smallContext,
width: currentWidth,
height: currentHeight
});
bigCanvas = smallCanvas;
bigContext = smallContext;
}
if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) {
return levels;
},
/**
* Free up canvas memory
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
* and Safari keeps canvas until its height and width will be set to 0).
* @function
*/
_freeupCanvasMemory: function (viewer) {
for (var i = 0; i < this.levels.length; i++) {
if(this.levels[i].context2D){
this.levels[i].context2D.canvas.height = 0;
this.levels[i].context2D.canvas.width = 0;
}
if(viewer){
/**
* Triggered when an image has just been unloaded
*
* @event image-unloaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {CanvasRenderingContext2D} context2D - The context that is being unloaded
* @private
*/
viewer.raiseEvent("image-unloaded", {
context2D: this.levels[i].context2D
});
}
let currentWidth = image.naturalWidth,
currentHeight = image.naturalHeight;
// We build smaller levels until either width or height becomes
// 2 pixel wide.
while (currentWidth >= 2 && currentHeight >= 2) {
currentWidth = Math.floor(currentWidth / 2);
currentHeight = Math.floor(currentHeight / 2);
}
}
},
});
levels.push({
width: currentWidth,
height: currentHeight,
});
}
return levels.reverse();
}
_createContext2D(data, w, h) {
const canvas = document.createElement("canvas"),
context = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
context.drawImage(data, 0, 0, w, h);
return context;
}
};
}(OpenSeadragon));

View file

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

View file

@ -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));

View file

@ -347,7 +347,6 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
},
setDisplayTransform: function(rule) {
setElementTransform(this.displayRegion, rule);
setElementTransform(this.canvas, rule);
setElementTransform(this.element, rule);
},

View file

@ -214,7 +214,7 @@
* For complete list of modes, please @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation/ globalCompositeOperation}
*
* @property {Boolean} [imageSmoothingEnabled=true]
* Image smoothing for canvas rendering (only if the canvas drawer is used). Note: Ignored
* Image smoothing for rendering (only if the canvas or webgl drawer is used). Note: Ignored
* by some (especially older) browsers which do not support this canvas property.
* This property can be changed in {@link Viewer.DrawerBase.setImageSmoothingEnabled}.
*
@ -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));

View file

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

View file

@ -128,9 +128,18 @@
};
}
this.elementWrapper = document.createElement('div');
this.element = options.element;
this.element.innerHTML = "<div>" + this.element.innerHTML + "</div>";
this.style = options.element.style;
this.elementWrapper.appendChild(this.element);
if (this.element.id) {
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);
};
@ -197,7 +206,7 @@
* @function
*/
destroy: function() {
var element = this.element;
var element = this.elementWrapper;
var style = this.style;
if (element.parentNode) {
@ -242,7 +251,7 @@
* @param {Element} container
*/
drawHTML: function(container, viewport) {
var element = this.element;
var element = this.elementWrapper;
if (element.parentNode !== container) {
//save the source parent for later if we need it
element.prevElementParent = element.parentNode;
@ -253,7 +262,7 @@
this.style.position = "absolute";
// this.size is used by overlays which don't get scaled in at
// least one direction when this.checkResize is set to false.
this.size = $.getElementSize(element);
this.size = $.getElementSize(this.elementWrapper);
}
var positionAndSize = this._getOverlayPositionAndSize(viewport);
var position = positionAndSize.position;
@ -270,15 +279,15 @@
this.onDraw(position, size, this.element);
} else {
var style = this.style;
var innerElement = element.firstChild;
var innerStyle = innerElement.style;
var innerStyle = this.element.style;
innerStyle.display = "block";
style.left = position.x + "px";
style.top = position.y + "px";
if (this.width !== null) {
style.width = size.x + "px";
innerStyle.width = size.x + "px";
}
if (this.height !== null) {
style.height = size.y + "px";
innerStyle.height = size.y + "px";
}
var transformOriginProp = $.getCssPropertyWithVendorPrefix(
'transformOrigin');
@ -303,7 +312,7 @@
style[transformProp] = "";
}
}
style.display = 'block';
style.display = 'flex';
}
},
@ -355,7 +364,7 @@
}
if (this.checkResize &&
(this.width === null || this.height === null)) {
var eltSize = this.size = $.getElementSize(this.element);
var eltSize = this.size = $.getElementSize(this.elementWrapper);
if (this.width === null) {
width = eltSize.x;
}

362
src/priorityqueue.js Normal file
View file

@ -0,0 +1,362 @@
/*
* OpenSeadragon - Queue
*
* Copyright (C) 2024 OpenSeadragon contributors (modified)
* Copyright (C) Google Inc., The Closure Library Authors.
* https://github.com/google/closure-library
*
* SPDX-License-Identifier: Apache-2.0
*/
(function($) {
/**
* @class PriorityQueue
* @classdesc Fast priority queue. Implemented as a Heap.
*/
$.PriorityQueue = class {
/**
* @param {?OpenSeadragon.PriorityQueue} optHeap Optional Heap or
* Object to initialize heap with.
*/
constructor(optHeap = undefined) {
/**
* The nodes of the heap.
*
* This is a densely packed array containing all nodes of the heap, using
* the standard flat representation of a tree as an array (i.e. element [0]
* at the top, with [1] and [2] as the second row, [3] through [6] as the
* third, etc). Thus, the children of element `i` are `2i+1` and `2i+2`, and
* the parent of element `i` is `⌊(i-1)/2⌋`.
*
* The only invariant is that children's keys must be greater than parents'.
*
* @private
*/
this.nodes_ = [];
if (optHeap) {
this.insertAll(optHeap);
}
}
/**
* Insert the given value into the heap with the given key.
* @param {K} key The key.
* @param {V} value The value.
*/
insert(key, value) {
this.insertNode(new Node(key, value));
}
/**
* Insert node item.
* @param node
*/
insertNode(node) {
const nodes = this.nodes_;
node.index = nodes.length;
nodes.push(node);
this.moveUp_(node.index);
}
/**
* Adds multiple key-value pairs from another Heap or Object
* @param {?OpenSeadragon.PriorityQueue} heap Object containing the data to add.
*/
insertAll(heap) {
let keys, values;
if (heap instanceof $.PriorityQueue) {
keys = heap.getKeys();
values = heap.getValues();
// If it is a heap and the current heap is empty, I can rely on the fact
// that the keys/values are in the correct order to put in the underlying
// structure.
if (this.getCount() <= 0) {
const nodes = this.nodes_;
for (let i = 0; i < keys.length; i++) {
const node = new Node(keys[i], values[i]);
node.index = nodes.length;
nodes.push(node);
}
return;
}
} else {
throw "insertAll supports only OpenSeadragon.PriorityQueue object!";
}
for (let i = 0; i < keys.length; i++) {
this.insert(keys[i], values[i]);
}
}
/**
* Retrieves and removes the root value of this heap.
* @return {Node} The root node item removed from the root of the heap. Returns
* undefined if the heap is empty.
*/
remove() {
const nodes = this.nodes_;
const count = nodes.length;
const rootNode = nodes[0];
if (count <= 0) {
return undefined;
} else if (count == 1) { // eslint-disable-line
nodes.length = 0;
} else {
nodes[0] = nodes.pop();
if (nodes[0]) {
nodes[0].index = 0;
}
this.moveDown_(0);
}
if (rootNode) {
delete rootNode.index;
}
return rootNode;
}
/**
* Retrieves but does not remove the root value of this heap.
* @return {V} The value at the root of the heap. Returns
* undefined if the heap is empty.
*/
peek() {
const nodes = this.nodes_;
if (nodes.length == 0) { // eslint-disable-line
return undefined;
}
return nodes[0].value;
}
/**
* Retrieves but does not remove the key of the root node of this heap.
* @return {string} The key at the root of the heap. Returns undefined if the
* heap is empty.
*/
peekKey() {
return this.nodes_[0] && this.nodes_[0].key;
}
/**
* Move the node up in hierarchy
* @param {Node} node the node
* @param {K} key new ley, must be smaller than current key
*/
decreaseKey(node, key) {
if (node.index === undefined) {
node.key = key;
this.insertNode(node);
} else {
node.key = key;
this.moveUp_(node.index);
}
}
/**
* Moves the node at the given index down to its proper place in the heap.
* @param {number} index The index of the node to move down.
* @private
*/
moveDown_(index) {
const nodes = this.nodes_;
const count = nodes.length;
// Save the node being moved down.
const node = nodes[index];
// While the current node has a child.
while (index < (count >> 1)) {
const leftChildIndex = this.getLeftChildIndex_(index);
const rightChildIndex = this.getRightChildIndex_(index);
// Determine the index of the smaller child.
const smallerChildIndex = rightChildIndex < count &&
nodes[rightChildIndex].key < nodes[leftChildIndex].key ?
rightChildIndex :
leftChildIndex;
// If the node being moved down is smaller than its children, the node
// has found the correct index it should be at.
if (nodes[smallerChildIndex].key > node.key) {
break;
}
// If not, then take the smaller child as the current node.
nodes[index] = nodes[smallerChildIndex];
nodes[index].index = index;
index = smallerChildIndex;
}
nodes[index] = node;
if (node) {
node.index = index;
}
}
/**
* Moves the node at the given index up to its proper place in the heap.
* @param {number} index The index of the node to move up.
* @private
*/
moveUp_(index) {
const nodes = this.nodes_;
const node = nodes[index];
// While the node being moved up is not at the root.
while (index > 0) {
// If the parent is greater than the node being moved up, move the parent
// down.
const parentIndex = this.getParentIndex_(index);
if (nodes[parentIndex].key > node.key) {
nodes[index] = nodes[parentIndex];
nodes[index].index = index;
index = parentIndex;
} else {
break;
}
}
nodes[index] = node;
if (node) {
node.index = index;
}
}
/**
* Gets the index of the left child of the node at the given index.
* @param {number} index The index of the node to get the left child for.
* @return {number} The index of the left child.
* @private
*/
getLeftChildIndex_(index) {
return index * 2 + 1;
}
/**
* Gets the index of the right child of the node at the given index.
* @param {number} index The index of the node to get the right child for.
* @return {number} The index of the right child.
* @private
*/
getRightChildIndex_(index) {
return index * 2 + 2;
}
/**
* Gets the index of the parent of the node at the given index.
* @param {number} index The index of the node to get the parent for.
* @return {number} The index of the parent.
* @private
*/
getParentIndex_(index) {
return (index - 1) >> 1;
}
/**
* Gets the values of the heap.
* @return {!Array<*>} The values in the heap.
*/
getValues() {
const nodes = this.nodes_;
const rv = [];
const l = nodes.length;
for (let i = 0; i < l; i++) {
rv.push(nodes[i].value);
}
return rv;
}
/**
* Gets the keys of the heap.
* @return {!Array<string>} The keys in the heap.
*/
getKeys() {
const nodes = this.nodes_;
const rv = [];
const l = nodes.length;
for (let i = 0; i < l; i++) {
rv.push(nodes[i].key);
}
return rv;
}
/**
* Whether the heap contains the given value.
* @param {V} val The value to check for.
* @return {boolean} Whether the heap contains the value.
*/
containsValue(val) {
return this.nodes_.some((node) => node.value == val); // eslint-disable-line
}
/**
* Whether the heap contains the given key.
* @param {string} key The key to check for.
* @return {boolean} Whether the heap contains the key.
*/
containsKey(key) {
return this.nodes_.some((node) => node.value == key); // eslint-disable-line
}
/**
* Clones a heap and returns a new heap
* @return {!OpenSeadragon.PriorityQueue} A new Heap with the same key-value pairs.
*/
clone() {
return new $.PriorityQueue(this);
}
/**
* The number of key-value pairs in the map
* @return {number} The number of pairs.
*/
getCount() {
return this.nodes_.length;
}
/**
* Returns true if this heap contains no elements.
* @return {boolean} Whether this heap contains no elements.
*/
isEmpty() {
return this.nodes_.length === 0;
}
/**
* Removes all elements from the heap.
*/
clear() {
this.nodes_.length = 0;
}
};
$.PriorityQueue.Node = class {
constructor(key, value) {
/**
* The key.
* @type {K}
* @private
*/
this.key = key;
/**
* The value.
* @type {V}
* @private
*/
this.value = value;
/**
* The node index value. Updated in the heap.
* @type {number}
* @private
*/
this.index = 0;
}
clone() {
return new Node(this.key, this.value);
}
};
}(OpenSeadragon));

View file

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

File diff suppressed because it is too large Load diff

View file

@ -163,6 +163,7 @@ $.TiledImage = function( options ) {
_needsUpdate: true, // Does the tiledImage need to update the viewport again?
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
_tilesLoading: 0, // The number of pending tile requests.
_zombieCache: false, // Allow cache to stay in memory upon deletion.
_tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile]
_lastDrawn: [], // array of tiles that were last fetched by the drawer
_isBlending: false, // Are any tiles still being blended?
@ -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);
},
/**
@ -878,6 +896,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._clip = null;
}
this._needsUpdate = true;
this._needsDraw = true;
/**
* Raised when the TiledImage's clip is changed.
@ -906,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();
@ -979,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
@ -1065,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
@ -1161,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;
}
@ -1226,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);
@ -1316,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
@ -1403,6 +1453,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
levelOpacity,
levelVisibility,
drawArea,
loadArea,
currentTime,
bestTiles
);
@ -1432,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
},
/**
@ -1559,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>
@ -1596,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
@ -1621,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()) {
@ -1640,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;
},
/**
@ -1729,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 ){
/**
@ -1762,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,
@ -1783,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
@ -1849,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
@ -1872,7 +2030,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
urlOrGetter,
post,
ajaxHeaders,
context2D,
tile,
tilesMatrix = this.tilesMatrix,
tileSource = this.source;
@ -1904,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,
@ -1914,7 +2068,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bounds,
exists,
urlOrGetter,
context2D,
undefined,
this.loadTilesWithAjax,
ajaxHeaders,
sourceBounds,
@ -1956,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,
@ -1965,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,
});
}
},
/**
@ -1982,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.
@ -2018,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) {
@ -2050,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();
}
},
@ -2169,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

View file

@ -55,7 +55,8 @@
* @param {Object} options
* You can either specify a URL, or literally define the TileSource (by specifying
* width, height, tileSize, tileOverlap, minLevel, and maxLevel). For the former,
* the extending class is expected to implement 'getImageInfo' and 'configure'.
* the extending class is expected to implement 'supports' and 'configure'.
* Note that _in this case, the child class of getImageInfo() is ignored!_
* For the latter, the construction is assumed to occur through
* the extending classes implementation of 'configure'.
* @param {String} [options.url]
@ -72,6 +73,7 @@
* @param {Boolean} [options.splitHashDataForPost]
* First occurrence of '#' in the options.url is used to split URL
* and the latter part is treated as POST data (applies to getImageInfo(...))
* Does not work if getImageInfo() is overridden and used (see the options description)
* @param {Number} [options.width]
* Width of the source image at max resolution in pixels.
* @param {Number} [options.height]
@ -139,6 +141,12 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
} );
}
/**
* Retrieve context2D of this tile source
* @memberOf OpenSeadragon.TileSource
* @function getContext2D
*/
/**
* Ratio of width to height
* @member {Number} aspectRatio
@ -170,6 +178,8 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
* @memberof OpenSeadragon.TileSource#
*/
// TODO potentially buggy behavior: what if .url is used by child class before it calls super constructor?
// this can happen if old JS class definition is used
if( 'string' === $.type( arguments[ 0 ] ) ){
this.url = arguments[0];
}
@ -425,6 +435,13 @@ $.TileSource.prototype = {
/**
* Responsible for retrieving, and caching the
* image metadata pertinent to this TileSources implementation.
* There are three scenarios of opening a tile source: providing a parseable string, plain object, or an URL.
* This method is only called by OSD if the TileSource configuration is a non-parseable string (~url).
*
* The string can contain a hash `#` symbol, followed by
* key=value arguments. If this is the case, this method sends this
* data as a POST body.
*
* @function
* @param {String} url
* @throws {Error}
@ -554,7 +571,7 @@ $.TileSource.prototype = {
* @property {String} message
* @property {String} source
* @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* see TileSource::getTilePostData) or null
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'open-failed', {
@ -586,6 +603,17 @@ $.TileSource.prototype = {
return false;
},
/**
* Check whether two tileSources are equal. This is used for example
* when replacing tile-sources, which turns on the zombie cache before
* old item removal.
* @param {OpenSeadragon.TileSource} otherSource
* @returns {Boolean}
*/
equals: function (otherSource) {
return false;
},
/**
* Responsible for parsing and configuring the
* image metadata pertinent to this TileSources implementation.
@ -608,6 +636,16 @@ $.TileSource.prototype = {
throw new Error( "Method not implemented." );
},
/**
* Shall this source need to free some objects
* upon unloading, it must be done here. For example, canvas
* size must be set to 0 for safari to free.
* @param {OpenSeadragon.Viewer} viewer
*/
destroy: function ( viewer ) {
//no-op
},
/**
* Responsible for retrieving the url which will return an image for the
* region specified by the given x, y, and level components.
@ -683,9 +721,12 @@ $.TileSource.prototype = {
* The tile cache object is uniquely determined by this key and used to lookup
* the image data in cache: keys should be different if images are different.
*
* In case a tile has context2D property defined (TileSource.prototype.getContext2D)
* or its context2D is set manually; the cache is not used and this function
* is irrelevant.
* You can return falsey tile cache key, in which case the tile will
* be created without invoking ImageJob --- but with data=null. Then,
* you are responsible for manually creating the cache data. This is useful
* particularly if you want to use empty TiledImage with client-side derived data
* only. The default tile-cache key is then called "" - an empty string.
*
* Note: default behaviour does not take into account post data.
* @param {Number} level tile level it was fetched with
* @param {Number} x x-coordinate in the pyramid level
@ -693,6 +734,9 @@ $.TileSource.prototype = {
* @param {String} url the tile was fetched with
* @param {Object} ajaxHeaders the tile was fetched with
* @param {*} postData data the tile was fetched with (type depends on getTilePostData(..) return type)
* @return {?String} can return the cache key or null, in that case an empty cache is initialized
* without downloading any data for internal use: user has to define the cache contents manually, via
* the cache interface of this class.
*/
getTileHashKey: function(level, x, y, url, ajaxHeaders, postData) {
function withHeaders(hash) {
@ -723,10 +767,15 @@ $.TileSource.prototype = {
/**
* Decide whether tiles have transparency: this is crucial for correct images blending.
* Overriden on a tile level by setting tile.hasTransparency = true;
* @param context2D unused, deprecated argument
* @param url tile.getUrl() value for given tile
* @param ajaxHeaders tile.ajaxHeaders value for given tile
* @param post tile.post value for given tile
* @returns {boolean} true if the image has transparency
*/
hasTransparency: function(context2D, url, ajaxHeaders, post) {
return !!context2D || url.match('.png');
return url.match('.png');
},
/**
@ -738,41 +787,45 @@ $.TileSource.prototype = {
* @param {String} [context.ajaxHeaders] - Headers to add to the image request if using AJAX.
* @param {Boolean} [context.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
* @param {String} [context.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [context.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* @param {?String|?Object} [context.postData] - HTTP POST data (usually but not necessarily
* in k=v&k2=v2... form, see TileSource::getTilePostData) or null
* @param {*} [context.userData] - Empty object to attach your own data and helper variables to.
* @param {Function} [context.finish] - Should be called unless abort() was executed, e.g. on all occasions,
* be it successful or unsuccessful request.
* Usage: context.finish(data, request, errMessage). Pass the downloaded data object or null upon failure.
* Add also reference to an ajax request if used. Provide error message in case of failure.
* @param {Function} [context.finish] - Should be called unless abort() was executed upon successful
* data retrieval.
* Usage: context.finish(data, request, dataType=undefined). Pass the downloaded data object
* add also reference to an ajax request if used. Optionally, specify what data type the data is.
* @param {Function} [context.fail] - Should be called unless abort() was executed upon unsuccessful request.
* Usage: context.fail(errMessage, request). Provide error message in case of failure,
* add also reference to an ajax request if used.
* @param {Function} [context.abort] - Called automatically when the job times out.
* Usage: context.abort().
* @param {Function} [context.callback] @private - Called automatically once image has been downloaded
* Usage: if you decide to abort the request (no fail/finish will be called), call context.abort().
* @param {Function} [context.callback] Private parameter. Called automatically once image has been downloaded
* (triggered by finish).
* @param {Number} [context.timeout] @private - The max number of milliseconds that
* @param {Number} [context.timeout] Private parameter. The max number of milliseconds that
* this image job may take to complete.
* @param {string} [context.errorMsg] @private - The final error message, default null (set by finish).
* @param {string} [context.errorMsg] Private parameter. The final error message, default null (set by finish).
*/
downloadTileStart: function (context) {
var dataStore = context.userData,
const dataStore = context.userData,
image = new Image();
dataStore.image = image;
dataStore.request = null;
var finish = function(error) {
if (!image) {
context.finish(null, dataStore.request, "Image load failed: undefined Image instance.");
const finalize = function(error) {
if (error || !image) {
context.fail(error || "[downloadTileStart] Image load failed: undefined Image instance.",
dataStore.request);
return;
}
image.onload = image.onerror = image.onabort = null;
context.finish(error ? null : image, dataStore.request, error);
context.finish(image, dataStore.request, "image");
};
image.onload = function () {
finish();
finalize();
};
image.onabort = image.onerror = function() {
finish("Image load aborted.");
finalize("[downloadTileStart] Image load aborted.");
};
// Load the tile with an AJAX request if the loadWithAjax option is
@ -792,21 +845,21 @@ $.TileSource.prototype = {
try {
blb = new window.Blob([request.response]);
} catch (e) {
var BlobBuilder = (
const BlobBuilder = (
window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder
);
if (e.name === 'TypeError' && BlobBuilder) {
var bb = new BlobBuilder();
const bb = new BlobBuilder();
bb.append(request.response);
blb = bb.getBlob();
}
}
// If the blob is empty for some reason consider the image load a failure.
if (blb.size === 0) {
finish("Empty image response.");
finalize("[downloadTileStart] Empty image response.");
} else {
// Create a URL for the blob data and make it the source of the image object.
// This will still trigger Image.onload to indicate a successful tile load.
@ -814,7 +867,7 @@ $.TileSource.prototype = {
}
},
error: function(request) {
finish("Image load aborted - XHR error");
finalize("[downloadTileStart] Image load aborted - XHR error");
}
});
} else {
@ -828,6 +881,8 @@ $.TileSource.prototype = {
/**
* Provide means of aborting the execution.
* Note that if you override this function, you should override also downloadTileStart().
* Note that calling job.abort() would create an infinite loop!
*
* @param {ImageJob} context job, the same object as with downloadTileStart(..)
* @param {*} [context.userData] - Empty object to attach (and mainly read) your own data.
*/
@ -846,33 +901,44 @@ $.TileSource.prototype = {
* cacheObject parameter should be used to attach the data to, there are no
* conventions on how it should be stored - all the logic is implemented within *TileCache() functions.
*
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* Note that
* - data is cached automatically as cacheObject.data
* - if you override any of *TileCache() functions, you should override all of them.
* - these functions might be called over shared cache object managed by other TileSources simultaneously.
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
* @param {Tile} tile instance the cache was created with
* @param {OpenSeadragon.Tile} tile instance the cache was created with
* @deprecated
*/
createTileCache: function(cacheObject, data, tile) {
cacheObject._data = data;
$.console.error("[TileSource.createTileCache] has been deprecated. Use cache API of a tile instead.");
//no-op, we create the cache automatically
},
/**
* Cache object destructor, unset all properties you created to allow GC collection.
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* Note that these functions might be called over shared cache object managed by other TileSources simultaneously.
* Original cache data is cacheObject.data, but do not delete it manually! It is taken care for,
* you might break things.
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @deprecated
*/
destroyTileCache: function (cacheObject) {
cacheObject._data = null;
cacheObject._renderedContext = null;
$.console.error("[TileSource.destroyTileCache] has been deprecated. Use cache API of a tile instead.");
//no-op, handled internally
},
/**
* Raw data getter
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @returns {*} cache data
* Raw data getter, should return anything that is compatible with the system, or undefined
* if the system can handle it.
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @returns {OpenSeadragon.Promise<?>} cache data
* @deprecated
*/
getTileCacheData: function(cacheObject) {
return cacheObject._data;
$.console.error("[TileSource.getTileCacheData] has been deprecated. Use cache API of a tile instead.");
return cacheObject.getDataAs(undefined, false);
},
/**
@ -880,11 +946,14 @@ $.TileSource.prototype = {
* - plugins might need image representation of the data
* - div HTML rendering relies on image element presence
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* Note that these functions might be called over shared cache object managed by other TileSources simultaneously.
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @returns {Image} cache data as an Image
* @deprecated
*/
getTileCacheDataAsImage: function(cacheObject) {
return cacheObject._data; //the data itself by default is Image
$.console.error("[TileSource.getTileCacheDataAsImage] has been deprecated. Use cache API of a tile instead.");
return cacheObject.getImage();
},
/**
@ -892,21 +961,13 @@ $.TileSource.prototype = {
* - most heavily used rendering method is a canvas-based approach,
* convert the data to a canvas and return it's 2D context
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @param {OpenSeadragon.CacheRecord} cacheObject context cache object
* @returns {CanvasRenderingContext2D} context of the canvas representation of the cache data
* @deprecated
*/
getTileCacheDataAsContext2D: function(cacheObject) {
if (!cacheObject._renderedContext) {
var canvas = document.createElement( 'canvas' );
canvas.width = cacheObject._data.width;
canvas.height = cacheObject._data.height;
cacheObject._renderedContext = canvas.getContext('2d');
cacheObject._renderedContext.drawImage( cacheObject._data, 0, 0 );
//since we are caching the prerendered image on a canvas
//allow the image to not be held in memory
cacheObject._data = null;
}
return cacheObject._renderedContext;
$.console.error("[TileSource.getTileCacheDataAsContext2D] has been deprecated. Use cache API of a tile instead.");
return cacheObject.getRenderedContext();
}
};

View file

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

View file

@ -432,6 +432,7 @@ $.Viewer = function( options ) {
// Create the tile cache
this.tileCache = new $.TileCache({
viewer: this,
maxImageCacheCount: this.maxImageCacheCount
});
@ -761,6 +762,30 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
return this;
},
/**
* Updates data within every tile in the viewer. Should be called
* when tiles are outdated and should be re-processed. Useful mainly
* for plugins that change tile data.
* @function
* @param {Boolean} [restoreTiles=true] if true, tile processing starts from the tile original data
* @fires OpenSeadragon.Viewer.event:tile-invalidated
* @return {OpenSeadragon.Promise<?>}
*/
requestInvalidate: function (restoreTiles = true) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return $.Promise.resolve();
}
const tStamp = $.now();
const worldPromise = this.world.requestInvalidate(restoreTiles, tStamp);
if (!this.navigator) {
return worldPromise;
}
const navigatorPromise = this.navigator.world.requestInvalidate(restoreTiles, tStamp);
return $.Promise.all([worldPromise, navigatorPromise]);
},
/**
* @function
@ -787,6 +812,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
THIS[ this.hash ].animating = false;
this.world.removeAll();
this.tileCache.clear();
this.imageLoader.clear();
/**
@ -1004,7 +1030,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* @returns {Boolean}
*/
isMouseNavEnabled: function () {
return this.innerTracker.isTracking();
return this.innerTracker.tracking;
},
/**
@ -1110,7 +1136,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
ajaxHeaders = {};
}
if (!$.isPlainObject(ajaxHeaders)) {
console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
$.console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return;
}
if (propagate === undefined) {
@ -1545,7 +1571,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas.
* @param {Number} [options.opacity=1] Proportional opacity of the tiled images (1=opaque, 0=hidden)
* @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
* @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
* @param {Boolean} [options.zombieCache] In the case that this method removes any TiledImage instance,
* allow the item-referenced cache to remain in memory even without active tiles. Default false.
* @param {Number} [options.degrees=0] Initial rotation of the tiled image around
* its top left corner in degrees.
* @param {Boolean} [options.flipped=false] Whether to horizontally flip the image.
@ -1683,11 +1711,15 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
_this._loadQueue.splice(0, 1);
if (queueItem.options.replace) {
var newIndex = _this.world.getIndexOfItem(queueItem.options.replaceItem);
const replaced = queueItem.options.replaceItem;
const newIndex = _this.world.getIndexOfItem(replaced);
if (newIndex !== -1) {
queueItem.options.index = newIndex;
}
_this.world.removeItem(queueItem.options.replaceItem);
if (!replaced._zombieCache && replaced.source.equals(queueItem.tileSource)) {
replaced.allowZombieCache(true);
}
_this.world.removeItem(replaced);
}
tiledImage = new $.TiledImage({
@ -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.
@ -2542,8 +2577,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
},
/**
* Update pixel density ratio, clears all tiles and triggers updates for
* all items if the ratio has changed.
* Update pixel density ratio and forces a resize operation.
* @private
*/
_updatePixelDensityRatio: function() {
@ -2551,8 +2585,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
var currentPixelDensityRatio = $.getCurrentPixelDensityRatio();
if (previusPixelDensityRatio !== currentPixelDensityRatio) {
$.pixelDensityRatio = currentPixelDensityRatio;
this.world.resetItems();
this.forceRedraw();
this.forceResize();
}
},
@ -2592,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();
}
},
});
@ -2699,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);
}
});
@ -3924,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 ) );
}
@ -3969,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();

View file

@ -40,23 +40,23 @@
/**
* @class OpenSeadragon.WebGLDrawer
* @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer
* loads tile data as textures to the graphics card as soon as it is available (via the tile-ready event),
* and unloads the data (via the image-unloaded event). The drawer utilizes a context-dependent two pass drawing pipeline.
* For the first pass, tile composition for a given TiledImage is always done using a canvas with a WebGL context.
* This allows tiles to be stitched together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
* overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output canvas
* with a Context2d context. This allows applications to have access to pixel data and other functionality provided by
* Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options, including compositeOperation,
* clip, croppingPolygons, and debugMode are implemented using Context2d operations; in these scenarios, each TiledImage is
* drawn onto the output canvas immediately after the tile composition step (pass 1). Otherwise, for efficiency, all TiledImages
* are copied over to the output canvas at once, after all tiles have been composited for all images.
* defines its own data type that ensures textures are correctly loaded to and deleted from the GPU memory.
* The drawer utilizes a context-dependent two pass drawing pipeline. For the first pass, tile composition
* for a given TiledImage is always done using a canvas with a WebGL context. This allows tiles to be stitched
* together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
* overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output
* canvas with a Context2d context. This allows applications to have access to pixel data and other functionality
* provided by Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options,
* including compositeOperation, clip, croppingPolygons, and debugMode are implemented using Context2d operations;
* in these scenarios, each TiledImage is drawn onto the output canvas immediately after the tile composition step
* (pass 1). Otherwise, for efficiency, all TiledImages are copied over to the output canvas at once, after all
* tiles have been composited for all images.
* @param {Object} options - Options for this Drawer.
* @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer.
* @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport.
* @param {Element} options.element - Parent element.
* @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
*/
OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){
super(options);
@ -76,15 +76,11 @@
// private members
this._destroyed = false;
this._TextureMap = new Map();
this._TileMap = new Map();
this._gl = null;
this._firstPass = null;
this._secondPass = null;
this._glFrameBuffer = null;
this._renderToTexture = null;
this._glFramebufferToCanvasTransform = null;
this._outputCanvas = null;
this._outputContext = null;
this._clippingCanvas = null;
@ -92,11 +88,7 @@
this._renderingCanvas = null;
this._backupCanvasDrawer = null;
// 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);
this._imageSmoothingEnabled = true; // will be updated by setImageSmoothingEnabled
// 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");
@ -108,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
/**
@ -135,11 +139,6 @@
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
let canvases = Array.from(this._TextureMap.keys());
canvases.forEach(canvas => {
this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap
});
// Delete all our created resources
gl.deleteBuffer(this._secondPass.bufferOutputPosition);
gl.deleteFramebuffer(this._glFrameBuffer);
@ -157,10 +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);
// 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;
@ -254,9 +253,10 @@
*/
draw(tiledImages){
let gl = this._gl;
const bounds = this.viewport.getBoundsNoRotateWithMargins(true);
let view = {
bounds: this.viewport.getBoundsNoRotate(true),
center: this.viewport.getCenter(true),
bounds: bounds,
center: new OpenSeadragon.Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2),
rotation: this.viewport.getRotation(true) * Math.PI / 180
};
@ -313,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){
@ -378,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
@ -448,7 +439,7 @@
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition);
gl.vertexAttribPointer(this._secondPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition);
gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(this._secondPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
// Draw the quad (two triangles)
gl.drawArrays(gl.TRIANGLES, 0, 6);
@ -472,8 +463,6 @@
}
}
});
if(renderingBufferHasImageData){
@ -484,11 +473,15 @@
// Public API required by all Drawer implementations
/**
* Required by DrawerBase, but has no effect on WebGLDrawer.
* @param {Boolean} enabled
* Sets whether image smoothing is enabled or disabled
* @param {Boolean} enabled If true, uses gl.LINEAR as the TEXTURE_MIN_FILTER and TEXTURE_MAX_FILTER, otherwise gl.NEAREST.
*/
setImageSmoothingEnabled(enabled){
// noop - this property does not impact WebGLDrawer
if( this._imageSmoothingEnabled !== enabled ){
this._imageSmoothingEnabled = enabled;
this.setInternalCacheNeedsRefresh();
this.viewer.forceRedraw();
}
}
/**
@ -598,6 +591,11 @@
}
// private
_textureFilter(){
return this._imageSmoothingEnabled ? this._gl.LINEAR : this._gl.NEAREST;
}
// private
_setupRenderer(){
let gl = this._gl;
@ -614,7 +612,7 @@
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this._renderingCanvas.width, this._renderingCanvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter());
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);
@ -813,14 +811,13 @@
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter());
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);
//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
@ -839,8 +836,7 @@
this._gl = this._renderingCanvas.getContext('webgl');
//make the additional canvas elements mirror size changes to the output canvas
this.viewer.addHandler("resize", function(){
this._resizeHandler = function(){
if(_this._outputCanvas !== _this.viewer.drawer.canvas){
_this._outputCanvas.style.width = _this.viewer.drawer.canvas.clientWidth + 'px';
@ -861,7 +857,102 @@
// important - update the size of the rendering viewport!
_this._resizeRenderer();
});
};
//make the additional canvas elements mirror size changes to the output canvas
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
@ -875,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, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Upload the image into the texture.
this._uploadImageData(tileContext);
}
}
// private
_calculateOverlapFraction(tile, tiledImage){
let overlap = tiledImage.source.tileOverlap;
@ -977,43 +982,13 @@
}
// private
_uploadImageData(tileContext){
// _unloadTextures(){
// let canvases = Array.from(this._TextureMap.keys());
// canvases.forEach(canvas => {
// this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap
// });
// }
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.
@ -1320,9 +1295,7 @@
return shaderProgram;
}
};
}( OpenSeadragon ));

View file

@ -69,6 +69,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
/**
* Add the specified item.
* @param {OpenSeadragon.TiledImage} item - The item to add.
* @param {Object} options - Options affecting insertion.
* @param {Number} [options.index] - Index for the item. If not specified, goes at the top.
* @fires OpenSeadragon.World.event:add-item
* @fires OpenSeadragon.World.event:metrics-change
@ -231,6 +232,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;
});
}
},
/**

View file

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

300
style.css Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,13 @@ const drawers = {
webgl: "New WebGL drawer"
}
const viewportMargins = {
left: 100,
top: 0,
right: 0,
bottom: 50,
};
//Support drawer type from the url
const url = new URL(window.location.href);
const drawer1 = url.searchParams.get("left") || 'canvas';
@ -40,6 +47,7 @@ let viewer1 = window.viewer1 = OpenSeadragon({
drawer:drawer1,
blendTime:0,
showNavigator:true,
viewportMargins,
});
// viewer2: webgl drawer
@ -55,6 +63,7 @@ let viewer2 = window.viewer2 = OpenSeadragon({
drawer:drawer2,
blendTime:0,
showNavigator:true,
viewportMargins,
});
// // viewer3: html drawer, unused
@ -184,6 +193,11 @@ function updateTiledImage(tiledImage, data, value, item){
tiledImage.setOpacity(Number(value));
} else if (field == 'flipped'){
tiledImage.setFlip($(item).prop('checked'));
} else if (field == 'smoothing'){
const checked = $(item).prop('checked');
viewer1.drawer.setImageSmoothingEnabled(checked);
viewer2.drawer.setImageSmoothingEnabled(checked);
$('[data-field=smoothing]').prop('checked', checked);
} else if (field == 'cropped'){
if( $(item).prop('checked') ){
let scale = tiledImage.source.width;
@ -346,6 +360,7 @@ function makeImagePickerElement(key, label){
<label>Debug: <input type="checkbox" data-image="" data-field="debug"></label>
<label>Composite: <select data-image="" data-field="composite"></select></label>
<label>Wrap: <select data-image="" data-field="wrapping"></select></label>
<label>Smoothing: <input type="checkbox" data-image="" data-field="smoothing" checked></label>
</div>
</div>`.replaceAll('data-image=""', `data-image="${key}"`).replace('__title__', label));

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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);
};
} )();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,10 +33,14 @@
}
}
function runTest(e) {
function runTest(e, async=false) {
context.raiseEvent(eName, e);
}
function runTestAwaiting(e, async=false) {
context.raiseEventAwaiting(eName, e);
}
QUnit.module( 'EventSource', {
beforeEach: function () {
context = new OpenSeadragon.EventSource();
@ -82,4 +86,58 @@
message: 'Prioritized callback order should follow [2,1,4,5,3].'
});
});
QUnit.test('EventSource: async non-synchronized order', function(assert) {
context.addHandler(eName, executor(1, 5));
context.addHandler(eName, executor(2, 50));
context.addHandler(eName, executor(3));
context.addHandler(eName, executor(4));
runTest({
assert: assert,
done: assert.async(),
expected: [3, 4, 1, 2],
message: 'Async callback order should follow [3,4,1,2].'
});
});
QUnit.test('EventSource: async non-synchronized priority order', function(assert) {
context.addHandler(eName, executor(1, 5));
context.addHandler(eName, executor(2, 50), undefined, -100);
context.addHandler(eName, executor(3), undefined, -500);
context.addHandler(eName, executor(4), undefined, 675);
runTest({
assert: assert,
done: assert.async(),
expected: [4, 3, 1, 2],
message: 'Async callback order with priority should follow [4,3,1,2]. Async functions do not respect priority.'
});
});
QUnit.test('EventSource: async synchronized order', function(assert) {
context.addHandler(eName, executor(1, 5));
context.addHandler(eName, executor(2, 50));
context.addHandler(eName, executor(3));
context.addHandler(eName, executor(4));
runTestAwaiting({
waitForPromiseHandlers: true,
assert: assert,
done: assert.async(),
expected: [1, 2, 3, 4],
message: 'Async callback order should follow [1,2,3,4], since it is synchronized.'
});
});
QUnit.test('EventSource: async synchronized priority order', function(assert) {
context.addHandler(eName, executor(1, 5));
context.addHandler(eName, executor(2), undefined, -500);
context.addHandler(eName, executor(3, 50), undefined, -200);
context.addHandler(eName, executor(4), undefined, 675);
runTestAwaiting({
waitForPromiseHandlers: true,
assert: assert,
done: assert.async(),
expected: [4, 1, 3, 2],
message: 'Async callback order with priority should follow [4,1,3,2], since priority is respected when synchronized.'
});
});
} )();

View file

@ -2,6 +2,7 @@
(function () {
var viewer;
var sleep = time => new Promise(res => setTimeout(res, time));
QUnit.module( 'Events', {
beforeEach: function () {
@ -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();

View file

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

View file

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

View file

@ -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'
);

View file

@ -254,10 +254,10 @@
var expPosition = viewport.imageToViewerElementCoordinates(
new OpenSeadragon.Point(13, 120));
var actPosition = $("#overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("overlay").position, true);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Y position mismatch " + contextMessage);
var zoom = viewport.viewportToImageZoom(viewport.getZoom(true));
@ -271,10 +271,10 @@
expPosition = viewport.imageToViewerElementCoordinates(
new OpenSeadragon.Point(400, 500));
actPosition = $("#fixed-overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
actPosition = viewport.pixelFromPoint(viewer.getOverlayById("fixed-overlay").position, true);;
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"Fixed overlay X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Fixed overlay Y position mismatch " + contextMessage);
Util.assessNumericValue(assert, $("#fixed-overlay").width(), 70, epsilon,
@ -326,10 +326,10 @@
var expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.2, 0.1));
var actPosition = $("#overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("overlay").position, true);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Y position mismatch " + contextMessage);
var expectedSize = viewport.deltaPixelsFromPoints(
@ -342,10 +342,10 @@
expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.5, 0.6));
actPosition = $("#fixed-overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
actPosition = viewport.pixelFromPoint(viewer.getOverlayById("fixed-overlay").position, true);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"Fixed overlay X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Fixed overlay Y position mismatch " + contextMessage);
Util.assessNumericValue(assert, $("#fixed-overlay").width(), 70, epsilon,
@ -402,10 +402,10 @@
var expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.2, 0.1));
var actPosition = $("#overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("overlay").position, true);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Y position mismatch " + contextMessage);
}
@ -415,10 +415,11 @@
var expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.5, 0.6))
.plus(expectedOffset);
var actPosition = $("#fixed-overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("fixed-overlay").position, true).plus(expectedOffset);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"Fixed overlay X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Fixed overlay Y position mismatch " + contextMessage);
}
@ -481,10 +482,10 @@
var expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.5, 0.6))
.plus(expectedOffset);
var actPosition = $("#fixed-overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("fixed-overlay").position, true).plus(expectedOffset);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"Fixed overlay X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Fixed overlay Y position mismatch " + contextMessage);
}
@ -536,10 +537,10 @@
var expPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.5, 0.6))
.plus(expectedOffset);
var actPosition = $("#fixed-overlay").position();
Util.assessNumericValue(assert, actPosition.left, expPosition.x, epsilon,
var actPosition = viewport.pixelFromPoint(viewer.getOverlayById("fixed-overlay").position, true).plus(expectedOffset);
Util.assessNumericValue(assert, actPosition.x, expPosition.x, epsilon,
"Fixed overlay X position mismatch " + contextMessage);
Util.assessNumericValue(assert, actPosition.top, expPosition.y, epsilon,
Util.assessNumericValue(assert, actPosition.y, expPosition.y, epsilon,
"Fixed overlay Y position mismatch " + contextMessage);
}
@ -789,10 +790,10 @@
var expectedPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(1, 1))
.minus(expectedSize);
var actualPosition = $overlay.position();
Util.assessNumericValue(assert, actualPosition.left, expectedPosition.x, epsilon,
var actualPosition = viewport.pixelFromPoint(viewer.getOverlayById("fully-scaled-overlay").position, true).minus(expectedSize);
Util.assessNumericValue(assert, actualPosition.x, expectedPosition.x, epsilon,
"Scaled overlay position.x should adjust to rotation.");
Util.assessNumericValue(assert, actualPosition.top, expectedPosition.y, epsilon,
Util.assessNumericValue(assert, actualPosition.y, expectedPosition.y, epsilon,
"Scaled overlay position.y should adjust to rotation.");
var actualWidth = $overlay.width();
@ -851,10 +852,10 @@
var expectedPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0.5, 0.5))
.minus(new OpenSeadragon.Point(expectedWidth / 2, notScaledHeight / 2));
var actualPosition = $overlay.position();
Util.assessNumericValue(assert, actualPosition.left, expectedPosition.x, epsilon,
var actualPosition = viewport.pixelFromPoint(viewer.getOverlayById("horizontally-scaled-overlay").position, true).minus(new OpenSeadragon.Point(expectedWidth / 2, notScaledHeight / 2));
Util.assessNumericValue(assert, actualPosition.x, expectedPosition.x, epsilon,
"Horizontally scaled overlay position.x should adjust to rotation.");
Util.assessNumericValue(assert, actualPosition.top, expectedPosition.y, epsilon,
Util.assessNumericValue(assert, actualPosition.y, expectedPosition.y, epsilon,
"Horizontally scaled overlay position.y should adjust to rotation.");
var actualWidth = $overlay.width();
@ -914,10 +915,10 @@
var expectedPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(0, 0.5))
.minus(new OpenSeadragon.Point(0, expectedHeight / 2));
var actualPosition = $overlay.position();
Util.assessNumericValue(assert, actualPosition.left, expectedPosition.x, epsilon,
var actualPosition= viewport.pixelFromPoint(viewer.getOverlayById("vertically-scaled-overlay").position).minus(new OpenSeadragon.Point(0, expectedHeight / 2));
Util.assessNumericValue(assert, actualPosition.x, expectedPosition.x, epsilon,
"Vertically scaled overlay position.x should adjust to rotation.");
Util.assessNumericValue(assert, actualPosition.top, expectedPosition.y, epsilon,
Util.assessNumericValue(assert, actualPosition.y, expectedPosition.y, epsilon,
"Vertically scaled overlay position.y should adjust to rotation.");
var actualWidth = $overlay.width();
@ -975,10 +976,10 @@
var expectedPosition = viewport.viewportToViewerElementCoordinates(
new OpenSeadragon.Point(1, 0))
.minus(new OpenSeadragon.Point(notScaledWidth, 0));
var actualPosition = $overlay.position();
Util.assessNumericValue(assert, actualPosition.left, expectedPosition.x, epsilon,
var actualPosition= viewport.pixelFromPoint(viewer.getOverlayById("not-scaled-overlay").position).minus(new OpenSeadragon.Point(notScaledWidth, 0));
Util.assessNumericValue(assert, actualPosition.x, expectedPosition.x, epsilon,
"Not scaled overlay position.x should adjust to rotation.");
Util.assessNumericValue(assert, actualPosition.top, expectedPosition.y, epsilon,
Util.assessNumericValue(assert, actualPosition.y, expectedPosition.y, epsilon,
"Not scaled overlay position.y should adjust to rotation.");
var actualWidth = $overlay.width();
@ -1060,6 +1061,28 @@
});
});
// ----------
QUnit.test('Remove Child from Overlay', function(assert){
var done = assert.async();
viewer = OpenSeadragon({
id: 'example-overlays',
prefixUrl: '/build/openseadragon/images/',
tileSources: '/test/data/testpattern.dzi'
});
viewer.addOnceHandler('open', function() {
var $overlay = document.createElement("div");
var something = document.createElement("p");
$overlay.appendChild(something);
viewer.addOverlay($overlay);
$overlay.removeChild(something);
something.innerHTML = '...';
assert.equal(something.innerHTML, '...');
done();
});
});
// ----------
QUnit.test('Fully scaled overlay rotation mode EXACT', function(assert) {
var done = assert.async();
@ -1082,7 +1105,6 @@
viewer.addOnceHandler('open', function() {
var viewport = viewer.viewport;
var $overlay = $("#fully-scaled-overlay");
var expectedSize = viewport.deltaPixelsFromPointsNoRotate(
new OpenSeadragon.Point(1, 1));
@ -1090,7 +1112,7 @@
new OpenSeadragon.Point(1, 1))
.minus(expectedSize);
// We can't rely on jQuery.position with transforms.
var actualStyle = $overlay.get(0).style;
var actualStyle= viewer.getOverlayById("fully-scaled-overlay").style;
var left = Number(actualStyle.left.replace("px", ""));
var top = Number(actualStyle.top.replace("px", ""));
Util.assessNumericValue(assert, left, expectedPosition.x, epsilon,

View file

@ -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) {

View file

@ -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;

View file

@ -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');
});
})();

View file

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

View file

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

View file

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

View file

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