diff --git a/.eslintrc.json b/.eslintrc.json index 7e027656..35c702a5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,10 +8,6 @@ "off", 4 ], - "linebreak-style": [ - "error", - "unix" - ], "quotes": [ "off", "double" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cd8fa4d..d8fd4d64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,3 +75,12 @@ You can also get a report of the tests' code coverage: The report shows up at `coverage/html/index.html` viewable in a browser. +### Installing from forked Github repo/branch + +This project is now compatible with direct installation of forked Github repos/branches via npm/yarn (possible because of the new [prepare](https://docs.npmjs.com/misc/scripts) command). This enables quick testing of a bugfix or feature addition via a forked repo. In order to do this: + +1. Install the Grunt command line runner (if you haven't already); on the command line, run `npm install -g grunt-cli` (or `yarn global add grunt-cli`) +1. Remove any currently installed openseadragon package via `npm uninstall openseadragon` or `yarn remove openseadragon` +1. Add the specific forked repo/branch by running `npm install git://github.com/username/openseadragon.git#branch-name` or `yarn add git://github.com/username/openseadragon.git#branch-name`. Make sure to replace username and branch-name with proper targets. + +During installation, the package should be correctly built via grunt and can then be used via `import Openseadragon from 'openseadragon'` or `var Openseadragon = require('openseadragon')` statements as if the official package were installed. diff --git a/Gruntfile.js b/Gruntfile.js index c2e821ad..77d48789 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -194,8 +194,12 @@ module.exports = function(grunt) { target: sources }, "git-describe": { + "options": { + failOnError: false + }, build: {} - } + }, + gitInfo: "unknown" }); // ---------- diff --git a/changelog.txt b/changelog.txt index d01f4f03..980d78a1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,32 +1,65 @@ OPENSEADRAGON CHANGELOG ======================= -2.2.2: (in progress) +2.3.1: (In Progress) + +2.3.0: * BREAKING CHANGE: Tile.distance has been removed (#1027) +* BREAKING CHANGE: Viewer's canvas-click event is now fired before it initiates the zoom (#1148) +* BREAKING CHANGE: Viewer's canvas-drag event is now fired before it pans (#1149) +* Added Zoomify tile source (#863) * You can now set the rotation of individual tiled images (#1006) -* Fixed CORS bug in IE 10 (#967) +* Added getFullyLoaded method and "fully-loaded-change" event to TiledImage to know when tiles are fully loaded (#837, #1073) +* You can now preload images without drawing them to the screen (#1071) * Added support for commonjs (#984) * Added an option to addTiledImage to change the crossOriginPolicy (#981) -* Fixed issue with tiles not appearing with wrapHorizontal/wrapVertical if you pan too far away from the origin (#987, #1066) -* The Viewer's tileSources option is now smarter about detecting JSON vs XML vs URL (#999) +* You can now load tiles via AJAX and custom AJAX request headers (#1055) +* Added ability to provide thumbnail URLs for reference strip (#1241) +* Improved panning constraints for constrainDuringPan (#1133 and #1245) +* You can now prevent canvas-click events from zooming on a per-event basis (#1148) +* You can now prevent canvas-drag events from panning on a per-event basis (#1149) * The navigationControlAnchor option now works for custom toolbar as well (#1004) -* Added getFullyLoaded method and "fully-loaded-change" event to TiledImage to know when tiles are fully loaded (#837, #1073) -* Fixed: Initial tile load wasn't happening in parallel (#1014) -* Added Zoomify tile source (#863) -* Fixed problem with "sparse image" DZI files (#995) -* Optimization: Use the squared distance when comparing tiles (#1027) -* Fix IndexSizeError on IE and Edge that occurred under certain circumstances (e.g. multi-image with transparency) (#1035) -* ImageTileSource now works in IE8 (#1041) * LegacyTileSource now allows any image URLs regardless of type (#1056) -* Fixed error in IE8 when zooming in (due to edge smoothing) (#1064) +* Enabled configuration of ImageLoader timeout (#1192) +* Viewer.open() now supports an initialPage argument for sequenceMode (#1196) +* New events for opacity and compositeOperation changes (#1203) +* Added support for setting debug mode after the Viewer object has been constructed (#1224) +* Added functions for dynamically adding and removing the reference strip in sequence mode (#1213) +* Better calculation for TileCache release cutoff (#1214) +* The navigator now picks up opacity and compositeOperation changes (#1203) +* Improved calculation for determining which level to load first (#1198) +* Added fix for supporting weird filenames that look like JSONs (#1189) * Improved DziTileSource guessing of tilesUrl (#1074) +* The Viewer's tileSources option is now smarter about detecting JSON vs XML vs URL (#999) +* Better compression for our UI images (#1134) +* Optimization: Use the squared distance when comparing tiles (#1027) +* Now clamping pixel ratio density to a minimum of 1, fixing display issues on low density devices (#1200) +* More forgiving check for DZI schema (#1249) +* ImageTileSource now works in IE8 (#1041) +* Fixed CORS bug in IE 10 (#967) +* Fixed issue with tiles not appearing with wrapHorizontal/wrapVertical if you pan too far away from the origin (#987, #1066) +* Fixed: Initial tile load wasn't happening in parallel (#1014) +* Fixed problem with "sparse image" DZI files (#995) +* Fix IndexSizeError on IE and Edge that occurred under certain circumstances (e.g. multi-image with transparency) (#1035) +* Fixed error in IE8 when zooming in (due to edge smoothing) (#1064) * Fixed issue with OpenSeadragon.version in the minified JavaScript (#1099) * Fixed smoothTileEdgesMinZoom performance degradation on single-tile images (#1101) * Fixed issue with tiles not appearing after rotation (#1102) * Fixed: The navigator wasn't respecting the constrainDuringPan setting (#1104) * Fixed an issue causing overlays to be mis-positioned in some circumstances (#1119) * Fixed: ImageTileSource would sometimes produce a double image (#1123) +* Fixed: console.debug caused exceptions on IE10 (#1129) +* Fixed: the reference strip would leak memory when opening new sets of images (#1175) +* Fixed: zoomTo/zoomBy ignore refPoint if immediately is true (#1184) +* Fixed: IIPImageServer didn't work with the latest OSD release (#1199) +* Fixed: setItemIndex method not working with navigator inside "open" event (#1201) +* Fixed: The reference strip didn't show the initial page if it wasn't the first page (#1208) +* Fixed: Sometimes the image would stick to the mouse when right-clicking and left-clicking simultaneously (#1223) +* Fixed issue with transparent images sometimes disappearing on Safari (#1222) +* Fixed: One image failing to load could cause the others to never load (#1229) +* Fixed: Mouse up outside map will cause "canvas-drag" event to stick (#1133) +* Fixed more issues with tracking multiple pointers (#1244) 2.2.1: diff --git a/images/button_grouphover.png b/images/button_grouphover.png new file mode 100755 index 00000000..9db590ea Binary files /dev/null and b/images/button_grouphover.png differ diff --git a/images/button_hover.png b/images/button_hover.png new file mode 100755 index 00000000..645c241b Binary files /dev/null and b/images/button_hover.png differ diff --git a/images/button_pressed.png b/images/button_pressed.png new file mode 100755 index 00000000..d5b1d7d0 Binary files /dev/null and b/images/button_pressed.png differ diff --git a/images/button_rest.png b/images/button_rest.png new file mode 100755 index 00000000..e232387d Binary files /dev/null and b/images/button_rest.png differ diff --git a/images/fullpage_grouphover.png b/images/fullpage_grouphover.png index 3ca4e1e3..da9002b1 100644 Binary files a/images/fullpage_grouphover.png and b/images/fullpage_grouphover.png differ diff --git a/images/fullpage_hover.png b/images/fullpage_hover.png index d08eb2d0..705b3d3e 100644 Binary files a/images/fullpage_hover.png and b/images/fullpage_hover.png differ diff --git a/images/fullpage_pressed.png b/images/fullpage_pressed.png index 0ee45b5f..6fa182ae 100644 Binary files a/images/fullpage_pressed.png and b/images/fullpage_pressed.png differ diff --git a/images/fullpage_rest.png b/images/fullpage_rest.png index 3172005c..bfab6433 100644 Binary files a/images/fullpage_rest.png and b/images/fullpage_rest.png differ diff --git a/images/home_grouphover.png b/images/home_grouphover.png index 204e1cc9..cb412ba4 100644 Binary files a/images/home_grouphover.png and b/images/home_grouphover.png differ diff --git a/images/home_hover.png b/images/home_hover.png index ec218a00..c8f860ba 100644 Binary files a/images/home_hover.png and b/images/home_hover.png differ diff --git a/images/home_pressed.png b/images/home_pressed.png index 4439508b..00c349b0 100644 Binary files a/images/home_pressed.png and b/images/home_pressed.png differ diff --git a/images/home_rest.png b/images/home_rest.png index 009d1bbf..6ac397da 100644 Binary files a/images/home_rest.png and b/images/home_rest.png differ diff --git a/images/next_grouphover.png b/images/next_grouphover.png index 8d83d8a1..18c1a925 100644 Binary files a/images/next_grouphover.png and b/images/next_grouphover.png differ diff --git a/images/next_hover.png b/images/next_hover.png index ba24ca98..fdce5820 100644 Binary files a/images/next_hover.png and b/images/next_hover.png differ diff --git a/images/next_pressed.png b/images/next_pressed.png index 95f169d6..5297c526 100644 Binary files a/images/next_pressed.png and b/images/next_pressed.png differ diff --git a/images/next_rest.png b/images/next_rest.png index 5ead544b..e3c5a3ca 100644 Binary files a/images/next_rest.png and b/images/next_rest.png differ diff --git a/images/previous_grouphover.png b/images/previous_grouphover.png index 016e6395..5e0fda1b 100644 Binary files a/images/previous_grouphover.png and b/images/previous_grouphover.png differ diff --git a/images/previous_hover.png b/images/previous_hover.png index d4a5c155..9f9efe61 100644 Binary files a/images/previous_hover.png and b/images/previous_hover.png differ diff --git a/images/previous_pressed.png b/images/previous_pressed.png index f999fe49..75c7e7d2 100644 Binary files a/images/previous_pressed.png and b/images/previous_pressed.png differ diff --git a/images/previous_rest.png b/images/previous_rest.png index 9716dac6..902a7b45 100644 Binary files a/images/previous_rest.png and b/images/previous_rest.png differ diff --git a/images/rotateleft_grouphover.png b/images/rotateleft_grouphover.png index 9aec7ac9..302ac628 100644 Binary files a/images/rotateleft_grouphover.png and b/images/rotateleft_grouphover.png differ diff --git a/images/rotateleft_hover.png b/images/rotateleft_hover.png index ba32c5a4..e757d87a 100644 Binary files a/images/rotateleft_hover.png and b/images/rotateleft_hover.png differ diff --git a/images/rotateleft_pressed.png b/images/rotateleft_pressed.png index b968ebf8..1480b1ae 100644 Binary files a/images/rotateleft_pressed.png and b/images/rotateleft_pressed.png differ diff --git a/images/rotateleft_rest.png b/images/rotateleft_rest.png index ebbf081b..f0b86541 100644 Binary files a/images/rotateleft_rest.png and b/images/rotateleft_rest.png differ diff --git a/images/rotateright_grouphover.png b/images/rotateright_grouphover.png index 86e8689c..9e713712 100644 Binary files a/images/rotateright_grouphover.png and b/images/rotateright_grouphover.png differ diff --git a/images/rotateright_hover.png b/images/rotateright_hover.png index d22a728f..08f14160 100644 Binary files a/images/rotateright_hover.png and b/images/rotateright_hover.png differ diff --git a/images/rotateright_pressed.png b/images/rotateright_pressed.png index fc2ead64..351f8243 100644 Binary files a/images/rotateright_pressed.png and b/images/rotateright_pressed.png differ diff --git a/images/rotateright_rest.png b/images/rotateright_rest.png index 07219678..d70468ac 100644 Binary files a/images/rotateright_rest.png and b/images/rotateright_rest.png differ diff --git a/images/zoomin_grouphover.png b/images/zoomin_grouphover.png index c985d0f9..98ecd291 100644 Binary files a/images/zoomin_grouphover.png and b/images/zoomin_grouphover.png differ diff --git a/images/zoomin_hover.png b/images/zoomin_hover.png index 3cab721f..c25bda42 100644 Binary files a/images/zoomin_hover.png and b/images/zoomin_hover.png differ diff --git a/images/zoomin_pressed.png b/images/zoomin_pressed.png index 9c3a7516..e617e034 100644 Binary files a/images/zoomin_pressed.png and b/images/zoomin_pressed.png differ diff --git a/images/zoomin_rest.png b/images/zoomin_rest.png index f4219a50..4380589e 100644 Binary files a/images/zoomin_rest.png and b/images/zoomin_rest.png differ diff --git a/images/zoomout_grouphover.png b/images/zoomout_grouphover.png index 46d21b3e..b588ecf7 100644 Binary files a/images/zoomout_grouphover.png and b/images/zoomout_grouphover.png differ diff --git a/images/zoomout_hover.png b/images/zoomout_hover.png index 7b924c26..a132cb45 100644 Binary files a/images/zoomout_hover.png and b/images/zoomout_hover.png differ diff --git a/images/zoomout_pressed.png b/images/zoomout_pressed.png index c028db72..679c5cda 100644 Binary files a/images/zoomout_pressed.png and b/images/zoomout_pressed.png differ diff --git a/images/zoomout_rest.png b/images/zoomout_rest.png index a13e07de..e3ac4abd 100644 Binary files a/images/zoomout_rest.png and b/images/zoomout_rest.png differ diff --git a/package.json b/package.json index 2fd0cad7..231c5158 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openseadragon", - "version": "2.2.1", + "version": "2.3.0", "description": "Provides a smooth, zoomable user interface for HTML/Javascript.", "keywords": ["image", "zoom", "pan", "openseadragon", "seadragon", "deepzoom", "dzi", "iiif", "osm", "tms"], "homepage": "http://openseadragon.github.io/", @@ -17,7 +17,7 @@ "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-clean": "^0.7.0", - "grunt-contrib-compress": "^0.13.0", + "grunt-contrib-compress": "^1.4.3", "grunt-contrib-concat": "^1.0.1", "grunt-contrib-connect": "^0.11.2", "grunt-contrib-uglify": "^2.0.0", @@ -30,6 +30,6 @@ }, "scripts": { "test": "grunt test", - "prepublish": "grunt build" + "prepare": "grunt build" } } diff --git a/psd/button.psd b/psd/button.psd new file mode 100755 index 00000000..d1574b81 Binary files /dev/null and b/psd/button.psd differ diff --git a/src/drawer.js b/src/drawer.js index 4485da98..eed97719 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -426,22 +426,22 @@ $.Drawer.prototype = { this.context.globalCompositeOperation = compositeOperation; } if (bounds) { - // Internet Explorer and Microsoft Edge throw IndexSizeError + // Internet Explorer, Microsoft Edge, and Safari have problems // when you call context.drawImage with negative x or y - // or width or height greater than the canvas width or height respectively + // or x + width or y + height greater than the canvas width or height respectively. if (bounds.x < 0) { bounds.width += bounds.x; bounds.x = 0; } - if (bounds.width > this.canvas.width) { - bounds.width = this.canvas.width; + if (bounds.x + bounds.width > this.canvas.width) { + bounds.width = this.canvas.width - bounds.x; } if (bounds.y < 0) { bounds.height += bounds.y; bounds.y = 0; } - if (bounds.height > this.canvas.height) { - bounds.height = this.canvas.height; + if (bounds.y + bounds.height > this.canvas.height) { + bounds.height = this.canvas.height - bounds.y; } this.context.drawImage( diff --git a/src/dzitilesource.js b/src/dzitilesource.js index 54b8cb68..ec5ea9a9 100644 --- a/src/dzitilesource.js +++ b/src/dzitilesource.js @@ -113,8 +113,10 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead } } - return ( "http://schemas.microsoft.com/deepzoom/2008" == ns || - "http://schemas.microsoft.com/deepzoom/2009" == ns ); + ns = (ns || '').toLowerCase(); + + return (ns.indexOf('schemas.microsoft.com/deepzoom/2008') !== -1 || + ns.indexOf('schemas.microsoft.com/deepzoom/2009') !== -1); }, /** @@ -140,7 +142,7 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead if (url && !options.tilesUrl) { options.tilesUrl = url.replace( - /([^\/]+?)(\.(dzi|xml|js))?(\?[^\/]*)?\/?$/, '$1_files/'); + /([^\/]+?)(\.(dzi|xml|js)?(\?[^\/]*)?)?\/?$/, '$1_files/'); if (url.search(/\.(dzi|xml|js)\?/) != -1) { options.queryParams = url.match(/\?.*/); diff --git a/src/imageloader.js b/src/imageloader.js index cd654722..14eb576b 100644 --- a/src/imageloader.js +++ b/src/imageloader.js @@ -32,15 +32,27 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(function( $ ){ +(function($){ -// private class -function ImageJob ( options ) { +/** + * @private + * @class ImageJob + * @classdesc Handles downloading of a single image. + * @param {Object} options - Options for this ImageJob. + * @param {String} [options.src] - URL of image to download. + * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. + * @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX. + * @param {String} [options.crossOriginPolicy] - CORS policy to use for downloads + * @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. + */ +function ImageJob (options) { - $.extend( true, this, { - timeout: $.DEFAULT_SETTINGS.timeout, - jobId: null - }, options ); + $.extend(true, this, { + timeout: $.DEFAULT_SETTINGS.timeout, + jobId: null + }, options); /** * Image object which will contain downloaded image. @@ -52,42 +64,103 @@ function ImageJob ( options ) { ImageJob.prototype = { errorMsg: null, + + /** + * Starts the image job. + * @method + */ start: function(){ - var _this = this; + var self = this; + var selfAbort = this.abort; this.image = new Image(); - if ( this.crossOriginPolicy !== false ) { - this.image.crossOrigin = this.crossOriginPolicy; - } - this.image.onload = function(){ - _this.finish( true ); + self.finish(true); }; - this.image.onabort = this.image.onerror = function(){ - _this.errorMsg = "Image load aborted"; - _this.finish( false ); + this.image.onabort = this.image.onerror = function() { + self.errorMsg = "Image load aborted"; + self.finish(false); }; - this.jobId = window.setTimeout( function(){ - _this.errorMsg = "Image load exceeded timeout"; - _this.finish( false ); + this.jobId = window.setTimeout(function(){ + self.errorMsg = "Image load exceeded timeout"; + self.finish(false); }, this.timeout); - this.image.src = this.src; + // Load the tile with an AJAX request if the loadWithAjax option is + // set. Otherwise load the image by setting the source proprety of the image object. + if (this.loadWithAjax) { + this.request = $.makeAjaxRequest({ + url: this.src, + withCredentials: this.ajaxWithCredentials, + headers: this.ajaxHeaders, + responseType: "arraybuffer", + success: function(request) { + var blb; + // Make the raw data into a blob. + // BlobBuilder fallback adapted from + // http://stackoverflow.com/questions/15293694/blob-constructor-browser-compatibility + try { + blb = new window.Blob([request.response]); + } catch (e) { + var BlobBuilder = ( + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder + ); + if (e.name === 'TypeError' && BlobBuilder) { + var 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) { + self.errorMsg = "Empty image response."; + self.finish(false); + } + // 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. + var url = (window.URL || window.webkitURL).createObjectURL(blb); + self.image.src = url; + }, + error: function(request) { + self.errorMsg = "Image load aborted - XHR error"; + self.finish(false); + } + }); + + // Provide a function to properly abort the request. + this.abort = function() { + self.request.abort(); + + // Call the existing abort function if available + if (typeof selfAbort === "function") { + selfAbort(); + } + }; + } else { + if (this.crossOriginPolicy !== false) { + this.image.crossOrigin = this.crossOriginPolicy; + } + + this.image.src = this.src; + } }, - finish: function( successful ) { + finish: function(successful) { this.image.onload = this.image.onerror = this.image.onabort = null; if (!successful) { this.image = null; } - if ( this.jobId ) { - window.clearTimeout( this.jobId ); + if (this.jobId) { + window.clearTimeout(this.jobId); } - this.callback( this ); + this.callback(this); } }; @@ -99,14 +172,16 @@ ImageJob.prototype = { * You generally won't have to interact with the ImageLoader directly. * @param {Object} options - Options for this ImageLoader. * @param {Number} [options.jobLimit] - The number of concurrent image requests. See imageLoaderLimit in {@link OpenSeadragon.Options} for details. + * @param {Number} [options.timeout] - The max number of milliseconds that an image job may take to complete. */ -$.ImageLoader = function( options ) { +$.ImageLoader = function(options) { - $.extend( true, this, { + $.extend(true, this, { jobLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, + timeout: $.DEFAULT_SETTINGS.timeout, jobQueue: [], jobsInProgress: 0 - }, options ); + }, options); }; @@ -116,22 +191,32 @@ $.ImageLoader.prototype = { /** * Add an unloaded image to the loader queue. * @method - * @param {String} src - URL of image to download. - * @param {String} crossOriginPolicy - CORS policy to use for downloads - * @param {Function} callback - Called once image has been downloaded. + * @param {Object} options - Options for this job. + * @param {String} [options.src] - URL of image to download. + * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. + * @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 {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. */ - addJob: function( options ) { + addJob: function(options) { var _this = this, - complete = function( job ) { - completeJob( _this, job, options.callback ); + complete = function(job) { + completeJob(_this, job, options.callback); }, jobOptions = { src: options.src, + loadWithAjax: options.loadWithAjax, + ajaxHeaders: options.loadWithAjax ? options.ajaxHeaders : null, crossOriginPolicy: options.crossOriginPolicy, + ajaxWithCredentials: options.ajaxWithCredentials, callback: complete, - abort: options.abort + abort: options.abort, + timeout: this.timeout }, - newJob = new ImageJob( jobOptions ); + newJob = new ImageJob(jobOptions); if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) { newJob.start(); @@ -166,18 +251,18 @@ $.ImageLoader.prototype = { * @param job - The ImageJob that has completed. * @param callback - Called once cleanup is finished. */ -function completeJob( loader, job, callback ) { +function completeJob(loader, job, callback) { var nextJob; loader.jobsInProgress--; - if ( (!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) { + if ((!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) { nextJob = loader.jobQueue.shift(); nextJob.start(); loader.jobsInProgress++; } - callback( job.image, job.errorMsg ); + callback(job.image, job.errorMsg, job.request); } -}( OpenSeadragon )); +}(OpenSeadragon)); diff --git a/src/mousetracker.js b/src/mousetracker.js index 3dbcf595..8a342d18 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -317,6 +317,25 @@ return this; }, + /** + * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for all but the given pointer device type. + * @function + * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. + * @returns {Array.} + */ + getActivePointersListsExceptType: function ( type ) { + var delegate = THIS[ this.hash ]; + var listArray = []; + + for (var i = 0; i < delegate.activePointersLists.length; ++i) { + if (delegate.activePointersLists[i].type !== type) { + listArray.push(delegate.activePointersLists[i]); + } + } + + return listArray; + }, + /** * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for the given pointer device type, * creating and caching a new {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} if one doesn't already exist for the type. @@ -862,6 +881,21 @@ blurHandler: function () { } }; + /** + * Resets all active mousetrakers. (Added to patch issue #697 "Mouse up outside map will cause "canvas-drag" event to stick") + * + * @private + * @member resetAllMouseTrackers + * @memberof OpenSeadragon.MouseTracker + */ + $.MouseTracker.resetAllMouseTrackers = function(){ + for(var i = 0; i < MOUSETRACKERS.length; i++){ + if (MOUSETRACKERS[i].isTracking()){ + MOUSETRACKERS[i].setTracking(false); + MOUSETRACKERS[i].setTracking(true); + } + } + }; /** * Provides continuous computation of velocity (speed and direction) of active pointers. @@ -1201,6 +1235,32 @@ } } return null; + }, + + /** + * Increment this pointer's contact count. + * It will evaluate whether this pointer type is allowed to have multiple contacts. + * @function + */ + addContact: function() { + ++this.contacts; + + if (this.contacts > 1 && (this.type === "mouse" || this.type === "pen")) { + this.contacts = 1; + } + }, + + /** + * Decrement this pointer's contact count. + * It will make sure the count does not go below 0. + * @function + */ + removeContact: function() { + --this.contacts; + + if (this.contacts < 0) { + this.contacts = 0; + } } }; @@ -2005,23 +2065,26 @@ * @private * @inner */ - function abortTouchContacts( tracker, event, pointsList ) { + function abortContacts( tracker, event, pointsList ) { var i, gPointCount = pointsList.getLength(), abortGPoints = []; - for ( i = 0; i < gPointCount; i++ ) { - abortGPoints.push( pointsList.getByIndex( i ) ); - } + // Check contact count for hoverable pointer types before aborting + if (pointsList.type === 'touch' || pointsList.contacts > 0) { + for ( i = 0; i < gPointCount; i++ ) { + abortGPoints.push( pointsList.getByIndex( i ) ); + } - if ( abortGPoints.length > 0 ) { - // simulate touchend - updatePointersUp( tracker, event, abortGPoints, 0 ); // 0 means primary button press/release or touch contact - // release pointer capture - pointsList.captureCount = 1; - releasePointer( tracker, 'touch' ); - // simulate touchleave - updatePointersExit( tracker, event, abortGPoints ); + if ( abortGPoints.length > 0 ) { + // simulate touchend/mouseup + updatePointersUp( tracker, event, abortGPoints, 0 ); // 0 means primary button press/release or touch contact + // release pointer capture + pointsList.captureCount = 1; + releasePointer( tracker, pointsList.type ); + // simulate touchleave/mouseout + updatePointersExit( tracker, event, abortGPoints ); + } } } @@ -2043,7 +2106,7 @@ if ( pointsList.getLength() > event.touches.length - touchCount ) { $.console.warn('Tracked touch contact count doesn\'t match event.touches.length. Removing all tracked touch pointers.'); - abortTouchContacts( tracker, event, pointsList ); + abortContacts( tracker, event, pointsList ); } for ( i = 0; i < touchCount; i++ ) { @@ -2213,7 +2276,7 @@ function onTouchCancel( tracker, event ) { var pointsList = tracker.getActivePointersListByType('touch'); - abortTouchContacts( tracker, event, pointsList ); + abortContacts( tracker, event, pointsList ); } @@ -2690,6 +2753,14 @@ } } + // Some pointers may steal control from another pointer without firing the appropriate release events + // e.g. Touching a screen while click-dragging with certain mice. + var otherPointsLists = tracker.getActivePointersListsExceptType(gPoints[ 0 ].type); + for (i = 0; i < otherPointsLists.length; i++) { + //If another pointer has contact, simulate the release + abortContacts(tracker, event, otherPointsLists[i]); // No-op if no active pointer + } + // Only capture and track primary button, pen, and touch contacts if ( buttonChanged !== 0 ) { // Aux Press @@ -2740,7 +2811,7 @@ startTrackingPointer( pointsList, curGPoint ); } - pointsList.contacts++; + pointsList.addContact(); //$.console.log('contacts++ ', pointsList.contacts); if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { @@ -2879,6 +2950,11 @@ } } + // A primary mouse button may have been released while the non-primary button was down + var otherPointsList = tracker.getActivePointersListByType("mouse"); + // Stop tracking the mouse; see https://github.com/openseadragon/openseadragon/pull/1223 + abortContacts(tracker, event, otherPointsList); // No-op if no active pointer + return false; } @@ -2907,7 +2983,7 @@ if ( wasCaptured ) { // Pointer was activated in our element but could have been removed in any element since events are captured to our element - pointsList.contacts--; + pointsList.removeContact(); //$.console.log('contacts-- ', pointsList.contacts); if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { @@ -3267,10 +3343,12 @@ } } - // True if inside an iframe, otherwise false. - // @member {Boolean} isInIframe - // @private - // @inner + /** + * True if inside an iframe, otherwise false. + * @member {Boolean} isInIframe + * @private + * @inner + */ var isInIframe = (function() { try { return window.self !== window.top; @@ -3279,10 +3357,12 @@ } })(); - // @function - // @private - // @inner - // @returns {Boolean} True if the target has access rights to events, otherwise false. + /** + * @function + * @private + * @inner + * @returns {Boolean} True if the target has access rights to events, otherwise false. + */ function canAccessEvents (target) { try { return target.addEventListener && target.removeEventListener; diff --git a/src/navigator.js b/src/navigator.js index 45379fa1..4c9848cf 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -231,8 +231,10 @@ $.Navigator = function( options ){ }); viewer.world.addHandler("item-index-change", function(event) { - var item = _this.world.getItemAt(event.previousIndex); - _this.world.setItemIndex(item, event.newIndex); + window.setTimeout(function(){ + var item = _this.world.getItemAt(event.previousIndex); + _this.world.setItemIndex(item, event.newIndex); + }, 1); }); viewer.world.addHandler("remove-item", function(event) { @@ -345,8 +347,18 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* _this._matchBounds(myItem, original); } + function matchOpacity() { + _this._matchOpacity(myItem, original); + } + + function matchCompositeOperation() { + _this._matchCompositeOperation(myItem, original); + } + original.addHandler('bounds-change', matchBounds); original.addHandler('clip-change', matchBounds); + original.addHandler('opacity-change', matchOpacity); + original.addHandler('composite-operation-change', matchCompositeOperation); } }); @@ -374,6 +386,16 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* myItem.setWidth(bounds.width, immediately); myItem.setRotation(theirItem.getRotation(), immediately); myItem.setClip(theirItem.getClip()); + }, + + // private + _matchOpacity: function(myItem, theirItem) { + myItem.setOpacity(theirItem.opacity); + }, + + // private + _matchCompositeOperation: function(myItem, theirItem) { + myItem.setCompositeOperation(theirItem.compositeOperation); } }); diff --git a/src/openseadragon.js b/src/openseadragon.js index a470de14..ade8761d 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -185,7 +185,11 @@ * If 0, adjusts to fit viewer. * * @property {Number} [opacity=1] - * Default opacity of the tiled images (1=opaque, 0=transparent) + * Default proportional opacity of the tiled images (1=opaque, 0=hidden) + * Hidden images do not draw and only load when preloading is allowed. + * + * @property {Boolean} [preload=false] + * Default switch for loading hidden images (true loads, false blocks) * * @property {String} [compositeOperation=null] * Valid values are 'source-over', 'source-atop', 'source-in', 'source-out', @@ -411,6 +415,7 @@ * The max number of images we should keep in memory (per drawer). * * @property {Number} [timeout=30000] + * The max number of milliseconds that an image job may take to complete. * * @property {Boolean} [useCanvas=true] * Set to false to not use an HTML canvas element for image rendering even if canvas is supported. @@ -584,9 +589,16 @@ * not use CORS, and the canvas will be tainted. * * @property {Boolean} [ajaxWithCredentials=false] - * Whether to set the withCredentials XHR flag for AJAX requests (when loading tile sources). + * Whether to set the withCredentials XHR flag for AJAX requests. * Note that this can be overridden at the {@link OpenSeadragon.TileSource} level. * + * @property {Boolean} [loadTilesWithAjax=false] + * Whether to load tile data using AJAX requests. + * Note that this can be overridden at the {@link OpenSeadragon.TileSource} level. + * + * @property {Object} [ajaxHeaders={}] + * A set of headers to include when making AJAX requests for tile sources or tiles. + * */ /** @@ -678,13 +690,6 @@ */ - /** - * This function serves as a single point of instantiation for an {@link OpenSeadragon.Viewer}, including all - * combinations of out-of-the-box configurable features. - * - * @param {OpenSeadragon.Options} options - Viewer options. - * @returns {OpenSeadragon.Viewer} - */ function OpenSeadragon( options ){ return new OpenSeadragon.Viewer( options ); } @@ -867,7 +872,8 @@ function OpenSeadragon( options ){ }; /** - * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density. Defaults to 1 if canvas isn't supported by the browser. + * 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. * @member {Number} pixelDensityRatio * @memberof OpenSeadragon */ @@ -880,7 +886,7 @@ function OpenSeadragon( options ){ context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1; - return devicePixelRatio / backingStoreRatio; + return Math.max(devicePixelRatio, 1) / backingStoreRatio; } else { return 1; } @@ -1005,6 +1011,8 @@ function OpenSeadragon( options ){ initialPage: 0, crossOriginPolicy: false, ajaxWithCredentials: false, + loadTilesWithAjax: false, + ajaxHeaders: {}, //PAN AND ZOOM SETTINGS AND CONSTRAINTS panHorizontal: true, @@ -1117,6 +1125,7 @@ function OpenSeadragon( options ){ // APPEARANCE opacity: 1, + preload: false, compositeOperation: null, placeholderFillStyle: null, @@ -2120,11 +2129,16 @@ function OpenSeadragon( options ){ * @param {String} options.url - the url to request * @param {Function} options.success - a function to call on a successful response * @param {Function} options.error - a function to call on when an error occurs + * @param {Object} options.headers - headers to add to the AJAX request + * @param {String} options.responseType - the response type of the the AJAX request * @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials * @throws {Error} + * @returns {XMLHttpRequest} */ makeAjaxRequest: function( url, onSuccess, onError ) { var withCredentials; + var headers; + var responseType; // Note that our preferred API is that you pass in a single object; the named // arguments are for legacy support. @@ -2132,6 +2146,8 @@ function OpenSeadragon( options ){ onSuccess = url.success; onError = url.error; withCredentials = url.withCredentials; + headers = url.headers; + responseType = url.responseType || null; url = url.url; } @@ -2147,9 +2163,9 @@ function OpenSeadragon( options ){ if ( request.readyState == 4 ) { request.onreadystatechange = function(){}; - // With protocols other than http/https, the status is 200 - // on Firefox and 0 on other browsers - if ( request.status === 200 || + // With protocols other than http/https, a successful request status is in + // the 200's on Firefox and 0 on other browsers + if ( (request.status >= 200 && request.status < 300) || ( request.status === 0 && protocol !== "http:" && protocol !== "https:" )) { @@ -2167,11 +2183,23 @@ function OpenSeadragon( options ){ try { request.open( "GET", url, true ); + if (responseType) { + request.responseType = responseType; + } + + if (headers) { + for (var headerName in headers) { + if (headers.hasOwnProperty(headerName) && headers[headerName]) { + request.setRequestHeader(headerName, headers[headerName]); + } + } + } + if (withCredentials) { request.withCredentials = true; } - request.send( null ); + request.send(null); } catch (e) { var msg = e.message; @@ -2231,6 +2259,8 @@ function OpenSeadragon( options ){ } } } + + return request; }, /** diff --git a/src/referencestrip.js b/src/referencestrip.js index 5559f85a..e1074572 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -178,6 +178,7 @@ $.ReferenceStrip = function ( options ) { this.panelWidth = ( viewerSize.x * this.sizeRatio ) + 8; this.panelHeight = ( viewerSize.y * this.sizeRatio ) + 8; this.panels = []; + this.miniViewers = {}; /*jshint loopfunc:true*/ for ( i = 0; i < viewer.tileSources.length; i++ ) { @@ -293,6 +294,12 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp // Overrides Viewer.destroy destroy: function() { + if (this.miniViewers) { + for (var key in this.miniViewers) { + this.miniViewers[key].destroy(); + } + } + if (this.element) { this.element.parentNode.removeChild(this.element); } @@ -421,9 +428,19 @@ function loadPanels( strip, viewerSize, scroll ) { for ( i = activePanelsStart; i < activePanelsEnd && i < strip.panels.length; i++ ) { element = strip.panels[i]; if ( !element.activePanel ) { + var miniTileSource; + var originalTileSource = strip.viewer.tileSources[i]; + if (originalTileSource.referenceStripThumbnailUrl) { + miniTileSource = { + type: 'image', + url: originalTileSource.referenceStripThumbnailUrl + }; + } else { + miniTileSource = originalTileSource; + } miniViewer = new $.Viewer( { id: element.id, - tileSources: [strip.viewer.tileSources[i]], + tileSources: [miniTileSource], element: element, navigatorSizeRatio: strip.sizeRatio, showNavigator: false, @@ -463,6 +480,8 @@ function loadPanels( strip, viewerSize, scroll ) { miniViewer.displayRegion ); + strip.miniViewers[element.id] = miniViewer; + element.activePanel = true; } } diff --git a/src/tile.js b/src/tile.js index 72776aac..61ba2e64 100644 --- a/src/tile.js +++ b/src/tile.js @@ -47,8 +47,10 @@ * @param {String} url The URL of this tile's image. * @param {CanvasRenderingContext2D} context2D The context2D of this tile if it * is provided directly by the tile source. + * @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). */ -$.Tile = function(level, x, y, bounds, exists, url, context2D) { +$.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders) { /** * The zoom level this tile belongs to. * @member {Number} level @@ -91,6 +93,29 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D) { * @memberOf OpenSeadragon.Tile# */ this.context2D = context2D; + /** + * Whether to load this tile's image with an AJAX request. + * @member {Boolean} loadWithAjax + * @memberof OpenSeadragon.Tile# + */ + this.loadWithAjax = loadWithAjax; + /** + * The headers to be used in requesting this tile's image. + * Only used if loadWithAjax is set to true. + * @member {Object} ajaxHeaders + * @memberof OpenSeadragon.Tile# + */ + this.ajaxHeaders = ajaxHeaders; + /** + * The unique cache key for this tile. + * @member {String} cacheKey + * @memberof OpenSeadragon.Tile# + */ + if (this.ajaxHeaders) { + this.cacheKey = this.url + "+" + JSON.stringify(this.ajaxHeaders); + } else { + this.cacheKey = this.url; + } /** * Is this tile loaded? * @member {Boolean} loaded diff --git a/src/tilecache.js b/src/tilecache.js index ee3a4662..05d4e9cd 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -140,6 +140,7 @@ $.TileCache.prototype = { * may temporarily surpass that number, but should eventually come back down to the max specified. * @param {Object} options - Tile info. * @param {OpenSeadragon.Tile} options.tile - The tile to cache. + * @param {String} options.tile.cacheKey - The unique key used to identify this tile in the cache. * @param {Image} options.image - The image of the tile to cache. * @param {OpenSeadragon.TiledImage} options.tiledImage - The TiledImage that owns that tile. * @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this @@ -149,16 +150,16 @@ $.TileCache.prototype = { cacheTile: function( options ) { $.console.assert( options, "[TileCache.cacheTile] options is required" ); $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" ); - $.console.assert( options.tile.url, "[TileCache.cacheTile] options.tile.url is required" ); + $.console.assert( options.tile.cacheKey, "[TileCache.cacheTile] options.tile.cacheKey is required" ); $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" ); var cutoff = options.cutoff || 0; var insertionIndex = this._tilesLoaded.length; - var imageRecord = this._imagesLoaded[options.tile.url]; + var imageRecord = this._imagesLoaded[options.tile.cacheKey]; if (!imageRecord) { $.console.assert( options.image, "[TileCache.cacheTile] options.image is required to create an ImageRecord" ); - imageRecord = this._imagesLoaded[options.tile.url] = new ImageRecord({ + imageRecord = this._imagesLoaded[options.tile.cacheKey] = new ImageRecord({ image: options.image }); @@ -232,9 +233,9 @@ $.TileCache.prototype = { }, // private - getImageRecord: function(url) { - $.console.assert(url, '[TileCache.getImageRecord] url is required'); - return this._imagesLoaded[url]; + getImageRecord: function(cacheKey) { + $.console.assert(cacheKey, '[TileCache.getImageRecord] cacheKey is required'); + return this._imagesLoaded[cacheKey]; }, // private @@ -246,11 +247,11 @@ $.TileCache.prototype = { tile.unload(); tile.cacheImageRecord = null; - var imageRecord = this._imagesLoaded[tile.url]; + var imageRecord = this._imagesLoaded[tile.cacheKey]; imageRecord.removeTile(tile); if (!imageRecord.getTileCount()) { imageRecord.destroy(); - delete this._imagesLoaded[tile.url]; + delete this._imagesLoaded[tile.cacheKey]; this._imagesLoadedCount--; } diff --git a/src/tiledimage.js b/src/tiledimage.js index 70859ec0..8851a933 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -70,11 +70,18 @@ * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. + * @param {Number} [options.opacity=1] - Set to draw at proportional opacity. If zero, images will not draw. + * @param {Boolean} [options.preload=false] - Set true to load even when the image is hidden by zero opacity. * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.ajaxWithCredentials] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.loadTilesWithAjax] + * Whether to load tile data using AJAX requests. + * Defaults to the setting in {@link OpenSeadragon.Options}. + * @param {Object} [options.ajaxHeaders={}] + * A set of headers to include when making tile AJAX requests. */ $.TiledImage = function( options ) { var _this = this; @@ -140,7 +147,8 @@ $.TiledImage = function( options ) { //internal state properties viewer: null, tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. - coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. + coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas have been drawn. + loadingCoverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas are loaded or are being loaded/blended. lastDrawn: [], // An unordered list of Tiles drawn last frame. lastResetTime: 0, // Last time for which the tiledImage was reset. _midDraw: false, // Is the tiledImage currently updating the viewport? @@ -161,11 +169,16 @@ $.TiledImage = function( options ) { iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, debugMode: $.DEFAULT_SETTINGS.debugMode, crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, opacity: $.DEFAULT_SETTINGS.opacity, + preload: $.DEFAULT_SETTINGS.preload, compositeOperation: $.DEFAULT_SETTINGS.compositeOperation }, options ); + this._preload = this.preload; + delete this.preload; + this._fullyLoaded = false; this._xSpring = new $.Spring({ @@ -293,7 +306,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * Draws the TiledImage to its Drawer. */ draw: function() { - if (this.opacity !== 0) { + if (this.opacity !== 0 || this._preload) { this._midDraw = true; this._updateViewport(); this._midDraw = false; @@ -764,10 +777,43 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * @param {Number} opacity Opacity the tiled image should be drawn at. + * @fires OpenSeadragon.TiledImage.event:opacity-change */ setOpacity: function(opacity) { + if (opacity === this.opacity) { + return; + } + this.opacity = opacity; this._needsDraw = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event opacity-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {Number} opacity - The new opacity value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('opacity-change', { + opacity: this.opacity + }); + }, + + /** + * @returns {Boolean} whether the tiledImage can load its tiles even when it has zero opacity. + */ + getPreload: function() { + return this._preload; + }, + + /** + * Set true to load even when hidden. Set false to block loading when hidden. + */ + setPreload: function(preload) { + this._preload = !!preload; + this._needsDraw = true; }, /** @@ -803,8 +849,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag }, /** - * @private * Get the point around which this tiled image is rotated + * @private * @param {Boolean} current True for current rotation point, false for target. * @returns {OpenSeadragon.Point} */ @@ -821,10 +867,28 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + * @fires OpenSeadragon.TiledImage.event:composite-operation-change */ setCompositeOperation: function(compositeOperation) { + if (compositeOperation === this.compositeOperation) { + return; + } + this.compositeOperation = compositeOperation; this._needsDraw = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event composite-operation-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {String} compositeOperation - The new compositeOperation value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('composite-operation-change', { + compositeOperation: this.compositeOperation + }); }, // private @@ -917,6 +981,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag _updateViewport: function() { this._needsDraw = false; this._tilesLoading = 0; + this.loadingCoverage = {}; // Reset tile's internal drawn state while (this.lastDrawn.length > 0) { @@ -971,7 +1036,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( this.source.getPixelRatio( Math.max( - this.source.getClosestLevel(viewport.containerSize) - 1, + this.source.getClosestLevel(), 0 ) ), @@ -1115,6 +1180,7 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, } resetCoverage(tiledImage.coverage, level); + resetCoverage(tiledImage.loadingCoverage, level); //OK, a new drawing so do your calculations var cornerTiles = tiledImage._getCornerTiles(level, topLeftBound, bottomRightBound); @@ -1179,6 +1245,7 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity var tile = getTile( x, y, level, + tiledImage, tiledImage.source, tiledImage.tilesMatrix, currentTime, @@ -1208,6 +1275,9 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity setCoverage( tiledImage.coverage, level, x, y, false ); + var loadingCoverage = tile.loaded || tile.loading || isCovered(tiledImage.loadingCoverage, level, x, y); + setCoverage(tiledImage.loadingCoverage, level, x, y, loadingCoverage); + if ( !tile.exists ) { return best; } @@ -1237,7 +1307,7 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity if (tile.context2D) { setTileLoaded(tiledImage, tile); } else { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + var imageRecord = tiledImage._tileCache.getImageRecord(tile.cacheKey); if (imageRecord) { var image = imageRecord.getImage(); setTileLoaded(tiledImage, tile, image); @@ -1261,7 +1331,7 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity } else if ( tile.loading ) { // the tile is already in the download queue tiledImage._tilesLoading++; - } else { + } else if (!loadingCoverage) { best = compareTiles( best, tile ); } @@ -1275,6 +1345,7 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity * @param {Number} x * @param {Number} y * @param {Number} level + * @param {OpenSeadragon.TiledImage} tiledImage * @param {OpenSeadragon.TileSource} tileSource * @param {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile. * @param {Number} time @@ -1283,12 +1354,23 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity * @param {Number} worldHeight * @returns {OpenSeadragon.Tile} */ -function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) { +function getTile( + x, y, + level, + tiledImage, + tileSource, + tilesMatrix, + time, + numTiles, + worldWidth, + worldHeight +) { var xMod, yMod, bounds, exists, url, + ajaxHeaders, context2D, tile; @@ -1305,6 +1387,18 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds = tileSource.getTileBounds( level, xMod, yMod ); exists = tileSource.tileExists( level, xMod, yMod ); url = tileSource.getTileUrl( level, xMod, yMod ); + + // Headers are only applicable if loadTilesWithAjax is set + if (tiledImage.loadTilesWithAjax) { + ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod ); + // Combine tile AJAX headers with tiled image AJAX headers (if applicable) + if ($.isPlainObject(tiledImage.ajaxHeaders)) { + ajaxHeaders = $.extend({}, tiledImage.ajaxHeaders, ajaxHeaders); + } + } else { + ajaxHeaders = null; + } + context2D = tileSource.getContext2D ? tileSource.getContext2D(level, xMod, yMod) : undefined; @@ -1318,7 +1412,9 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, - context2D + context2D, + tiledImage.loadTilesWithAjax, + ajaxHeaders ); } @@ -1340,9 +1436,12 @@ function loadTile( tiledImage, tile, time ) { tile.loading = true; tiledImage._imageLoader.addJob({ src: tile.url, + loadWithAjax: tile.loadWithAjax, + ajaxHeaders: tile.ajaxHeaders, crossOriginPolicy: tiledImage.crossOriginPolicy, - callback: function( image, errorMsg ){ - onTileLoad( tiledImage, tile, time, image, errorMsg ); + ajaxWithCredentials: tiledImage.ajaxWithCredentials, + callback: function( image, errorMsg, tileRequest ){ + onTileLoad( tiledImage, tile, time, image, errorMsg, tileRequest ); }, abort: function() { tile.loading = false; @@ -1359,8 +1458,9 @@ function loadTile( tiledImage, tile, time ) { * @param {Number} time * @param {Image} image * @param {String} errorMsg + * @param {XMLHttpRequest} tileRequest */ -function onTileLoad( tiledImage, tile, time, image, errorMsg ) { +function onTileLoad( tiledImage, tile, time, image, errorMsg, tileRequest ) { if ( !image ) { $.console.log( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); /** @@ -1373,8 +1473,15 @@ function onTileLoad( tiledImage, tile, time, image, errorMsg ) { * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. * @property {number} time - The time in milliseconds when the tile load began. * @property {string} message - The error message. + * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available. */ - tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg}); + tiledImage.viewer.raiseEvent("tile-load-failed", { + tile: tile, + tiledImage: tiledImage, + time: time, + message: errorMsg, + tileRequest: tileRequest + }); tile.loading = false; tile.exists = false; return; @@ -1387,9 +1494,8 @@ function onTileLoad( tiledImage, tile, time, image, errorMsg ) { } var finish = function() { - var cutoff = Math.ceil( Math.log( - tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) ); - setTileLoaded(tiledImage, tile, image, cutoff); + var cutoff = tiledImage.source.getClosestLevel(); + setTileLoaded(tiledImage, tile, image, cutoff, tileRequest); }; // Check if we're mid-update; this can happen on IE8 because image load events for @@ -1410,7 +1516,7 @@ function onTileLoad( tiledImage, tile, time, image, errorMsg ) { * @param {Image} image * @param {Number} cutoff */ -function setTileLoaded(tiledImage, tile, image, cutoff) { +function setTileLoaded(tiledImage, tile, image, cutoff, tileRequest) { var increment = 0; function getCompletionCallback() { @@ -1445,6 +1551,7 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { * @property {Image} image - The image of the tile. * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. + * @property {XMLHttpRequest} tiledImage - 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 @@ -1453,6 +1560,7 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { tiledImage.viewer.raiseEvent("tile-loaded", { tile: tile, tiledImage: tiledImage, + tileRequest: tileRequest, image: image, getCompletionCallback: getCompletionCallback }); @@ -1697,7 +1805,7 @@ function compareTiles( previousBest, tile ) { * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. */ function drawTiles( tiledImage, lastDrawn ) { - if (lastDrawn.length === 0) { + if (tiledImage.opacity === 0 || lastDrawn.length === 0) { return; } var tile = lastDrawn[0]; diff --git a/src/tilesource.js b/src/tilesource.js index f826ab48..b40598ae 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -60,11 +60,15 @@ * the extending classes implementation of 'configure'. * @param {String} [options.url] * The URL for the data necessary for this TileSource. + * @param {String} [options.referenceStripThumbnailUrl] + * The URL for a thumbnail image to be used by the reference strip * @param {Function} [options.success] * A function to be called upon successful creation. * @param {Boolean} [options.ajaxWithCredentials] * If this TileSource needs to make an AJAX call, this specifies whether to set * the XHR's withCredentials (for accessing secure data). + * @param {Object} [options.ajaxHeaders] + * A set of headers to include in AJAX requests. * @param {Number} [options.width] * Width of the source image at max resolution in pixels. * @param {Number} [options.height] @@ -318,25 +322,20 @@ $.TileSource.prototype = { /** * @function - * @param {Number} level + * @returns {Number} The highest level in this tile source that can be contained in a single tile. */ - getClosestLevel: function( rect ) { + getClosestLevel: function() { var i, - tilesPerSide, tiles; - for( i = this.minLevel; i < this.maxLevel; i++ ){ - tiles = this.getNumTiles( i ); - tilesPerSide = new $.Point( - Math.floor( rect.x / this.getTileWidth(i) ), - Math.floor( rect.y / this.getTileHeight(i) ) - ); - - if( tiles.x + 1 >= tilesPerSide.x && tiles.y + 1 >= tilesPerSide.y ){ + for (i = this.minLevel + 1; i <= this.maxLevel; i++){ + tiles = this.getNumTiles(i); + if (tiles.x > 1 || tiles.y > 1) { break; } } - return Math.max( 0, i - 1 ); + + return i - 1; }, /** @@ -475,6 +474,7 @@ $.TileSource.prototype = { $.makeAjaxRequest( { url: url, withCredentials: this.ajaxWithCredentials, + headers: this.ajaxHeaders, success: function( xhr ) { var data = processResponse( xhr ); callback( data ); @@ -559,7 +559,7 @@ $.TileSource.prototype = { }, /** - * Responsible for retriving the url which will return an image for the + * Responsible for retrieving the url which will return an image for the * region specified by the given x, y, and level components. * This method is not implemented by this class other than to throw an Error * announcing you have to implement it. Because of the variety of tile @@ -575,6 +575,23 @@ $.TileSource.prototype = { throw new Error( "Method not implemented." ); }, + /** + * Responsible for retrieving the headers which will be attached to the image request for the + * region specified by the given x, y, and level components. + * This option is only relevant if {@link OpenSeadragon.Options}.loadTilesWithAjax is set to true. + * The headers returned here will override headers specified at the Viewer or TiledImage level. + * Specifying a falsy value for a header will clear its existing value set at the Viewer or + * TiledImage level (if any). + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {Object} + */ + getTileAjaxHeaders: function( level, x, y ) { + return {}; + }, + /** * @function * @param {Number} level @@ -629,7 +646,11 @@ function processResponse( xhr ){ data = xhr.responseText; } }else if( responseText.match(/\s*[\{\[].*/) ){ - data = $.parseJSON(responseText); + try{ + data = $.parseJSON(responseText); + } catch(e){ + data = responseText; + } }else{ data = responseText; } diff --git a/src/viewer.js b/src/viewer.js index cea725a3..649456de 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -96,6 +96,12 @@ $.Viewer = function( options ) { //internal state and dom identifiers id: options.id, hash: options.hash || nextHash++, + /** + * Index for page to be shown first next time open() is called (only used in sequenceMode). + * @member {Number} initialPage + * @memberof OpenSeadragon.Viewer# + */ + initialPage: 0, //dom nodes /** @@ -370,7 +376,8 @@ $.Viewer = function( options ) { // Create the image loader this.imageLoader = new $.ImageLoader({ - jobLimit: this.imageLoaderLimit + jobLimit: this.imageLoaderLimit, + timeout: options.timeout }); // Create the tile cache @@ -481,11 +488,13 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * except for the index property; images are added in sequence. * A TileSource specifier is anything you could pass as the tileSource property * of the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}. + * @param {Number} initialPage - If sequenceMode is true, display this page initially + * for the given tileSources. If specified, will overwrite the Viewer's existing initialPage property. * @return {OpenSeadragon.Viewer} Chainable. * @fires OpenSeadragon.Viewer.event:open * @fires OpenSeadragon.Viewer.event:open-failed */ - open: function (tileSources) { + open: function (tileSources, initialPage) { var _this = this; this.close(); @@ -500,23 +509,17 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.referenceStrip = null; } + if (typeof initialPage != 'undefined' && !isNaN(initialPage)) { + this.initialPage = initialPage; + } + this.tileSources = tileSources; this._sequenceIndex = Math.max(0, Math.min(this.tileSources.length - 1, this.initialPage)); if (this.tileSources.length) { this.open(this.tileSources[this._sequenceIndex]); if ( this.showReferenceStrip ){ - this.referenceStrip = new $.ReferenceStrip({ - id: this.referenceStripElement, - position: this.referenceStripPosition, - sizeRatio: this.referenceStripSizeRatio, - scroll: this.referenceStripScroll, - height: this.referenceStripHeight, - width: this.referenceStripWidth, - tileSources: this.tileSources, - prefixUrl: this.prefixUrl, - viewer: this - }); + this.addReferenceStrip(); } } @@ -845,6 +848,22 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return this; }, + /** + * Turns debugging mode on or off for this viewer. + * + * @function + * @param {Boolean} true to turn debug on, false to turn debug off. + */ + setDebugMode: function(debugMode){ + + for (var i = 0; i < this.world.getItemCount(); i++) { + this.world.getItemAt(i).debugMode = debugMode; + } + + this.debugMode = debugMode; + this.forceRedraw(); + }, + /** * @function * @return {Boolean} @@ -1226,12 +1245,22 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to * (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] Opacity the tiled image should be drawn at by default. + * @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 {Number} [options.degrees=0] Initial rotation of the tiled image around * its top left corner in degrees. * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy. + * @param {Boolean} [options.ajaxWithCredentials] Whether to set withCredentials on tile AJAX + * @param {Boolean} [options.loadTilesWithAjax] + * Whether to load tile data using AJAX requests. + * Defaults to the setting in {@link OpenSeadragon.Options}. + * @param {Object} [options.ajaxHeaders] + * A set of headers to include when making tile AJAX requests. + * Note that these headers will be merged over any headers specified in {@link OpenSeadragon.Options}. + * Specifying a falsy value for a header will clear its existing value set at the Viewer level (if any). + * requests. * @param {Function} [options.success] A function that gets called when the image is * successfully added. It's passed the event object which contains a single property: * "item", the resulting TiledImage. @@ -1264,12 +1293,26 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if (options.opacity === undefined) { options.opacity = this.opacity; } + if (options.preload === undefined) { + options.preload = this.preload; + } if (options.compositeOperation === undefined) { options.compositeOperation = this.compositeOperation; } if (options.crossOriginPolicy === undefined) { options.crossOriginPolicy = options.tileSource.crossOriginPolicy !== undefined ? options.tileSource.crossOriginPolicy : this.crossOriginPolicy; } + if (options.ajaxWithCredentials === undefined) { + options.ajaxWithCredentials = this.ajaxWithCredentials; + } + if (options.loadTilesWithAjax === undefined) { + options.loadTilesWithAjax = this.loadTilesWithAjax; + } + if (options.ajaxHeaders === undefined || options.ajaxHeaders === null) { + options.ajaxHeaders = this.ajaxHeaders; + } else if ($.isPlainObject(options.ajaxHeaders) && $.isPlainObject(this.ajaxHeaders)) { + options.ajaxHeaders = $.extend({}, this.ajaxHeaders, options.ajaxHeaders); + } var myQueueItem = { options: options @@ -1332,11 +1375,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this._loadQueue.push(myQueueItem); - getTileSourceImplementation( this, options.tileSource, options, function( tileSource ) { - - myQueueItem.tileSource = tileSource; - - // add everybody at the front of the queue that's ready to go + function processReadyItems() { var queueItem, tiledImage, optionsClone; while (_this._loadQueue.length) { queueItem = _this._loadQueue[0]; @@ -1370,6 +1409,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, clip: queueItem.options.clip, placeholderFillStyle: queueItem.options.placeholderFillStyle, opacity: queueItem.options.opacity, + preload: queueItem.options.preload, degrees: queueItem.options.degrees, compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, @@ -1384,6 +1424,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom, iOSDevice: _this.iOSDevice, crossOriginPolicy: queueItem.options.crossOriginPolicy, + ajaxWithCredentials: queueItem.options.ajaxWithCredentials, + loadTilesWithAjax: queueItem.options.loadTilesWithAjax, + ajaxHeaders: queueItem.options.ajaxHeaders, debugMode: _this.debugMode }); @@ -1419,9 +1462,20 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, }); } } + } + + getTileSourceImplementation( this, options.tileSource, options, function( tileSource ) { + + myQueueItem.tileSource = tileSource; + + // add everybody at the front of the queue that's ready to go + processReadyItems(); }, function( event ) { event.options = options; raiseAddItemFailed(event); + + // add everybody at the front of the queue that's ready to go + processReadyItems(); } ); }, @@ -2094,6 +2148,52 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, */ _cancelPendingImages: function() { this._loadQueue = []; + }, + + /** + * Removes the reference strip and disables displaying it. + * @function + */ + removeReferenceStrip: function() { + this.showReferenceStrip = false; + + if (this.referenceStrip) { + this.referenceStrip.destroy(); + this.referenceStrip = null; + } + }, + + /** + * Enables and displays the reference strip based on the currently set tileSources. + * Works only when the Viewer has sequenceMode set to true. + * @function + */ + addReferenceStrip: function() { + this.showReferenceStrip = true; + + if (this.sequenceMode) { + if (this.referenceStrip) { + return; + } + + if (this.tileSources.length && this.tileSources.length > 1) { + this.referenceStrip = new $.ReferenceStrip({ + id: this.referenceStripElement, + position: this.referenceStripPosition, + sizeRatio: this.referenceStripSizeRatio, + scroll: this.referenceStripScroll, + height: this.referenceStripHeight, + width: this.referenceStripWidth, + tileSources: this.tileSources, + prefixUrl: this.prefixUrl, + viewer: this + }); + + this.referenceStrip.setFocus( this._sequenceIndex ); + } + } else { + $.console.warn('Attempting to display a reference strip while "sequenceMode" is off.'); + } } }); @@ -2113,6 +2213,7 @@ function _getSafeElemSize (oElement) { ); } + /** * @function * @private @@ -2128,7 +2229,12 @@ function getTileSourceImplementation( viewer, tileSource, imgOptions, successCal tileSource = $.parseXml( tileSource ); //json should start with "{" or "[" and end with "}" or "]" } else if ( tileSource.match(/^\s*[\{\[].*[\}\]]\s*$/ ) ) { - tileSource = $.parseJSON(tileSource); + try { + var tileSourceJ = $.parseJSON(tileSource); + tileSource = tileSourceJ; + } catch (e) { + //tileSource = tileSource; + } } } @@ -2156,6 +2262,7 @@ function getTileSourceImplementation( viewer, tileSource, imgOptions, successCal crossOriginPolicy: imgOptions.crossOriginPolicy !== undefined ? imgOptions.crossOriginPolicy : viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, + ajaxHeaders: viewer.ajaxHeaders, useCanvas: viewer.useCanvas, success: function( event ) { successCallback( event.tileSource ); @@ -2463,16 +2570,15 @@ function onCanvasClick( event ) { this.canvas.focus(); } - if ( !event.preventDefaultAction && this.viewport && event.quick ) { - gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); - if ( gestureSettings.clickToZoom ) { - this.viewport.zoomBy( - event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick, - this.viewport.pointFromPixel( event.position, true ) - ); - this.viewport.applyConstraints(); - } - } + var canvasClickEventArgs = { + tracker: event.eventSource, + position: event.position, + quick: event.quick, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: event.preventDefaultAction + }; + /** * Raised when a mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element. * @@ -2485,15 +2591,21 @@ function onCanvasClick( event ) { * @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags. * @property {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default click to zoom behaviour. Default: false. * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.raiseEvent( 'canvas-click', { - tracker: event.eventSource, - position: event.position, - quick: event.quick, - shift: event.shift, - originalEvent: event.originalEvent - }); + this.raiseEvent( 'canvas-click', canvasClickEventArgs); + + if ( !canvasClickEventArgs.preventDefaultAction && this.viewport && event.quick ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.clickToZoom ) { + this.viewport.zoomBy( + event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + } } function onCanvasDblClick( event ) { @@ -2533,19 +2645,16 @@ function onCanvasDblClick( event ) { function onCanvasDrag( event ) { var gestureSettings; - if ( !event.preventDefaultAction && this.viewport ) { - gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); - if( !this.panHorizontal ){ - event.delta.x = 0; - } - if( !this.panVertical ){ - event.delta.y = 0; - } - this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled ); - if( this.constrainDuringPan ){ - this.viewport.applyConstraints(); - } - } + var canvasDragEventArgs = { + tracker: event.eventSource, + position: event.position, + delta: event.delta, + speed: event.speed, + direction: event.direction, + shift: event.shift, + originalEvent: event.originalEvent, + preventDefaultAction: event.preventDefaultAction + }; /** * Raised when a mouse or touch drag operation occurs on the {@link OpenSeadragon.Viewer#canvas} element. * @@ -2560,17 +2669,43 @@ function onCanvasDrag( event ) { * @property {Number} direction - Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. * @property {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag behaviour. Default: false. * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.raiseEvent( 'canvas-drag', { - tracker: event.eventSource, - position: event.position, - delta: event.delta, - speed: event.speed, - direction: event.direction, - shift: event.shift, - originalEvent: event.originalEvent - }); + this.raiseEvent( 'canvas-drag', canvasDragEventArgs); + + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if( !this.panHorizontal ){ + event.delta.x = 0; + } + if( !this.panVertical ){ + event.delta.y = 0; + } + + if( this.constrainDuringPan ){ + var delta = this.viewport.deltaPointsFromPixels( event.delta.negate() ); + + this.viewport.centerSpringX.target.value += delta.x; + this.viewport.centerSpringY.target.value += delta.y; + + var bounds = this.viewport.getBounds(); + var constrainedBounds = this.viewport.getConstrainedBounds(); + + this.viewport.centerSpringX.target.value -= delta.x; + this.viewport.centerSpringY.target.value -= delta.y; + + if (bounds.x != constrainedBounds.x) { + event.delta.x = 0; + } + + if (bounds.y != constrainedBounds.y) { + event.delta.y = 0; + } + } + + this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled && !this.constrainDuringPan); + } } function onCanvasDragEnd( event ) { @@ -2652,6 +2787,11 @@ function onCanvasEnter( event ) { } function onCanvasExit( event ) { + + if (window.location != window.parent.location){ + $.MouseTracker.resetAllMouseTrackers(); + } + /** * Raised when a pointer leaves the {@link OpenSeadragon.Viewer#canvas} element. * diff --git a/src/viewport.js b/src/viewport.js index 7d5418bd..9d357910 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -485,10 +485,9 @@ $.Viewport.prototype = { * @function * @private * @param {OpenSeadragon.Rect} bounds - * @param {Boolean} immediately * @return {OpenSeadragon.Rect} constrained bounds. */ - _applyBoundaryConstraints: function(bounds, immediately) { + _applyBoundaryConstraints: function(bounds) { var newBounds = new $.Rect( bounds.x, bounds.y, @@ -531,6 +530,16 @@ $.Viewport.prototype = { } } + return newBounds; + }, + + /** + * @function + * @private + * @param {Boolean} [immediately=false] - whether the function that triggered this event was + * called with the "immediately" flag + */ + _raiseConstraintsEvent: function(immediately) { if (this.viewer) { /** * Raised when the viewport constraints are applied (see {@link OpenSeadragon.Viewport#applyConstraints}). @@ -539,15 +548,14 @@ $.Viewport.prototype = { * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. - * @property {Boolean} immediately + * @property {Boolean} immediately - whether the function that triggered this event was + * called with the "immediately" flag * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.viewer.raiseEvent( 'constrain', { immediately: immediately }); } - - return newBounds; }, /** @@ -567,8 +575,8 @@ $.Viewport.prototype = { } var bounds = this.getBoundsNoRotate(); - var constrainedBounds = this._applyBoundaryConstraints( - bounds, immediately); + var constrainedBounds = this._applyBoundaryConstraints(bounds); + this._raiseConstraintsEvent(immediately); if (bounds.x !== constrainedBounds.x || bounds.y !== constrainedBounds.y || @@ -638,8 +646,9 @@ $.Viewport.prototype = { newBounds.y = center.y - newBounds.height / 2; } - newBounds = this._applyBoundaryConstraints(newBounds, immediately); + newBounds = this._applyBoundaryConstraints(newBounds); center = newBounds.getCenter(); + this._raiseConstraintsEvent(immediately); } if (immediately) { @@ -733,6 +742,23 @@ $.Viewport.prototype = { }, + /** + * Returns bounds taking constraints into account + * Added to improve constrained panning + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @return {OpenSeadragon.Viewport} Chainable. + */ + getConstrainedBounds: function(current) { + var bounds, + constrainedBounds; + + bounds = this.getBounds(current); + + constrainedBounds = this._applyBoundaryConstraints(bounds); + + return constrainedBounds; + }, + /** * @function * @param {OpenSeadragon.Point} delta @@ -805,7 +831,8 @@ $.Viewport.prototype = { * @return {OpenSeadragon.Viewport} Chainable. * @fires OpenSeadragon.Viewer.event:zoom */ - zoomTo: function( zoom, refPoint, immediately ) { + zoomTo: function(zoom, refPoint, immediately) { + var _this = this; this.zoomPoint = refPoint instanceof $.Point && !isNaN(refPoint.x) && @@ -813,13 +840,15 @@ $.Viewport.prototype = { refPoint : null; - if ( immediately ) { - this.zoomSpring.resetTo( zoom ); + if (immediately) { + this._adjustCenterSpringsForZoomPoint(function() { + _this.zoomSpring.resetTo(zoom); + }); } else { - this.zoomSpring.springTo( zoom ); + this.zoomSpring.springTo(zoom); } - if( this.viewer ){ + if (this.viewer) { /** * Raised when the viewport zoom level changes (see {@link OpenSeadragon.Viewport#zoomBy} and {@link OpenSeadragon.Viewport#zoomTo}). * @@ -832,7 +861,7 @@ $.Viewport.prototype = { * @property {Boolean} immediately * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.viewer.raiseEvent( 'zoom', { + this.viewer.raiseEvent('zoom', { zoom: zoom, refPoint: refPoint, immediately: immediately @@ -938,25 +967,10 @@ $.Viewport.prototype = { * @returns {Boolean} True if any change has been made, false otherwise. */ update: function() { - - if (this.zoomPoint) { - var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); - this.zoomSpring.update(); - var newZoomPixel = this.pixelFromPoint(this.zoomPoint, true); - - var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel); - var deltaZoomPoints = this.deltaPointsFromPixels( - deltaZoomPixels, true); - - this.centerSpringX.shiftBy(deltaZoomPoints.x); - this.centerSpringY.shiftBy(deltaZoomPoints.y); - - if (this.zoomSpring.isAtTargetValue()) { - this.zoomPoint = null; - } - } else { - this.zoomSpring.update(); - } + var _this = this; + this._adjustCenterSpringsForZoomPoint(function() { + _this.zoomSpring.update(); + }); this.centerSpringX.update(); this.centerSpringY.update(); @@ -972,6 +986,27 @@ $.Viewport.prototype = { return changed; }, + _adjustCenterSpringsForZoomPoint: function(zoomSpringHandler) { + if (this.zoomPoint) { + var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); + zoomSpringHandler(); + var newZoomPixel = this.pixelFromPoint(this.zoomPoint, true); + + var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel); + var deltaZoomPoints = this.deltaPointsFromPixels( + deltaZoomPixels, true); + + this.centerSpringX.shiftBy(deltaZoomPoints.x); + this.centerSpringY.shiftBy(deltaZoomPoints.y); + + if (this.zoomSpring.isAtTargetValue()) { + this.zoomPoint = null; + } + } else { + zoomSpringHandler(); + } + }, + /** * Convert a delta (translation vector) from viewport coordinates to pixels * coordinates. This method does not take rotation into account. @@ -1208,6 +1243,7 @@ $.Viewport.prototype = { * in image coordinate system. * @param {Number} [pixelWidth] the width in pixel of the rectangle. * @param {Number} [pixelHeight] the height in pixel of the rectangle. + * @returns {OpenSeadragon.Rect} This image's bounds in viewport coordinates */ imageToViewportRectangle: function(imageX, imageY, pixelWidth, pixelHeight) { var rect = imageX; diff --git a/src/zoomifytilesource.js b/src/zoomifytilesource.js index ad6aee52..a9f44ac2 100644 --- a/src/zoomifytilesource.js +++ b/src/zoomifytilesource.js @@ -18,7 +18,7 @@ * tilesUrl: "/test/data/zoomify/" * } * - * The tileSize is currently hardcoded to 256 (the usual Zoomify default). The tileUrl must the the path to the image _directory_. + * The tileSize is currently hardcoded to 256 (the usual Zoomify default). The tileUrl must the path to the image _directory_. * * 2) Loading image metadata from xml file: (CURRENTLY NOT SUPPORTED) * diff --git a/test/coverage.html b/test/coverage.html index 65711abc..36e2e056 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -10,6 +10,11 @@
+ + + @@ -80,6 +85,8 @@ + + diff --git a/test/data/testpattern.blob b/test/data/testpattern.blob new file mode 100644 index 00000000..fe027d91 Binary files /dev/null and b/test/data/testpattern.blob differ diff --git a/test/demo/constrainedpan.html b/test/demo/constrainedpan.html new file mode 100644 index 00000000..1cd7578a --- /dev/null +++ b/test/demo/constrainedpan.html @@ -0,0 +1,48 @@ + + + + OpenSeadragon fitBoundsWithConstraints() Demo + + + + +
+

Simple demo to see panning improvements using the following settings:

+ +
+ +
+ + + + \ No newline at end of file diff --git a/test/demo/customheaders.html b/test/demo/customheaders.html new file mode 100644 index 00000000..95063052 --- /dev/null +++ b/test/demo/customheaders.html @@ -0,0 +1,75 @@ + + + + OpenSeadragon Custom Request Headers Demo + + + + + +

+ Demo of how the loadTilesWithAjax and ajaxHeaders options as well as the getTileHeaders() method on TileSource can be applied. +

+

+ Examine the network requests in your browser developer tools to see the custom headers sent with each request. +

+
+ + + diff --git a/test/demo/iframe-embed.html b/test/demo/iframe-embed.html new file mode 100644 index 00000000..33a34b45 --- /dev/null +++ b/test/demo/iframe-embed.html @@ -0,0 +1,32 @@ + + + + Iframe example embed + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/test/demo/iframe-host.html b/test/demo/iframe-host.html new file mode 100644 index 00000000..ea602473 --- /dev/null +++ b/test/demo/iframe-host.html @@ -0,0 +1,18 @@ + + + + Iframe example + + + + + + + + + + + + \ No newline at end of file diff --git a/test/demo/setdebugmode.html b/test/demo/setdebugmode.html new file mode 100644 index 00000000..838e62ca --- /dev/null +++ b/test/demo/setdebugmode.html @@ -0,0 +1,44 @@ + + + + + OpenSeadragon Basic Demo + + + + + + +
+ Turn debug mode on and off after viewer has been created. + + +
+
+ + + + diff --git a/test/demo/zoomify.html b/test/demo/zoomify.html new file mode 100644 index 00000000..f4ba031e --- /dev/null +++ b/test/demo/zoomify.html @@ -0,0 +1,38 @@ + + + + OpenSeadragon Zoomify Demo + + + + + +
+ Simple demo page to show a default OpenSeadragon viewer with a Zoomify tile source. +
+
+ + + diff --git a/test/modules/ajax-tiles.js b/test/modules/ajax-tiles.js new file mode 100644 index 00000000..646e80b8 --- /dev/null +++ b/test/modules/ajax-tiles.js @@ -0,0 +1,242 @@ +/* global module, asyncTest, start, $, ok, equal, deepEqual, testLog */ + +(function() { + var viewer; + + // These values are generated by a script that concatenates all the tile files and records + // their byte ranges in a multi-dimensional array. + + // eslint-disable-next-line + var tileManifest = {"tileRanges":[[[[0,3467]]],[[[3467,6954]]],[[[344916,348425]]],[[[348425,351948]]],[[[351948,355576]]],[[[355576,359520]]],[[[359520,364663]]],[[[364663,374196]]],[[[374196,407307]]],[[[407307,435465],[435465,463663]],[[463663,491839],[491839,520078]]],[[[6954,29582],[29582,50315],[50315,71936],[71936,92703]],[[92703,113385],[113385,133265],[133265,154763],[154763,175710]],[[175710,197306],[197306,218807],[218807,242177],[242177,263007]],[[263007,283790],[283790,304822],[304822,325691],[325691,344916]]]],"totalSize":520078} + + function getTileRangeHeader(level, x, y) { + return 'bytes=' + tileManifest.tileRanges[level][x][y].join('-') + '/' + tileManifest.totalSize; + } + + // This tile source demonstrates how you can retrieve individual tiles from a single file + // using the Range header. + var customTileSource = { + width: 1000, + height: 1000, + tileWidth: 254, + tileHeight: 254, + tileOverlap: 1, + maxLevel: 10, + minLevel: 0, + // The tile URL is always the same. Only the Range header changes + getTileUrl: function () { + return '/test/data/testpattern.blob'; + }, + // This method will send the appropriate range header for this tile based on the data + // in tileByteRanges. + getTileAjaxHeaders: function(level, x, y) { + return { + Range: getTileRangeHeader(level, x, y) + }; + }, + }; + + module('AJAX-Tiles', { + setup: function() { + $('
').appendTo('#qunit-fixture'); + + testLog.reset(); + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + springStiffness: 100, // Faster animation = faster tests, + loadTilesWithAjax: true, + ajaxHeaders: { + 'X-Viewer-Header': 'ViewerHeaderValue' + } + }); + }, + teardown: function() { + if (viewer && viewer.close) { + viewer.close(); + } + + viewer = null; + } + }); + + asyncTest('tile-loaded event includes AJAX request object', function() { + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + ok(evt.tileRequest, 'Event includes tileRequest property'); + equal(evt.tileRequest.readyState, XMLHttpRequest.DONE, 'tileRequest is in completed state'); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + viewer.open(customTileSource); + }); + + asyncTest('withCredentials is set in tile AJAX requests', function() { + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + ok(evt.tileRequest, 'Event includes tileRequest property'); + equal(evt.tileRequest.readyState, XMLHttpRequest.DONE, 'tileRequest is in completed state'); + equal(evt.tileRequest.withCredentials, true, 'withCredentials is set in tile request'); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + viewer.addTiledImage({ + tileSource: customTileSource, + ajaxWithCredentials: true + }); + }); + + asyncTest('tile-load-failed event includes AJAX request object', function() { + // Create a tile source that points to a broken URL + var brokenTileSource = OpenSeadragon.extend({}, customTileSource, { + getTileUrl: function () { + return '/test/data/testpattern.blob.invalid'; + } + }); + + var tileLoadFailed = function tileLoadFailed(evt) { + viewer.removeHandler('tile-load-failed', tileLoadFailed); + ok(evt.tileRequest, 'Event includes tileRequest property'); + equal(evt.tileRequest.readyState, XMLHttpRequest.DONE, 'tileRequest is in completed state'); + start(); + }; + + viewer.addHandler('tile-load-failed', tileLoadFailed); + viewer.open(brokenTileSource); + }); + + asyncTest('Headers can be set per-tile', function() { + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + var tile = evt.tile; + ok(tile, 'tile property exists on event'); + ok(tile.ajaxHeaders, 'Tile has ajaxHeaders property'); + equal(tile.ajaxHeaders.Range, getTileRangeHeader(tile.level, tile.x, tile.y), 'Tile has correct range header.'); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + + viewer.open(customTileSource); + }); + + asyncTest('Headers are propagated correctly', function() { + // Create a tile source that sets a static header for tiles + var staticHeaderTileSource = OpenSeadragon.extend({}, customTileSource, { + getTileAjaxHeaders: function() { + return { + 'X-Tile-Header': 'TileHeaderValue' + }; + } + }); + + var expectedHeaders = { + 'X-Viewer-Header': 'ViewerHeaderValue', + 'X-TiledImage-Header': 'TiledImageHeaderValue', + 'X-Tile-Header': 'TileHeaderValue' + }; + + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + var tile = evt.tile; + ok(tile, 'tile property exists on event'); + ok(tile.ajaxHeaders, 'Tile has ajaxHeaders property'); + deepEqual( + tile.ajaxHeaders, expectedHeaders, + 'Tile headers include headers set on Viewer and TiledImage' + ); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + + viewer.addTiledImage({ + ajaxHeaders: { + 'X-TiledImage-Header': 'TiledImageHeaderValue' + }, + tileSource: staticHeaderTileSource + }); + }); + + asyncTest('Viewer headers are overwritten by TiledImage', function() { + // Create a tile source that sets a static header for tiles + var staticHeaderTileSource = OpenSeadragon.extend({}, customTileSource, { + getTileAjaxHeaders: function() { + return { + 'X-Tile-Header': 'TileHeaderValue' + }; + } + }); + + var expectedHeaders = { + 'X-Viewer-Header': 'ViewerHeaderValue-Overwritten', + 'X-TiledImage-Header': 'TiledImageHeaderValue', + 'X-Tile-Header': 'TileHeaderValue' + }; + + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + var tile = evt.tile; + ok(tile, 'tile property exists on event'); + ok(tile.ajaxHeaders, 'Tile has ajaxHeaders property'); + deepEqual( + tile.ajaxHeaders, expectedHeaders, + 'TiledImage header overwrites viewer header' + ); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + + viewer.addTiledImage({ + ajaxHeaders: { + 'X-TiledImage-Header': 'TiledImageHeaderValue', + 'X-Viewer-Header': 'ViewerHeaderValue-Overwritten' + }, + tileSource: staticHeaderTileSource + }); + }); + + asyncTest('TiledImage headers are overwritten by Tile', function() { + + var expectedHeaders = { + 'X-Viewer-Header': 'ViewerHeaderValue', + 'X-TiledImage-Header': 'TiledImageHeaderValue-Overwritten', + 'X-Tile-Header': 'TileHeaderValue' + }; + + var tileLoaded = function tileLoaded(evt) { + viewer.removeHandler('tile-loaded', tileLoaded); + var tile = evt.tile; + ok(tile, 'tile property exists on event'); + ok(tile.ajaxHeaders, 'Tile has ajaxHeaders property'); + deepEqual( + tile.ajaxHeaders, expectedHeaders, + 'Tile header overwrites TiledImage header' + ); + start(); + }; + + viewer.addHandler('tile-loaded', tileLoaded); + + // Create a tile source that sets a static header for tiles + var staticHeaderTileSource = OpenSeadragon.extend({}, customTileSource, { + getTileAjaxHeaders: function() { + return { + 'X-TiledImage-Header': 'TiledImageHeaderValue-Overwritten', + 'X-Tile-Header': 'TileHeaderValue' + }; + } + }); + + viewer.addTiledImage({ + ajaxHeaders: { + 'X-TiledImage-Header': 'TiledImageHeaderValue' + }, + tileSource: staticHeaderTileSource + }); + }); +})(); diff --git a/test/modules/basic.js b/test/modules/basic.js index 7dc2a9ef..4cecce2a 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -424,11 +424,47 @@ } ); - test('version object', function() { - equal(typeof OpenSeadragon.version.versionStr, "string", "versionStr should be a string"); - ok(OpenSeadragon.version.major >= 0, "major should be a positive number"); - ok(OpenSeadragon.version.minor >= 0, "minor should be a positive number"); - ok(OpenSeadragon.version.revision >= 0, "revision should be a positive number"); + + asyncTest('SetDebugMode', function() { + ok(viewer, 'Viewer exists'); + + var checkImageTilesDebugState = function (expectedState) { + + for (var i = 0; i < viewer.world.getItemCount(); i++) { + if(viewer.world.getItemAt(i).debugMode != expectedState) { + return false; + } + } + return true; + }; + + var openHandler = function(event) { + viewer.removeHandler('open', openHandler); + + //Ensure we start with debug mode turned off + viewer.setDebugMode(false); + ok(checkImageTilesDebugState(false), "All image tiles have debug mode turned off."); + ok(!viewer.debugMode, "Viewer debug mode is turned off."); + + //Turn debug mode on and check that the Viewer and all tiled images are in debug mode. + viewer.setDebugMode(true); + ok(checkImageTilesDebugState(true), "All image tiles have debug mode turned on."); + ok(viewer.debugMode, "Viewer debug mode is turned on."); + + start(); + }; + + viewer.addHandler('open', openHandler); + viewer.open('/test/data/testpattern.dzi'); }); + //Version numbers are injected by the build process, so skip version tests if we are only running code coverage + if(!window.isCoverageTest ){ + test('version object', function() { + equal(typeof OpenSeadragon.version.versionStr, "string", "versionStr should be a string"); + ok(OpenSeadragon.version.major >= 0, "major should be a positive number"); + ok(OpenSeadragon.version.minor >= 0, "minor should be a positive number"); + ok(OpenSeadragon.version.revision >= 0, "revision should be a positive number"); + }); + } })(); diff --git a/test/modules/dzitilesource.js b/test/modules/dzitilesource.js index e7eb2114..25b0c265 100644 --- a/test/modules/dzitilesource.js +++ b/test/modules/dzitilesource.js @@ -36,6 +36,9 @@ testImplicitTilesUrl( '/iiipsrv?DeepZoom=/path/my.dzi', '/iiipsrv?DeepZoom=/path/my_files/', 'querystring in dzi url should not be ignored before slashes'); + testImplicitTilesUrl( + '/fcg-bin/iipsrv.fcgi?Deepzoom=123test.tif.dzi', '/fcg-bin/iipsrv.fcgi?Deepzoom=123test.tif_files/', + 'filename in querystring does not have to contain slash'); }); }()); diff --git a/test/modules/imageloader.js b/test/modules/imageloader.js new file mode 100644 index 00000000..1750ea73 --- /dev/null +++ b/test/modules/imageloader.js @@ -0,0 +1,88 @@ +/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */ + +(function() { + var viewer, + baseOptions = { + id: 'example', + prefixUrl: '/build/openseadragon/images/', + springStiffness: 100 // Faster animation = faster tests + }; + + module('ImageLoader', { + setup: function () { + var example = $('
').appendTo("#qunit-fixture"); + + testLog.reset(); + }, + teardown: function () { + if (viewer && viewer.close) { + viewer.close(); + } + + viewer = null; + } + }); + + // ---------- + + test('Default timeout', function() { + var actual, + expected = OpenSeadragon.DEFAULT_SETTINGS.timeout, + message, + options = OpenSeadragon.extend(true, baseOptions, { + imageLoaderLimit: 1 + }), + viewer = OpenSeadragon(options), + imageLoader = viewer.imageLoader; + + message = 'ImageLoader timeout should be set to the default value of ' + expected + ' when none is specified'; + actual = imageLoader.timeout; + equal(actual, expected, message); + + // Manually seize the ImageLoader + imageLoader.jobsInProgress = imageLoader.jobLimit; + imageLoader.addJob({ + src: 'test', + loadWithAjax: false, + crossOriginPolicy: 'test', + ajaxWithCredentials: false, + abort: function() {} + }); + + message = 'ImageJob should inherit the ImageLoader timeout value'; + actual = imageLoader.jobQueue.shift().timeout; + equal(actual, expected, message); + }); + + // ---------- + + test('Configure timeout', function() { + var actual, + expected = 123456, + message, + options = OpenSeadragon.extend(true, baseOptions, { + imageLoaderLimit: 1, + timeout: expected + }), + viewer = OpenSeadragon(options), + imageLoader = viewer.imageLoader; + + message = 'ImageLoader timeout should be configurable'; + actual = imageLoader.timeout; + equal(actual, expected, message); + + imageLoader.jobsInProgress = imageLoader.jobLimit; + imageLoader.addJob({ + src: 'test', + loadWithAjax: false, + crossOriginPolicy: 'test', + ajaxWithCredentials: false, + abort: function() {} + }); + + message = 'ImageJob should inherit the ImageLoader timeout value'; + actual = imageLoader.jobQueue.shift().timeout; + equal(actual, expected, message); + }); + +})(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index c58560a6..3f75f9f4 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -844,6 +844,88 @@ viewer.addHandler('open', openHandler); }); + asyncTest('Item opacity is synchronized', function() { + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + tileSources: ['/test/data/testpattern.dzi', '/test/data/testpattern.dzi'], + springStiffness: 100, // Faster animation = faster tests + showNavigator: true + }); + + var navOpenHandler = function(event) { + if (viewer.navigator.world.getItemCount() === 2) { + viewer.navigator.world.removeHandler('add-item', navOpenHandler); + + setTimeout(function() { + // Test initial formation + for (var i = 0; i < 2; i++) { + equal(viewer.navigator.world.getItemAt(i).getOpacity(), + viewer.world.getItemAt(i).getOpacity(), 'opacity is the same'); + } + + // Try changing the opacity of one + viewer.world.getItemAt(1).setOpacity(0.5); + equal(viewer.navigator.world.getItemAt(1).getOpacity(), + viewer.world.getItemAt(1).getOpacity(), 'opacity is the same after change'); + + start(); + }, 1); + } + }; + + var openHandler = function() { + viewer.removeHandler('open', openHandler); + viewer.navigator.world.addHandler('add-item', navOpenHandler); + // The navigator may already have added the items. + navOpenHandler(); + }; + + viewer.addHandler('open', openHandler); + }); + + asyncTest('Item composite operation is synchronized', function() { + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + tileSources: ['/test/data/testpattern.dzi', '/test/data/testpattern.dzi'], + springStiffness: 100, // Faster animation = faster tests + showNavigator: true + }); + + var navOpenHandler = function(event) { + if (viewer.navigator.world.getItemCount() === 2) { + viewer.navigator.world.removeHandler('add-item', navOpenHandler); + + setTimeout(function() { + // Test initial formation + for (var i = 0; i < 2; i++) { + equal(viewer.navigator.world.getItemAt(i).getCompositeOperation(), + viewer.world.getItemAt(i).getCompositeOperation(), 'composite operation is the same'); + } + + // Try changing the composite operation of one + viewer.world.getItemAt(1).setCompositeOperation('multiply'); + equal(viewer.navigator.world.getItemAt(1).getCompositeOperation(), + viewer.world.getItemAt(1).getCompositeOperation(), 'composite operation is the same after change'); + + start(); + }, 1); + } + }; + + var openHandler = function() { + viewer.removeHandler('open', openHandler); + viewer.navigator.world.addHandler('add-item', navOpenHandler); + // The navigator may already have added the items. + navOpenHandler(); + }; + + viewer.addHandler('open', openHandler); + }); + asyncTest('Viewer options transmitted to navigator', function() { viewer = OpenSeadragon({ diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 80bb44de..ba89b73a 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -25,12 +25,14 @@ var fakeTile0 = { url: 'foo.jpg', + cacheKey: 'foo.jpg', image: {}, unload: function() {} }; var fakeTile1 = { url: 'foo.jpg', + cacheKey: 'foo.jpg', image: {}, unload: function() {} }; @@ -74,18 +76,21 @@ 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() {} }; diff --git a/test/modules/viewport.js b/test/modules/viewport.js index e49c1e68..236d4f9c 100644 --- a/test/modules/viewport.js +++ b/test/modules/viewport.js @@ -36,7 +36,7 @@ var TALL_PATH = '/test/data/tall.dzi'; var WIDE_PATH = '/test/data/wide.dzi'; - var testZoomLevels = [-1, 0, 0.1, 0.5, 4, 10]; + var testZoomLevels = [0.1, 0.2, 0.5, 1, 4, 10]; var testPoints = [ new OpenSeadragon.Point(0, 0), @@ -59,7 +59,6 @@ var reopenViewerHelper = function(config) { var expected, level, actual, i = 0; var openHandler = function(event) { - viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; expected = config.processExpected(level, expected); actual = viewport[config.method](); @@ -70,7 +69,7 @@ "Test " + config.method + " with zoom level of " + level + ". Expected : " + expected + ", got " + actual ); i++; - if(i < testZoomLevels.length){ + if (i < testZoomLevels.length) { level = expected = testZoomLevels[i]; var viewerConfig = { id: VIEWER_ID, @@ -80,15 +79,22 @@ viewerConfig[config.property] = level; viewer = OpenSeadragon(viewerConfig); - viewer.addHandler('open', openHandler); + viewer.addOnceHandler('open', openHandler); viewer.open(DZI_PATH); } else { start(); } }; - viewer.addHandler('open', openHandler); level = expected = testZoomLevels[i]; - viewer[config.property] = level; + var viewerConfig = { + id: VIEWER_ID, + prefixUrl: PREFIX_URL, + springStiffness: SPRING_STIFFNESS + }; + + viewerConfig[config.property] = level; + viewer = OpenSeadragon(viewerConfig); + viewer.addOnceHandler('open', openHandler); viewer.open(DZI_PATH); }; @@ -211,15 +217,9 @@ property: 'defaultZoomLevel', method: 'getHomeBounds', processExpected: function(level, expected) { - // Have to special case this to avoid dividing by 0 - if(level === -1 || level === 0){ - expected = new OpenSeadragon.Rect(0, 0, 1, 1); - } else { - var sideLength = 1.0 / viewer.defaultZoomLevel; // it's a square in this case - var position = 0.5 - (sideLength / 2.0); - expected = new OpenSeadragon.Rect(position, position, sideLength, sideLength); - } - return expected; + var sideLength = 1.0 / viewer.defaultZoomLevel; // it's a square in this case + var position = 0.5 - (sideLength / 2.0); + return new OpenSeadragon.Rect(position, position, sideLength, sideLength); } }); }); @@ -333,44 +333,39 @@ // I don't use the helper for this one because it sets a couple more // properties that would need special casing. asyncTest('getHomeZoomWithHomeFillsViewer', function() { - var expected, level, i = 0; + var i = 0; var openHandler = function(event) { - viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.zoomTo(ZOOM_FACTOR, null, true); - // Special cases for oddball levels - if (level === -1) { - expected = 0.25; - } else if(level === 0){ - expected = 1; - } - equal( viewport.getHomeZoom(), - expected, - "Test getHomeZoom with homeFillsViewer = true and default zoom level of " + expected + testZoomLevels[i], + "Test getHomeZoom with homeFillsViewer = true and default zoom level of " + testZoomLevels[i] ); i++; - if(i < testZoomLevels.length){ - level = expected = testZoomLevels[i]; + if (i < testZoomLevels.length) { viewer = OpenSeadragon({ - id: VIEWER_ID, - prefixUrl: PREFIX_URL, + id: VIEWER_ID, + prefixUrl: PREFIX_URL, springStiffness: SPRING_STIFFNESS, - defaultZoomLevel: level, + defaultZoomLevel: testZoomLevels[i], homeFillsViewer: true }); - viewer.addHandler('open', openHandler); + viewer.addOnceHandler('open', openHandler); viewer.open(TALL_PATH); // use a different image for homeFillsViewer } else { start(); } }; - viewer.addHandler('open', openHandler); - level = expected = testZoomLevels[i]; - viewer.homeFillsViewer = true; - viewer.defaultZoomLevel = expected; + viewer = OpenSeadragon({ + id: VIEWER_ID, + prefixUrl: PREFIX_URL, + springStiffness: SPRING_STIFFNESS, + defaultZoomLevel: testZoomLevels[i], + homeFillsViewer: true + }); + viewer.addOnceHandler('open', openHandler); viewer.open(TALL_PATH); // use a different image for homeFillsViewer }); @@ -725,27 +720,18 @@ viewer.open(DZI_PATH); }); - asyncTest('zoomBy', function(){ + asyncTest('zoomBy no ref point', function() { var openHandler = function(event) { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; - for (var i = 0; i < testZoomLevels.length; i++){ + for (var i = 0; i < testZoomLevels.length; i++) { viewport.zoomBy(testZoomLevels[i], null, true); propEqual( viewport.getZoom(), testZoomLevels[i], "Zoomed by the correct amount." ); - - // now use a ref point - // TODO: check the ending position due to ref point - viewport.zoomBy(testZoomLevels[i], testPoints[i], true); - propEqual( - viewport.getZoom(), - testZoomLevels[i], - "Zoomed by the correct amount." - ); } start(); @@ -754,27 +740,88 @@ viewer.open(DZI_PATH); }); - asyncTest('zoomTo', function(){ + asyncTest('zoomBy with ref point', function() { var openHandler = function(event) { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; - for (var i = 0; i < testZoomLevels.length; i++){ + var expectedCenters = [ + new OpenSeadragon.Point(5, 5), + new OpenSeadragon.Point(6.996, 6.996), + new OpenSeadragon.Point(7.246, 6.996), + new OpenSeadragon.Point(7.246, 6.996), + new OpenSeadragon.Point(7.621, 7.371), + new OpenSeadragon.Point(7.621, 7.371), + ]; + + for (var i = 0; i < testZoomLevels.length; i++) { + viewport.zoomBy(testZoomLevels[i], testPoints[i], true); + propEqual( + viewport.getZoom(), + testZoomLevels[i], + "Zoomed by the correct amount." + ); + assertPointsEquals( + viewport.getCenter(), + expectedCenters[i], + 1e-14, + "Panned to the correct location." + ); + } + + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('zoomTo no ref point', function() { + var openHandler = function(event) { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + + for (var i = 0; i < testZoomLevels.length; i++) { viewport.zoomTo(testZoomLevels[i], null, true); propEqual( viewport.getZoom(), testZoomLevels[i], "Zoomed to the correct level." ); + } - // now use a ref point - // TODO: check the ending position due to ref point + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('zoomTo with ref point', function() { + var openHandler = function(event) { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + + var expectedCenters = [ + new OpenSeadragon.Point(5, 5), + new OpenSeadragon.Point(4.7505, 4.7505), + new OpenSeadragon.Point(4.6005, 4.7505), + new OpenSeadragon.Point(4.8455, 4.9955), + new OpenSeadragon.Point(5.2205, 5.3705), + new OpenSeadragon.Point(5.2205, 5.3705), + ]; + + for (var i = 0; i < testZoomLevels.length; i++) { viewport.zoomTo(testZoomLevels[i], testPoints[i], true); propEqual( viewport.getZoom(), testZoomLevels[i], "Zoomed to the correct level." ); + assertPointsEquals( + viewport.getCenter(), + expectedCenters[i], + 1e-14, + "Panned to the correct location." + ); } start(); diff --git a/test/test.html b/test/test.html index 985dcb3e..16e7e54a 100644 --- a/test/test.html +++ b/test/test.html @@ -42,6 +42,8 @@ + +