diff --git a/.gitignore b/.gitignore index aaaabf8c..282b4dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ build/ sftp-config.json coverage/ temp/ -.idea \ No newline at end of file +.idea +/nbproject/private/ +.directory diff --git a/Gruntfile.js b/Gruntfile.js index 06511e31..8c7ff60a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,6 +39,7 @@ module.exports = function(grunt) { "src/osmtilesource.js", "src/tmstilesource.js", "src/legacytilesource.js", + "src/imagetilesource.js", "src/tilesourcecollection.js", "src/button.js", "src/buttongroup.js", diff --git a/changelog.txt b/changelog.txt index 6639deeb..83bf2f07 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,40 +1,46 @@ OPENSEADRAGON CHANGELOG ======================= -2.1.0: (in progress) +2.1.1: (in progress) + +2.1.0: + * BREAKING CHANGE: the tile does not hold a reference to its image anymore. Only the tile cache keep a reference to images. * BREAKING CHANGE: TileSource.tileSize no longer exists; use TileSource.getTileWidth() and TileSource.getTileHeight() instead. * DEPRECATION: let ImageRecord.getRenderedContext create the rendered context instead of using ImageRecord.setRenderedContext * DEPRECATION: TileSource.getTileSize() is deprecated. Use TileSource.getTileWidth() and TileSource.getTileHeight() instead. -* Added "tile-loaded" event on the viewer allowing to modify a tile before it is marked ready to be drawn (#659) -* Added "tile-unloaded" event on the viewer allowing to free up memory one has allocated on a tile (#659) -* Fixed flickering tiles with useCanvas=false when no cache is used (#661) -* Added additional coordinates conversion methods to TiledImage (#662) -* 'display: none' no longer gets reset on overlays during draw (#668) -* Added `preserveImageSizeOnResize` option (#666) -* Better error reporting for tile load failures (#679) -* Added collectionColumns as a configuration parameter (#680) +* Changed resize behaviour to prevent "snapping" to world bounds when constraints allow more space (#711) * Added support for non-square tiles (#673) * TileSource.Options objects can now optionally provide tileWidth/tileHeight instead of tileSize for non-square tile support. * IIIFTileSources will now respect non-square tiles if available. +* Added new tile source for simple images: ImageTileSource (#760) +* Optimized adding large numbers of items to the world with collectionMode (#735) +* Registers as an AMD module where possible (#719) +* Added "tile-loaded" event on the viewer allowing to modify a tile before it is marked ready to be drawn (#659) +* Added "tile-unloaded" event on the viewer allowing to free up memory one has allocated on a tile (#659) +* Added 'tile-load-failed' event (#725) +* Added additional coordinates conversion methods to TiledImage (#662) +* Added `preserveImageSizeOnResize` option (#666) +* Added collectionColumns as a configuration parameter (#680) +* Added option in addTiledImage to replace tiledImage at index (#706) +* Added autoRefigureSizes flag to World for optimizing mass rearrangements (#715) +* You can now change viewport margins after the viewer is created (#721) +* Added a patch to help slow down the scroll devices that fire too fast (#754) +* Fixed flickering tiles with useCanvas=false when no cache is used (#661) +* 'display: none' no longer gets reset on overlays during draw (#668) +* Better error reporting for tile load failures (#679) * Added XDomainRequest as fallback method for ajax requests if XMLHttpRequest fails (for IE < 10) (#693) * Now avoiding using eval when JSON.parse is available (#696) * Rotation now works properly on retina display (#708) -* Added option in addTiledImage to replace tiledImage at index (#706) -* Changed resize behaviour to prevent "snapping" to world bounds when constraints allow more space (#711) -* Registers as an AMD module where possible (#719) -* Added autoRefigureSizes flag to World for optimizing mass rearrangements (#715) -* Added 'tile-load-failed' event (#725) * Fixed issue with tiledImages loading tiles at every level instead of just the best level (#728) * Fixed placeholderFillStyle flicker (#727) * Fix for Chrome (v45) issue that key is sometimes undefined outside of the for-in loop (#730) * World.removeAll now cancels any in-flight image loads; same for Viewer.open and Viewer.close (#734) -* Optimized adding large numbers of items to the world with collectionMode (#735) * Fixed overlays position (use rounding instead of flooring and ceiling) (#741) * Fixed issue with including overlays in your tileSources array when creating/opening in the viewer (#745) * Fixed issue in iOS devices that would cause all touch events to fail after a Multitasking Gesture was triggered (#744) * Fixed an issue with TiledImage setPosition/setWidth/setHeight not reliably triggering a redraw (#720) -* You can now change viewport margins after the viewer is created (#721) +* Fixed zooming in with plus key on a Swedish keyboard (#763) 2.0.0: diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 00000000..422675b4 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,18 @@ +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=80 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project +auxiliary.org-netbeans-modules-editor-indent.text.javascript.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.indent-shift-width=2 +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.spaces-per-tab=2 +auxiliary.org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder=src +browser.autorefresh.Chromium.INTEGRATED=true +browser.highlightselection.Chromium.INTEGRATED=true +file.reference.openseadragon-openseadragon=. +files.encoding=UTF-8 +site.root.folder=${file.reference.openseadragon-openseadragon} +start.file=test/demo/basic.html +web.context.root=/ diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 00000000..53613f5c --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.web.clientproject + + + openseadragon + + + diff --git a/package.json b/package.json index d2081b58..43f837d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OpenSeadragon", - "version": "2.0.0", + "version": "2.1.0", "description": "Provides a smooth, zoomable user interface for HTML/Javascript.", "devDependencies": { "grunt": "^0.4.5", diff --git a/src/imagetilesource.js b/src/imagetilesource.js new file mode 100644 index 00000000..e077faa6 --- /dev/null +++ b/src/imagetilesource.js @@ -0,0 +1,288 @@ +/* + * OpenSeadragon - ImageTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function ($) { + + /** + * @class ImageTileSource + * @classdesc The ImageTileSource allows a simple image to be loaded + * into an OpenSeadragon Viewer. + * There are 2 ways to open an ImageTileSource: + * 1. viewer.open({type: 'image', url: fooUrl}); + * 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl})); + * + * With the first syntax, the crossOriginPolicy, ajaxWithCredentials and + * useCanvas options are inherited from the viewer if they are not + * specified directly in the options object. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Object} options Options object. + * @param {String} options.url URL of the image + * @param {Boolean} [options.buildPyramid=true] If set to true (default), a + * pyramid will be built internally to provide a better downsampling. + * @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are + * 'Anonymous', 'use-credentials', and false. If false, image requests will + * not use CORS preventing internal pyramid building for images from other + * domains. + * @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set + * the withCredentials XHR flag for AJAX requests (when loading tile sources). + * @param {Boolean} [options.useCanvas=true] Set to false to prevent any use + * of the canvas API. + */ + $.ImageTileSource = function (options) { + + options = $.extend({ + buildPyramid: true, + crossOriginPolicy: false, + ajaxWithCredentials: false, + useCanvas: true + }, options); + $.TileSource.apply(this, [options]); + + }; + + $.extend($.ImageTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ImageTileSource.prototype */{ + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function (data, url) { + return data.type && data.type === "image"; + }, + /** + * + * @function + * @param {Object} options - the options + * @param {String} dataUrl - the url the image was retreived from, if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function (options, dataUrl) { + return options; + }, + /** + * Responsible for retrieving, and caching the + * image metadata pertinent to this TileSources implementation. + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo: function (url) { + var image = this._image = new Image(); + var _this = this; + + if (this.crossOriginPolicy) { + image.crossOrigin = this.crossOriginPolicy; + } + if (this.ajaxWithCredentials) { + image.useCredentials = this.ajaxWithCredentials; + } + + $.addEvent(image, 'load', function () { + _this.width = image.naturalWidth; + _this.height = image.naturalHeight; + _this.aspectRatio = _this.width / _this.height; + _this.dimensions = new $.Point(_this.width, _this.height); + _this._tileWidth = _this.width; + _this._tileHeight = _this.height; + _this.tileOverlap = 0; + _this.minLevel = 0; + _this.levels = _this._buildLevels(); + _this.maxLevel = _this.levels.length - 1; + + _this.ready = true; + /** + * Raised when a TileSource is opened and initialized. + * + * @event ready + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference + * to the TileSource which raised the event. + * @property {Object} tileSource + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent('ready', {tileSource: _this}); + }); + + $.addEvent(image, 'error', function () { + /*** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference + * to the TileSource which raised the event. + * @property {String} message + * @property {String} source + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent('open-failed', { + message: "Error loading image at " + url, + source: url + }); + }); + + image.src = url; + }, + /** + * @function + * @param {Number} level + */ + getLevelScale: function (level) { + var levelScale = NaN; + if (level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + }, + /** + * @function + * @param {Number} level + */ + getNumTiles: function (level) { + var scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + }, + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function (level, point) { + return new $.Point(0, 0); + }, + /** + * Retrieves a tile url + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getTileUrl: function (level, x, y) { + var url = null; + if (level >= this.minLevel && level <= this.maxLevel) { + url = this.levels[level].url; + } + return url; + }, + /** + * Retrieves a tile context 2D + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getContext2D: function (level, x, y) { + var context = null; + if (level >= this.minLevel && level <= this.maxLevel) { + context = this.levels[level].context2D; + } + return context; + }, + + // private + // + // Builds the differents levels of the pyramid if possible + // (i.e. if canvas API enabled and no canvas tainting issue). + _buildLevels: function () { + var levels = [{ + url: this._image.src, + width: this._image.naturalWidth, + height: this._image.naturalHeight + }]; + + if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) { + // We don't need the image anymore. Allows it to be GC. + delete this._image; + return levels; + } + + var currentWidth = this._image.naturalWidth; + var currentHeight = this._image.naturalHeight; + + var bigCanvas = document.createElement("canvas"); + var bigContext = bigCanvas.getContext("2d"); + + bigCanvas.width = currentWidth; + bigCanvas.height = currentHeight; + bigContext.drawImage(this._image, 0, 0, currentWidth, currentHeight); + // We cache the context of the highest level because the browser + // is a lot faster at downsampling something it already has + // downsampled before. + levels[0].context2D = bigContext; + // We don't need the image anymore. Allows it to be GC. + delete this._image; + + if ($.isCanvasTainted(bigCanvas)) { + // If the canvas is tainted, we can't compute the pyramid. + return levels; + } + + // We build smaller levels until either width or height becomes + // 1 pixel wide. + while (currentWidth >= 2 && currentHeight >= 2) { + currentWidth = Math.floor(currentWidth / 2); + currentHeight = Math.floor(currentHeight / 2); + var smallCanvas = document.createElement("canvas"); + var smallContext = smallCanvas.getContext("2d"); + smallCanvas.width = currentWidth; + smallCanvas.height = currentHeight; + smallContext.drawImage(bigCanvas, 0, 0, currentWidth, currentHeight); + + levels.splice(0, 0, { + context2D: smallContext, + width: currentWidth, + height: currentHeight + }); + + bigCanvas = smallCanvas; + bigContext = smallContext; + } + return levels; + } + }); + +}(OpenSeadragon)); diff --git a/src/openseadragon.js b/src/openseadragon.js index b3b1bd6e..8613d2a8 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -260,6 +260,11 @@ * @property {Boolean} [preserveImageSizeOnResize=false] * Set to true to have the image size preserved when the viewer is resized. This requires autoResize=true (default). * + * @property {Number} [minScrollDeltaTime=50] + * Number of milliseconds between canvas-scroll events. This value helps normalize the rate of canvas-scroll + * events between different devices, causing the faster devices to slow down enough to make the zoom control + * more manageable. + * * @property {Number} [pixelsPerWheelLine=40] * For pixel-resolution scrolling devices, the number of pixels equal to one scroll line. * @@ -844,6 +849,23 @@ if (typeof define === 'function' && define.amd) { canvasElement.getContext( '2d' ) ); }()); + /** + * Test whether the submitted canvas is tainted or not. + * @argument {Canvas} canvas The canvas to test. + * @returns {Boolean} True if the canvas is tainted. + */ + $.isCanvasTainted = function(canvas) { + var isTainted = false; + try { + // We test if the canvas is tainted by retrieving data from it. + // An exception will be raised if the canvas is tainted. + var data = canvas.getContext('2d').getImageData(0, 0, 1, 1); + } catch (e) { + isTainted = true; + } + return isTainted; + }; + /** * 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. * @member {Number} pixelDensityRatio @@ -1009,6 +1031,7 @@ if (typeof define === 'function' && define.amd) { pixelsPerWheelLine: 40, autoResize: true, preserveImageSizeOnResize: false, // requires autoResize=true + minScrollDeltaTime: 50, //DEFAULT CONTROL SETTINGS showSequenceControl: true, //SEQUENCE diff --git a/src/tile.js b/src/tile.js index 12a33a22..7f598ef2 100644 --- a/src/tile.js +++ b/src/tile.js @@ -45,8 +45,10 @@ * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has * this tile failed to load? ) * @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. */ -$.Tile = function(level, x, y, bounds, exists, url) { +$.Tile = function(level, x, y, bounds, exists, url, context2D) { /** * The zoom level this tile belongs to. * @member {Number} level @@ -83,6 +85,12 @@ $.Tile = function(level, x, y, bounds, exists, url) { * @memberof OpenSeadragon.Tile# */ this.url = url; + /** + * The context2D of this tile if it is provided directly by the tile source. + * @member {CanvasRenderingContext2D} context2D + * @memberOf OpenSeadragon.Tile# + */ + this.context2D = context2D; /** * Is this tile loaded? * @member {Boolean} loaded @@ -249,14 +257,14 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ size = this.size.times($.pixelDensityRatio), rendered; - if (!this.cacheImageRecord) { + if (!this.context2D && !this.cacheImageRecord) { $.console.warn( '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString()); return; } - rendered = this.cacheImageRecord.getRenderedContext(); + rendered = this.context2D || this.cacheImageRecord.getRenderedContext(); if ( !this.loaded || !rendered ){ $.console.warn( @@ -275,7 +283,8 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ //ie its done fading or fading is turned off, and if we are drawing //an image with an alpha channel, then the only way //to avoid seeing the tile underneath is to clear the rectangle - if( context.globalAlpha == 1 && this.url.match('.png') ){ + if (context.globalAlpha === 1 && + (this.context2D || this.url.match('.png'))) { //clearing only the inside of the rectangle occupied //by the png prevents edge flikering context.clearRect( diff --git a/src/tiledimage.js b/src/tiledimage.js index f2059eba..b7b85995 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -797,7 +797,7 @@ function updateViewport( tiledImage ) { drawTiles( tiledImage, tiledImage.lastDrawn ); // Load the new 'best' tile - if ( best ) { + if (best && !best.context2D) { loadTile( tiledImage, best, currentTime ); } @@ -942,10 +942,14 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity ); if (!tile.loaded) { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); - if (imageRecord) { - var image = imageRecord.getImage(); - setTileLoaded(tiledImage, tile, image); + if (tile.context2D) { + setTileLoaded(tiledImage, tile); + } else { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + if (imageRecord) { + var image = imageRecord.getImage(); + setTileLoaded(tiledImage, tile, image); + } } } @@ -978,6 +982,7 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, + context2D, tile; if ( !tilesMatrix[ level ] ) { @@ -993,6 +998,8 @@ 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 ); + context2D = tileSource.getContext2D ? + tileSource.getContext2D(level, xMod, yMod) : undefined; bounds.x += ( x - xMod ) / numTiles.x; bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); @@ -1003,7 +1010,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid y, bounds, exists, - url + url, + context2D ); } @@ -1082,12 +1090,14 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { if (increment === 0) { tile.loading = false; tile.loaded = true; - tiledImage._tileCache.cacheTile({ - image: image, - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); + if (!tile.context2D) { + tiledImage._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); + } tiledImage._needsDraw = true; } } diff --git a/src/viewer.js b/src/viewer.js index 0851f0e6..6dfa5c7c 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -203,6 +203,8 @@ $.Viewer = function( options ) { this._loadQueue = []; this.currentOverlays = []; + this._lastScrollTime = $.now(); // variable used to help normalize the scroll event speed of different devices + //Inherit some behaviors and properties $.EventSource.call( this ); @@ -1286,19 +1288,21 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } } + if ($.isArray(options.tileSource)) { + setTimeout(function() { + raiseAddItemFailed({ + message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", + source: options.tileSource, + options: options + }); + }); + return; + } + this._loadQueue.push(myQueueItem); getTileSourceImplementation( this, options.tileSource, function( tileSource ) { - if ( tileSource instanceof Array ) { - raiseAddItemFailed({ - message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", - source: tileSource, - options: options - }); - return; - } - myQueueItem.tileSource = tileSource; // add everybody at the front of the queue that's ready to go @@ -2041,12 +2045,30 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, } } + function waitUntilReady(tileSource, originalTileSource) { + if (tileSource.ready) { + successCallback(tileSource); + } else { + tileSource.addHandler('ready', function () { + successCallback(tileSource); + }); + tileSource.addHandler('open-failed', function (event) { + failCallback({ + message: event.message, + source: originalTileSource + }); + }); + } + } + setTimeout( function() { if ( $.type( tileSource ) == 'string' ) { //If its still a string it means it must be a url at this point tileSource = new $.TileSource({ url: tileSource, + crossOriginPolicy: viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, + useCanvas: viewer.useCanvas, success: function( event ) { successCallback( event.tileSource ); } @@ -2055,10 +2077,16 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, failCallback( event ); } ); - } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { + if (!tileSource.crossOriginPolicy && viewer.crossOriginPolicy) { + tileSource.crossOriginPolicy = viewer.crossOriginPolicy; + } if (tileSource.ajaxWithCredentials === undefined) { tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials; } + if (tileSource.useCanvas === undefined) { + tileSource.useCanvas = viewer.useCanvas; + } if ( $.isFunction( tileSource.getTileUrl ) ) { //Custom tile source @@ -2076,14 +2104,13 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, return; } var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] ); - var readySource = new $TileSource( options ); - successCallback( readySource ); + waitUntilReady(new $TileSource(options), tileSource); } } else { //can assume it's already a tile source implementation - successCallback( tileSource ); + waitUntilReady(tileSource, tileSource); } - }, 1 ); + }); } function getOverlayObject( viewer, overlay ) { @@ -2294,6 +2321,7 @@ function onCanvasKeyDown( event ) { function onCanvasKeyPress( event ) { if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { switch( event.keyCode ){ + case 43://=|+ case 61://=|+ this.viewport.zoomBy(1.1); this.viewport.applyConstraints(); @@ -2738,44 +2766,57 @@ function onCanvasPinch( event ) { function onCanvasScroll( event ) { var gestureSettings, - factor; + factor, + thisScrollTime, + deltaScrollTime; - if ( !event.preventDefaultAction && this.viewport ) { - gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); - if ( gestureSettings.scrollToZoom ) { - factor = Math.pow( this.zoomPerScroll, event.scroll ); - this.viewport.zoomBy( - factor, - this.viewport.pointFromPixel( event.position, true ) - ); - this.viewport.applyConstraints(); + /* Certain scroll devices fire the scroll event way too fast so we are injecting a simple adjustment to keep things + * partially normalized. If we have already fired an event within the last 'minScrollDelta' milliseconds we skip + * this one and wait for the next event. */ + thisScrollTime = $.now(); + deltaScrollTime = thisScrollTime - this._lastScrollTime; + if (deltaScrollTime > this.minScrollDeltaTime) { + this._lastScrollTime = thisScrollTime; + + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.scrollToZoom ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewport.zoomBy( + factor, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + } + /** + * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). + * + * @event canvas-scroll + * @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 {Number} scroll - The scroll delta for the event. + * @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-scroll', { + tracker: event.eventSource, + position: event.position, + scroll: event.scroll, + shift: event.shift, + originalEvent: event.originalEvent + }); + if (gestureSettings && gestureSettings.scrollToZoom) { + //cancels event + return false; } } - /** - * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). - * - * @event canvas-scroll - * @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 {Number} scroll - The scroll delta for the event. - * @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-scroll', { - tracker: event.eventSource, - position: event.position, - scroll: event.scroll, - shift: event.shift, - originalEvent: event.originalEvent - }); - - if (gestureSettings && gestureSettings.scrollToZoom) { - //cancels event - return false; + else { + return false; // We are swallowing this event } } diff --git a/test/coverage.html b/test/coverage.html index 65cc2818..dc857202 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -32,6 +32,7 @@ + diff --git a/test/modules/basic.js b/test/modules/basic.js index eae33474..4ce56654 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -314,23 +314,11 @@ var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - callback(!isCanvasTainted(ctx)); + callback(!OpenSeadragon.isCanvasTainted(canvas)); }; img.src = corsImg; } - function isCanvasTainted(context) { - var isTainted = false; - try { - // We test if the canvas is tainted by retrieving data from it. - // An exception will be raised if the canvas is tainted. - var url = context.getImageData(0, 0, 1, 1); - } catch (e) { - isTainted = true; - } - return isTainted; - } - asyncTest( 'CrossOriginPolicyMissing', function () { viewer.crossOriginPolicy = false; @@ -344,7 +332,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(isCanvasTainted(viewer.drawer.context), "Canvas should be tainted."); + ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should be tainted."); start(); }); @@ -367,7 +356,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(!isCanvasTainted(viewer.drawer.context), "Canvas should not be tainted."); + ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should not be tainted."); start(); }); } diff --git a/test/modules/formats.js b/test/modules/formats.js index 2cef3576..5166f796 100644 --- a/test/modules/formats.js +++ b/test/modules/formats.js @@ -142,4 +142,12 @@ '}'); }); + // ---------- + asyncTest('ImageTileSource', function () { + testOpen({ + type: "image", + url: "/test/data/A.png" + }); + }); + })(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index c6381a20..e300b4a0 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -801,7 +801,6 @@ }); asyncTest('Item positions including collection mode', function() { - var navAddCount = 0; viewer = OpenSeadragon({ id: 'example', @@ -815,16 +814,16 @@ var openHandler = function() { viewer.removeHandler('open', openHandler); viewer.navigator.world.addHandler('add-item', navOpenHandler); + // The navigator may already have added the items. + navOpenHandler(); }; var navOpenHandler = function(event) { - navAddCount++; - if (navAddCount === 2) { + if (viewer.navigator.world.getItemCount() === 2) { viewer.navigator.world.removeHandler('add-item', navOpenHandler); setTimeout(function() { // Test initial formation - equal(viewer.navigator.world.getItemCount(), 2, 'navigator has both items'); for (var i = 0; i < 2; i++) { propEqual(viewer.navigator.world.getItemAt(i).getBounds(), viewer.world.getItemAt(i).getBounds(), 'bounds are the same');