diff --git a/changelog.txt b/changelog.txt index 69b73d6b..8d2e670c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,9 +1,14 @@ OPENSEADRAGON CHANGELOG ======================= -2.2.2: (in progress) +2.3.0: (in progress) * 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) +* Optimization: Use the squared distance when comparing tiles (#1027) +* 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) * You can now set the rotation of individual tiled images (#1006) * Fixed CORS bug in IE 10 (#967) * Added support for commonjs (#984) @@ -15,7 +20,6 @@ OPENSEADRAGON CHANGELOG * 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) @@ -28,6 +32,26 @@ OPENSEADRAGON CHANGELOG * 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) +* Better compression for our UI images (#1134) +* Fixed: the reference strip would leak memory when opening new sets of images (#1175) +* You can now load tiles via AJAX and custom AJAX request headers (#1055) +* Added fix for supporting weird filenames that look like JSONs (#1189) +* Fixed: zoomTo/zoomBy ignore refPoint if immediately is true (#1184) +* Enabled configuration of ImageLoader timeout (#1192) +* Viewer.open() now supports an initialPage argument for sequenceMode (#1196) +* Fixed: IIPImageServer didn't work with the latest OSD release (#1199) +* Now clamping pixel ratio density to a minimum of 1, fixing display issues on low density devices (#1200) +* Improved calculation for determining which level to load first (#1198) +* Fixed: setItemIndex method not working with navigator inside "open" event (#1201) +* The navigator now picks up opacity and compositeOperation changes (#1203) +* New events for opacity and compositeOperation changes (#1203) +* Fixed: The reference strip didn't show the initial page if it wasn't the first page (#1208) +* Added support for setting debug mode after the Viewer object has been constructed (#1224) +* 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) +* Better calculation for TileCache release cutoff (#1214) +* Fixed: One image failing to load could cause the others to never load (#1229) +* Added functions for dynamically adding and removing the reference strip in sequence mode (#1213) 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/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..443bfaca 100644 --- a/src/dzitilesource.js +++ b/src/dzitilesource.js @@ -140,7 +140,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 0480300a..aa5eb35f 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -2894,6 +2894,12 @@ } } + // A primary mouse button may have been released while the non-primary button was down + if (pointsList.contacts > 0 && pointsList.type === 'mouse') { + // Stop tracking the mouse; see https://github.com/openseadragon/openseadragon/pull/1223 + pointsList.contacts--; + return true; + } return false; } 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..9b07d922 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -411,6 +411,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 +585,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. + * */ /** @@ -867,7 +875,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 +889,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 +1014,8 @@ function OpenSeadragon( options ){ initialPage: 0, crossOriginPolicy: false, ajaxWithCredentials: false, + loadTilesWithAjax: false, + ajaxHeaders: {}, //PAN AND ZOOM SETTINGS AND CONSTRAINTS panHorizontal: true, @@ -2120,11 +2131,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 +2148,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 +2165,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 +2185,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 +2261,8 @@ function OpenSeadragon( options ){ } } } + + return request; }, /** diff --git a/src/referencestrip.js b/src/referencestrip.js index 5559f85a..69fa8e29 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); } @@ -463,6 +470,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..f7080aa7 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -75,6 +75,12 @@ * @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; @@ -161,6 +167,7 @@ $.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, compositeOperation: $.DEFAULT_SETTINGS.compositeOperation @@ -764,10 +771,28 @@ $.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 + }); }, /** @@ -821,10 +846,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 @@ -971,7 +1014,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 ) ), @@ -1179,6 +1222,7 @@ function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity var tile = getTile( x, y, level, + tiledImage, tiledImage.source, tiledImage.tilesMatrix, currentTime, @@ -1237,7 +1281,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); @@ -1275,6 +1319,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 +1328,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 +1361,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 +1386,9 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, - context2D + context2D, + tiledImage.loadTilesWithAjax, + ajaxHeaders ); } @@ -1340,9 +1410,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 +1432,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 +1447,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 +1468,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 +1490,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 +1525,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 +1534,7 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { tiledImage.viewer.raiseEvent("tile-loaded", { tile: tile, tiledImage: tiledImage, + tileRequest: tileRequest, image: image, getCompletionCallback: getCompletionCallback }); diff --git a/src/tilesource.js b/src/tilesource.js index f826ab48..e83c4fab 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -65,6 +65,8 @@ * @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 +320,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 +472,7 @@ $.TileSource.prototype = { $.makeAjaxRequest( { url: url, withCredentials: this.ajaxWithCredentials, + headers: this.ajaxHeaders, success: function( xhr ) { var data = processResponse( xhr ); callback( data ); @@ -559,7 +557,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 +573,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 +644,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 75061146..936e4eaf 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} @@ -1232,6 +1251,15 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @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. @@ -1270,6 +1298,17 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, 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 +1371,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]; @@ -1384,6 +1419,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 +1457,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 +2143,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 +2208,7 @@ function _getSafeElemSize (oElement) { ); } + /** * @function * @private @@ -2128,7 +2224,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 +2257,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 +2565,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 +2586,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,6 +2640,35 @@ function onCanvasDblClick( event ) { function onCanvasDrag( event ) { var gestureSettings; + 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. + * + * @event canvas-drag + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. + * @property {Number} speed - Current computed speed, in pixels per second. + * @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', canvasDragEventArgs); + if ( !event.preventDefaultAction && this.viewport ) { gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); if( !this.panHorizontal ){ @@ -2565,32 +2701,6 @@ function onCanvasDrag( event ) { this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled ); } - - /** - * Raised when a mouse or touch drag operation occurs on the {@link OpenSeadragon.Viewer#canvas} element. - * - * @event canvas-drag - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. - * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. - * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. - * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. - * @property {Number} speed - Current computed speed, in pixels per second. - * @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 {?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 - }); } function onCanvasDragEnd( event ) { diff --git a/src/viewport.js b/src/viewport.js index e218e3d8..6a23b85f 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -823,7 +823,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) && @@ -831,13 +832,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}). * @@ -850,7 +853,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 @@ -956,25 +959,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(); @@ -990,6 +978,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. @@ -1226,6 +1235,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/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/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 @@ + +