diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..61b85e21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# editorconfig.org +root = true + +# We need to specify each folder specifically to avoid including test/lib and test/data +[{Gruntfile.js,src/**,test/*,test/demo/**,test/helpers/**,test/modules/**}] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,.travis.yml,.jshintrc}] +indent_style = space +indent_size = 2 diff --git a/.jshintrc b/.jshintrc index 2c3f7d32..e40e7d81 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,14 +1,14 @@ { - "browser": true, - "curly": true, - "eqeqeq": false, - "loopfunc": false, - "noarg": true, - "trailing": true, - "undef": true, - "unused": false, + "browser": true, + "curly": true, + "eqeqeq": false, + "loopfunc": false, + "noarg": true, + "trailing": true, + "undef": true, + "unused": false, - "globals": { - "OpenSeadragon": true - } + "globals": { + "OpenSeadragon": true + } } diff --git a/README.md b/README.md index 75fc5c92..efb8b4be 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The report shows up at `coverage/html/index.html` viewable in a browser. OpenSeadragon is truly a community project; we welcome your involvement! -When contributing, please attempt to match the code style already in the codebase. Note that we use four spaces per indentation stop. For more thoughts on code style, see https://github.com/rwldrn/idiomatic.js/. +When contributing, please attempt to match the code style already in the codebase. Note that we use four spaces per indentation stop. For easier setup you can also install [EditorConfig](http://editorconfig.org/) if your IDE is supported. For more thoughts on code style, see [idiomatic.js](https://github.com/rwldrn/idiomatic.js/). When fixing bugs and adding features, when appropriate please also: @@ -86,6 +86,6 @@ If you're new to open source in general, check out [GitHub's open source intro g ## License -OpenSeadragon is released under the New BSD license. For details, see the file LICENSE.txt. +OpenSeadragon is released under the New BSD license. For details, see the file LICENSE.txt. [![Build Status](https://secure.travis-ci.org/openseadragon/openseadragon.png?branch=master)](http://travis-ci.org/openseadragon/openseadragon) diff --git a/changelog.txt b/changelog.txt index 5ff344fb..a76881cb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,28 @@ OPENSEADRAGON CHANGELOG ======================= -1.3.0: (in progress) +2.1.0: (in progress) +* BREAKING CHANGE: the tile does not hold a reference to its image anymore. Only the tile cache keep a reference to images. +* 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) +* 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 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) + +2.0.0: * True multi-image mode (#450) * BREAKING CHANGE: Passing an array for the tileSources option is no longer enough to trigger sequence mode; you have to set the sequenceMode option to true as well @@ -37,6 +58,8 @@ OPENSEADRAGON CHANGELOG * Viewport.open supports positioning config properties * For multi-image open, drawing isn't started until all tileSources have been opened * You can specify a clip area for each image (only works on browsers that support the HTML5 canvas) (#594) + * Added placeholderFillStyle so image rectangles can be drawn even before their tiles load (#635) + * Ability to set opacity on individual TiledImages (#644) * Margins option to push the home region in from the edges of the Viewer (#505) * Rect and Point toString() functions are now consistent: rounding values to nearest hundredth * Overlays appear in the DOM immediately on open or addOverlay (#507) @@ -57,6 +80,8 @@ OPENSEADRAGON CHANGELOG * Fixed: Cross Origin policy not working (#613) * Optimized tile loading by clearing the queue on a re-draw when imageLoaderLimit is set (#616) * Now animating zoom spring exponentially (#631) +* Added http://editorconfig.org/ config file (#637) +* Keyboard pan speed is now the same regardless of zoom level (#645) 1.2.1: diff --git a/package.json b/package.json index 99e60e8a..d2081b58 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,20 @@ { "name": "OpenSeadragon", - "version": "1.2.1", + "version": "2.0.0", "description": "Provides a smooth, zoomable user interface for HTML/Javascript.", "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-clean": "^0.5.0", - "grunt-text-replace": "^0.3.11", + "grunt-contrib-compress": "^0.9.1", "grunt-contrib-concat": "^0.4.0", - "grunt-git-describe": "^2.3.2", + "grunt-contrib-connect": "^0.7.1", + "grunt-contrib-jshint": "^0.10.0", "grunt-contrib-uglify": "^0.4.0", "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-jshint": "^0.10.0", - "grunt-contrib-compress": "^0.9.1", - "grunt-contrib-connect": "^0.7.1", - "qunitjs": "^1.14.0", - "grunt-qunit-istanbul": "^0.4.5" + "grunt-git-describe": "^2.3.2", + "grunt-qunit-istanbul": "^0.5.0", + "grunt-text-replace": "^0.3.11", + "qunitjs": "^1.18.0" }, "scripts": { "test": "grunt test" diff --git a/src/buttongroup.js b/src/buttongroup.js index 49cc9c2e..77d61371 100644 --- a/src/buttongroup.js +++ b/src/buttongroup.js @@ -68,7 +68,7 @@ $.ButtonGroup = function( options ) { */ this.element = options.element || $.makeNeutralElement( "div" ); - // TODO What if there IS an options.group specified? + // TODO What if there IS an options.group specified? if( !options.group ){ this.label = $.makeNeutralElement( "label" ); //TODO: support labels for ButtonGroups diff --git a/src/drawer.js b/src/drawer.js index 1f18f102..9b74b981 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -42,11 +42,9 @@ * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. * @param {Element} options.element - Parent element. - * @param {Number} [options.opacity=1] - See opacity in {@link OpenSeadragon.Options} for details. * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. */ $.Drawer = function( options ) { - var _this = this; $.console.assert( options.viewer, "[Drawer] options.viewer is required" ); @@ -72,7 +70,9 @@ $.Drawer = function( options ) { this.viewer = options.viewer; this.viewport = options.viewport; this.debugGridColor = options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor; - this.opacity = options.opacity === undefined ? $.DEFAULT_SETTINGS.opacity : options.opacity; + if (options.opacity) { + $.console.error( "[Drawer] options.opacity is no longer accepted; set the opacity on the TiledImage instead" ); + } this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true ); /** @@ -96,6 +96,13 @@ $.Drawer = function( options ) { */ this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null; + /** + * Sketch canvas used to temporarily draw tiles which cannot be drawn directly + * to the main canvas due to opacity. Lazily initialized. + */ + this.sketchCanvas = null; + this.sketchContext = null; + /** * @member {Element} element * @memberof OpenSeadragon.Drawer# @@ -160,8 +167,11 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @return {OpenSeadragon.Drawer} Chainable. */ setOpacity: function( opacity ) { - this.opacity = opacity; - $.setElementOpacity( this.canvas, this.opacity, true ); + $.console.error("drawer.setOpacity is deprecated. Use tiledImage.setOpacity instead."); + var world = this.viewer.world; + for (var i = 0; i < world.getItemCount(); i++) { + world.getItemAt( i ).setOpacity( opacity ); + } return this; }, @@ -170,7 +180,16 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @returns {Number} */ getOpacity: function() { - return this.opacity; + $.console.error("drawer.getOpacity is deprecated. Use tiledImage.getOpacity instead."); + var world = this.viewer.world; + var maxOpacity = 0; + for (var i = 0; i < world.getItemCount(); i++) { + var opacity = world.getItemAt( i ).getOpacity(); + if ( opacity > maxOpacity ) { + maxOpacity = opacity; + } + } + return maxOpacity; }, // deprecated @@ -214,6 +233,8 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed) this.canvas.width = 1; this.canvas.height = 1; + this.sketchCanvas = null; + this.sketchContext = null; }, /** @@ -227,189 +248,260 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ this.canvas.height != viewportSize.y ) { this.canvas.width = viewportSize.x; this.canvas.height = viewportSize.y; + if ( this.sketchCanvas !== null ) { + this.sketchCanvas.width = this.canvas.width; + this.sketchCanvas.height = this.canvas.height; + } } - this.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); + this._clear(); } }, + _clear: function ( useSketch ) { + if ( !this.useCanvas ) { + return; + } + var context = this._getContext( useSketch ); + var canvas = context.canvas; + context.clearRect( 0, 0, canvas.width, canvas.height ); + }, + + /** + * Translates from OpenSeadragon viewer rectangle to drawer rectangle. + * @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system. + * @return {OpenSeadragon.Rect} Rectangle in drawer coordinate system. + */ + viewportToDrawerRectangle: function(rectangle) { + var topLeft = this.viewport.pixelFromPoint(rectangle.getTopLeft(), true); + var size = this.viewport.deltaPixelsFromPoints(rectangle.getSize(), true); + + return new $.Rect( + topLeft.x * $.pixelDensityRatio, + topLeft.y * $.pixelDensityRatio, + size.x * $.pixelDensityRatio, + size.y * $.pixelDensityRatio + ); + }, + /** * Draws the given tile. * @param {OpenSeadragon.Tile} tile - The tile to draw. * @param {Function} drawingHandler - Method for firing the drawing event if using canvas. * drawingHandler({context, tile, rendered}) + * @param {Boolean} useSketch - Whether to use the sketch canvas or not. * where rendered is the context with the pre-drawn image. */ - drawTile: function( tile, drawingHandler ) { + drawTile: function( tile, drawingHandler, useSketch ) { $.console.assert(tile, '[Drawer.drawTile] tile is required'); $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); if ( this.useCanvas ) { + var context = this._getContext( useSketch ); // TODO do this in a more performant way // specifically, don't save,rotate,restore every time we draw a tile if( this.viewport.degrees !== 0 ) { - this._offsetForRotation( tile, this.viewport.degrees ); - tile.drawCanvas( this.context, drawingHandler ); - this._restoreRotationChanges( tile ); + this._offsetForRotation( tile, this.viewport.degrees, useSketch ); + tile.drawCanvas( context, drawingHandler ); + this._restoreRotationChanges( tile, useSketch ); } else { - tile.drawCanvas( this.context, drawingHandler ); + tile.drawCanvas( context, drawingHandler ); } } else { tile.drawHTML( this.canvas ); } }, + _getContext: function( useSketch ) { + var context = this.context; + if ( useSketch ) { + if (this.sketchCanvas === null) { + this.sketchCanvas = document.createElement( "canvas" ); + this.sketchCanvas.width = this.canvas.width; + this.sketchCanvas.height = this.canvas.height; + this.sketchContext = this.sketchCanvas.getContext( "2d" ); + } + context = this.sketchContext; + } + return context; + }, + // private - saveContext: function() { + saveContext: function( useSketch ) { if (!this.useCanvas) { return; } + this._getContext( useSketch ).save(); + }, + + // private + restoreContext: function( useSketch ) { + if (!this.useCanvas) { + return; + } + + this._getContext( useSketch ).restore(); + }, + + // private + setClip: function(rect, useSketch) { + if (!this.useCanvas) { + return; + } + + var context = this._getContext( useSketch ); + context.beginPath(); + context.rect(rect.x, rect.y, rect.width, rect.height); + context.clip(); + }, + + // private + drawRectangle: function(rect, fillStyle, useSketch) { + if (!this.useCanvas) { + return; + } + + var context = this._getContext( useSketch ); + context.save(); + context.fillStyle = fillStyle; + context.fillRect(rect.x, rect.y, rect.width, rect.height); + context.restore(); + }, + + /** + * Blends the sketch canvas in the main canvas. + * @param {Float} opacity The opacity of the blending. + * @returns {undefined} + */ + blendSketch: function(opacity) { + if (!this.useCanvas || !this.sketchCanvas) { + return; + } + this.context.save(); - }, - - // private - restoreContext: function() { - if (!this.useCanvas) { - return; - } - + this.context.globalAlpha = opacity; + this.context.drawImage(this.sketchCanvas, 0, 0); this.context.restore(); }, // private - setClip: function(rect) { - if (!this.useCanvas) { + drawDebugInfo: function( tile, count, i ){ + if ( !this.useCanvas ) { return; } - this.context.beginPath(); - this.context.rect(rect.x, rect.y, rect.width, rect.height); - this.context.clip(); - }, + var context = this.context; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; + context.strokeStyle = this.debugGridColor; + context.fillStyle = this.debugGridColor; - // private - drawDebugInfo: function( tile, count, i ){ - if ( this.useCanvas ) { - this.context.save(); - this.context.lineWidth = 2 * $.pixelDensityRatio; - this.context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; - this.context.strokeStyle = this.debugGridColor; - this.context.fillStyle = this.debugGridColor; - - if ( this.viewport.degrees !== 0 ) { - this._offsetForRotation( tile, this.canvas, this.context, this.viewport.degrees ); - } - - this.context.strokeRect( - tile.position.x * $.pixelDensityRatio, - tile.position.y * $.pixelDensityRatio, - tile.size.x * $.pixelDensityRatio, - tile.size.y * $.pixelDensityRatio - ); - - var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; - var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; - - // Rotate the text the right way around. - this.context.translate( tileCenterX, tileCenterY ); - this.context.rotate( Math.PI / 180 * -this.viewport.degrees ); - this.context.translate( -tileCenterX, -tileCenterY ); - - if( tile.x === 0 && tile.y === 0 ){ - this.context.fillText( - "Zoom: " + this.viewport.getZoom(), - tile.position.x * $.pixelDensityRatio, - (tile.position.y - 30) * $.pixelDensityRatio - ); - this.context.fillText( - "Pan: " + this.viewport.getBounds().toString(), - tile.position.x * $.pixelDensityRatio, - (tile.position.y - 20) * $.pixelDensityRatio - ); - } - this.context.fillText( - "Level: " + tile.level, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 20) * $.pixelDensityRatio - ); - this.context.fillText( - "Column: " + tile.x, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 30) * $.pixelDensityRatio - ); - this.context.fillText( - "Row: " + tile.y, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 40) * $.pixelDensityRatio - ); - this.context.fillText( - "Order: " + i + " of " + count, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 50) * $.pixelDensityRatio - ); - this.context.fillText( - "Size: " + tile.size.toString(), - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 60) * $.pixelDensityRatio - ); - this.context.fillText( - "Position: " + tile.position.toString(), - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 70) * $.pixelDensityRatio - ); - - if ( this.viewport.degrees !== 0 ) { - this._restoreRotationChanges( tile, this.canvas, this.context ); - } - this.context.restore(); + if ( this.viewport.degrees !== 0 ) { + this._offsetForRotation( tile, this.viewport.degrees ); } + + context.strokeRect( + tile.position.x * $.pixelDensityRatio, + tile.position.y * $.pixelDensityRatio, + tile.size.x * $.pixelDensityRatio, + tile.size.y * $.pixelDensityRatio + ); + + var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; + var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; + + // Rotate the text the right way around. + context.translate( tileCenterX, tileCenterY ); + context.rotate( Math.PI / 180 * -this.viewport.degrees ); + context.translate( -tileCenterX, -tileCenterY ); + + if( tile.x === 0 && tile.y === 0 ){ + context.fillText( + "Zoom: " + this.viewport.getZoom(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 30) * $.pixelDensityRatio + ); + context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 20) * $.pixelDensityRatio + ); + } + context.fillText( + "Level: " + tile.level, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 20) * $.pixelDensityRatio + ); + context.fillText( + "Column: " + tile.x, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 30) * $.pixelDensityRatio + ); + context.fillText( + "Row: " + tile.y, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 40) * $.pixelDensityRatio + ); + context.fillText( + "Order: " + i + " of " + count, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 50) * $.pixelDensityRatio + ); + context.fillText( + "Size: " + tile.size.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 60) * $.pixelDensityRatio + ); + context.fillText( + "Position: " + tile.position.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 70) * $.pixelDensityRatio + ); + + if ( this.viewport.degrees !== 0 ) { + this._restoreRotationChanges( tile ); + } + context.restore(); }, // private debugRect: function(rect) { if ( this.useCanvas ) { - this.context.save(); - this.context.lineWidth = 2 * $.pixelDensityRatio; - this.context.strokeStyle = this.debugGridColor; - this.context.fillStyle = this.debugGridColor; + var context = this.context; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.strokeStyle = this.debugGridColor; + context.fillStyle = this.debugGridColor; - this.context.strokeRect( + context.strokeRect( rect.x * $.pixelDensityRatio, rect.y * $.pixelDensityRatio, rect.width * $.pixelDensityRatio, rect.height * $.pixelDensityRatio ); - this.context.restore(); + context.restore(); } }, // private - _offsetForRotation: function( tile, degrees ){ + _offsetForRotation: function( tile, degrees, useSketch ){ var cx = this.canvas.width / 2, - cy = this.canvas.height / 2, - px = tile.position.x - cx, - py = tile.position.y - cy; + cy = this.canvas.height / 2; - this.context.save(); + var context = this._getContext( useSketch ); + context.save(); - this.context.translate(cx, cy); - this.context.rotate( Math.PI / 180 * degrees); - tile.position.x = px; - tile.position.y = py; + context.translate(cx, cy); + context.rotate( Math.PI / 180 * degrees); + context.translate(-cx, -cy); }, // private - _restoreRotationChanges: function( tile ){ - var cx = this.canvas.width / 2, - cy = this.canvas.height / 2, - px = tile.position.x + cx, - py = tile.position.y + cy; - - tile.position.x = px; - tile.position.y = py; - - this.context.restore(); + _restoreRotationChanges: function( tile, useSketch ){ + var context = this._getContext( useSketch ); + context.restore(); }, // private diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index ca755b9f..bf3da020 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -55,14 +55,19 @@ $.IIIFTileSource = function( options ){ options.tileSizePerScaleFactor = {}; // N.B. 2.0 renamed scale_factors to scaleFactors - if ( this.tile_width ) { + if ( this.tile_width && this.tile_height ) { + options.tileWidth = this.tile_width; + options.tileHeight = this.tile_height; + } else if ( this.tile_width ) { options.tileSize = this.tile_width; } else if ( this.tile_height ) { options.tileSize = this.tile_height; } else if ( this.tiles ) { // Version 2.0 forwards if ( this.tiles.length == 1 ) { - options.tileSize = this.tiles[0].width; + options.tileWidth = this.tiles[0].width; + // Use height if provided, otherwise assume square tiles and use width. + options.tileHeight = this.tiles[0].height || this.tiles[0].width; this.scale_factors = this.tiles[0].scaleFactors; } else { // Multiple tile sizes at different levels @@ -71,13 +76,15 @@ $.IIIFTileSource = function( options ){ for (var sf = 0; sf < this.tiles[t].scaleFactors.length; sf++) { var scaleFactor = this.tiles[t].scaleFactors[sf]; this.scale_factors.push(scaleFactor); - options.tileSizePerScaleFactor[scaleFactor] = this.tiles[t].width; + options.tileSizePerScaleFactor[scaleFactor] = { + width: this.tiles[t].width, + height: this.tiles[t].height || this.tiles[t].width + }; } } } } else { // use the largest of tileOptions that is smaller than the short dimension - var shortDim = Math.min( this.height, this.width ), tileOptions = [256,512,1024], smallerTiles = []; @@ -94,8 +101,6 @@ $.IIIFTileSource = function( options ){ // If we're smaller than 256, just use the short side. options.tileSize = shortDim; } - this.tile_width = options.tileSize; // So that 'full' gets used for - this.tile_height = options.tileSize; // the region below } if ( !options.maxLevel ) { @@ -117,6 +122,7 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea * @param {Object|Array} data * @param {String} optional - url */ + supports: function( data, url ) { // Version 2.0 and forwards if (data.protocol && data.protocol == 'http://iiif.io/api/image') { @@ -181,20 +187,34 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea }, /** - * Return the tileSize for the given level. + * Return the tileWidth for the given level. * @function * @param {Number} level - */ - - getTileSize: function( level ){ + */ + getTileWidth: function( level ) { var scaleFactor = Math.pow(2, this.maxLevel - level); - // cache it in case any external code is going to read it directly + if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { - this.tileSize = this.tileSizePerScaleFactor[scaleFactor]; + return this.tileSizePerScaleFactor[scaleFactor].width; } - return this.tileSize; + return this._tileWidth; }, + /** + * Return the tileHeight for the given level. + * @function + * @param {Number} level + */ + getTileHeight: function( level ) { + var scaleFactor = Math.pow(2, this.maxLevel - level); + + if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { + return this.tileSizePerScaleFactor[scaleFactor].height; + } + return this._tileHeight; + }, + + /** * Responsible for retreiving the url which will return an image for the * region specified by the given x, y, and level components. @@ -216,7 +236,8 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea levelHeight = Math.ceil( this.height * scale ), //## iiif region - tileSize, + tileWidth, + tileHeight, iiifTileSizeWidth, iiifTileSizeHeight, iiifRegion, @@ -228,9 +249,10 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea iiifQuality, uri; - tileSize = this.getTileSize(level); - iiifTileSizeWidth = Math.ceil( tileSize / scale ); - iiifTileSizeHeight = iiifTileSizeWidth; + tileWidth = this.getTileWidth(level); + tileHeight = this.getTileHeight(level); + iiifTileSizeWidth = Math.ceil( tileWidth / scale ); + iiifTileSizeHeight = Math.ceil( tileHeight / scale ); if ( this['@context'].indexOf('/1.0/context.json') > -1 || this['@context'].indexOf('/1.1/context.json') > -1 || @@ -240,7 +262,7 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea iiifQuality = "default.jpg"; } - if ( levelWidth < tileSize && levelHeight < tileSize ){ + if ( levelWidth < tileWidth && levelHeight < tileHeight ){ iiifSize = levelWidth + ","; iiifRegion = 'full'; } else { diff --git a/src/imageloader.js b/src/imageloader.js index a61c8936..ef3a4f4f 100644 --- a/src/imageloader.js +++ b/src/imageloader.js @@ -51,6 +51,7 @@ function ImageJob ( options ) { } ImageJob.prototype = { + errorMsg: null, start: function(){ var _this = this; @@ -64,10 +65,12 @@ ImageJob.prototype = { _this.finish( true ); }; this.image.onabort = this.image.onerror = function(){ + _this.errorMsg = "Image load aborted"; _this.finish( false ); }; this.jobId = window.setTimeout( function(){ + _this.errorMsg = "Image load exceeded timeout"; _this.finish( false ); }, this.timeout); @@ -173,7 +176,7 @@ function completeJob( loader, job, callback ) { loader.jobsInProgress++; } - callback( job.image ); + callback( job.image, job.errorMsg ); } }( OpenSeadragon )); diff --git a/src/mousetracker.js b/src/mousetracker.js index ec677fb7..e9b5acfc 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -133,7 +133,7 @@ */ this.element = $.getElement( options.element ); /** - * The number of milliseconds within which a pointer down-up event combination + * The number of milliseconds within which a pointer down-up event combination * will be treated as a click gesture. * @member {Number} clickTimeThreshold * @memberof OpenSeadragon.MouseTracker# @@ -244,7 +244,7 @@ // Active pointers lists. Array of GesturePointList objects, one for each pointer device type. // GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()). - // Active pointers are any pointer being tracked for this element which are in the hit-test area + // Active pointers are any pointer being tracked for this element which are in the hit-test area // of the element (for hover-capable devices) and/or have contact or a button press initiated in the element. activePointersLists: [], @@ -1032,7 +1032,7 @@ $.MouseTracker.mousePointerId = "legacy-mouse"; $.MouseTracker.maxTouchPoints = 10; } - + /////////////////////////////////////////////////////////////////////////////// // Classes and typedefs @@ -1078,7 +1078,7 @@ /** * @class GesturePointList * @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type. - * Active pointers are any pointer being tracked for this element which are in the hit-test area + * Active pointers are any pointer being tracked for this element which are in the hit-test area * of the element (for hover-capable devices) and/or have contact or a button press initiated in the element. * @memberof OpenSeadragon.MouseTracker * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. @@ -1198,7 +1198,7 @@ return null; } }; - + /////////////////////////////////////////////////////////////////////////////// // Utility functions @@ -1282,7 +1282,7 @@ false ); } - + clearTrackedPointers( tracker ); delegate.tracking = true; @@ -1694,7 +1694,7 @@ /** - * Handles 'wheel' events. + * Handles 'wheel' events. * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()). * * @private @@ -1943,7 +1943,7 @@ handleMouseMove( tracker, event ); } - + /** * This handler is attached to the window object (on the capture phase) to emulate mouse capture. * onMouseMove is still attached to the tracked element, so stop propagation to avoid processing twice. @@ -2191,7 +2191,7 @@ var i, touchCount = event.changedTouches.length, gPoints = []; - + for ( i = 0; i < touchCount; i++ ) { gPoints.push( { id: event.changedTouches[ i ].identifier, @@ -2420,7 +2420,7 @@ */ function startTrackingPointer( pointsList, gPoint ) { - // If isPrimary is not known for the pointer then set it according to our rules: + // If isPrimary is not known for the pointer then set it according to our rules: // true if the first pointer in the gesture, otherwise false if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) { if ( pointsList.getLength() === 0 ) { @@ -2617,7 +2617,7 @@ * Gesture points associated with the event. * @param {Number} buttonChanged * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser. - * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, + * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events. * * @returns {Boolean} True if pointers should be captured to the tracked element, otherwise false. @@ -2779,7 +2779,7 @@ * Gesture points associated with the event. * @param {Number} buttonChanged * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser. - * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, + * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events. * * @returns {Boolean} True if pointer capture should be released from the tracked element, otherwise false. diff --git a/src/navigator.js b/src/navigator.js index 9f3923df..7addc5ea 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -108,7 +108,9 @@ $.Navigator = function( options ){ immediateRender: true, blendTime: 0, animationTime: 0, - autoResize: options.autoResize + autoResize: options.autoResize, + // prevent resizing the navigator from adding unwanted space around the image + minZoomImageRatio: 1.0 }); options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio; diff --git a/src/openseadragon.js b/src/openseadragon.js index 8d231cbe..e457a5c1 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -204,7 +204,12 @@ * If 0, adjusts to fit viewer. * * @property {Number} [opacity=1] - * Opacity of the drawer (1=opaque, 0=transparent) + * Default opacity of the tiled images (1=opaque, 0=transparent) + * + * @property {String|CanvasGradient|CanvasPattern|Function} [placeholderFillStyle=null] + * Draws a colored rectangle behind the tile if it is not loaded yet. + * You can pass a CSS color value like "#FF8800". + * When passing a function the tiledImage and canvas context are available as argument which is useful when you draw a gradient or pattern. * * @property {Number} [degrees=0] * Initial rotation. @@ -236,7 +241,7 @@ * @property {Number} [minZoomImageRatio=0.9] * The minimum percentage ( expressed as a number between 0 and 1 ) of * the viewport height or width at which the zoom out will be constrained. - * Setting it to 0, for example will allow you to zoom out infinitly. + * Setting it to 0, for example will allow you to zoom out infinity. * * @property {Number} [maxZoomPixelRatio=1.1] * The maximum ratio to allow a zoom-in to affect the highest level pixel @@ -247,6 +252,9 @@ * @property {Boolean} [autoResize=true] * Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior. * + * @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} [pixelsPerWheelLine=40] * For pixel-resolution scrolling devices, the number of pixels equal to one scroll line. * @@ -262,7 +270,7 @@ * Possible subproperties (Numbers, in screen coordinates): left, top, right, bottom. * * @property {Number} [imageLoaderLimit=0] - * The maximum number of image requests to make concurrently. By default + * The maximum number of image requests to make concurrently. By default * it is set to 0 allowing the browser to make the maximum number of * image requests in parallel as allowed by the browsers policy. * @@ -348,7 +356,7 @@ * @property {Boolean} [showNavigator=false] * Set to true to make the navigator minimap appear. * - * @property {Boolean} [navigatorId=navigator-GENERATED DATE] + * @property {String} [navigatorId=navigator-GENERATED DATE] * The ID of a div to hold the navigator minimap. * If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, and navigatorTop|Left|Height|Width options will be ignored. * If an ID is not specified, a div element will be generated and placed on top of the main image. @@ -551,6 +559,10 @@ * If collectionMode is true, specifies how many rows the grid should have. Use 1 to make a line. * If collectionLayout is 'vertical', specifies how many columns instead. * + * @property {Number} [collectionColumns=0] + * If collectionMode is true, specifies how many columns the grid should have. Use 1 to make a line. + * If collectionLayout is 'vertical', specifies how many rows instead. Ignored if collectionRows is not set to a falsy value. + * * @property {String} [collectionLayout='horizontal'] * If collectionMode is true, specifies whether to arrange vertically or horizontally. * @@ -982,6 +994,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ maxZoomPixelRatio: 1.1, //-> higher allows 'over zoom' into pixels pixelsPerWheelLine: 40, autoResize: true, + preserveImageSizeOnResize: false, // requires autoResize=true //DEFAULT CONTROL SETTINGS showSequenceControl: true, //SEQUENCE @@ -1013,10 +1026,11 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ navigatorRotate: true, // INITIAL ROTATION - degrees: 0, + degrees: 0, // APPEARANCE - opacity: 1, + opacity: 1, + placeholderFillStyle: null, //REFERENCE STRIP SETTINGS showReferenceStrip: false, @@ -1029,6 +1043,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ //COLLECTION VISUALIZATION SETTINGS collectionRows: 3, //or columns depending on layout + collectionColumns: 0, //columns in horizontal layout, rows in vertical layout collectionLayout: 'horizontal', //vertical collectionMode: false, collectionTileSize: 800, @@ -2030,8 +2045,40 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ request.onreadystatechange = function(){}; - if ( $.isFunction( onError ) ) { - onError( request, e ); + if (window.XDomainRequest) { // IE9 or IE8 might as well try to use XDomainRequest + var xdr = new XDomainRequest(); + if (xdr) { + xdr.onload = function (e) { + if ( $.isFunction( onSuccess ) ) { + onSuccess({ // Faking an xhr object + responseText: xdr.responseText, + status: 200, // XDomainRequest doesn't support status codes, so we just fake one! :/ + statusText: 'OK' + }); + } + }; + xdr.onerror = function (e) { + if ( $.isFunction ( onError ) ) { + onError({ // Faking an xhr object + responseText: xdr.responseText, + status: 444, // 444 No Response + statusText: 'An error happened. Due to an XDomainRequest deficiency we can not extract any information about this error. Upgrade your browser.' + }); + } + }; + try { + xdr.open('GET', url); + xdr.send(); + } catch (e2) { + if ( $.isFunction( onError ) ) { + onError( request, e ); + } + } + } + } else { + if ( $.isFunction( onError ) ) { + onError( request, e ); + } } } }, @@ -2161,6 +2208,24 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ return $.parseXml( string ); }, + /** + * Parses a JSON string into a Javascript object. + * @function + * @param {String} string + * @returns {Object} + */ + parseJSON: function(string) { + if (window.JSON && window.JSON.parse) { + $.parseJSON = window.JSON.parse; + } else { + // Should only be used by IE8 in non standards mode + $.parseJSON = function(string) { + /*jshint evil:true*/ + return eval('(' + string + ')'); + }; + } + return $.parseJSON(string); + }, /** * Reports whether the image format is supported for tiling in this diff --git a/src/overlay.js b/src/overlay.js index 0bdbbf34..0484ead6 100644 --- a/src/overlay.js +++ b/src/overlay.js @@ -282,7 +282,10 @@ style.left = position.x + "px"; style.top = position.y + "px"; style.position = "absolute"; - style.display = 'block'; + + if (style.display != 'none') { + style.display = 'block'; + } if ( scales ) { style.width = size.x + "px"; diff --git a/src/tile.js b/src/tile.js index 756d9d4a..48f3503c 100644 --- a/src/tile.js +++ b/src/tile.js @@ -67,7 +67,7 @@ $.Tile = function(level, x, y, bounds, exists, url) { this.y = y; /** * Where this tile fits, in normalized coordinates - * @member {OpenSeadragon.Point} bounds + * @member {OpenSeadragon.Rect} bounds * @memberof OpenSeadragon.Tile# */ this.bounds = bounds; @@ -190,7 +190,14 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ * @param {Element} container */ drawHTML: function( container ) { - if ( !this.loaded || !this.image ) { + if (!this.cacheImageRecord) { + $.console.warn( + '[Tile.drawHTML] attempting to draw tile %s when it\'s not cached', + this.toString()); + return; + } + + if ( !this.loaded ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -203,8 +210,7 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ if ( !this.element ) { this.element = $.makeNeutralElement( "div" ); - this.imgElement = $.makeNeutralElement( "img" ); - this.imgElement.src = this.url; + this.imgElement = this.cacheImageRecord.getImage().cloneNode(); this.imgElement.style.msInterpolationMode = "nearest-neighbor"; this.imgElement.style.width = "100%"; this.imgElement.style.height = "100%"; @@ -239,17 +245,18 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ var position = this.position, size = this.size, - rendered, - canvas; + rendered; if (!this.cacheImageRecord) { - $.console.warn('[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString()); + $.console.warn( + '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', + this.toString()); return; } rendered = this.cacheImageRecord.getRenderedContext(); - if ( !this.loaded || !( this.image || rendered) ){ + if ( !this.loaded || !rendered ){ $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -276,19 +283,8 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ } - if(!rendered){ - canvas = document.createElement( 'canvas' ); - canvas.width = this.image.width; - canvas.height = this.image.height; - rendered = canvas.getContext('2d'); - rendered.drawImage( this.image, 0, 0 ); - this.cacheImageRecord.setRenderedContext(rendered); - //since we are caching the prerendered image on a canvas - //allow the image to not be held in memory - this.image = null; - } - - // This gives the application a chance to make image manipulation changes as we are rendering the image + // This gives the application a chance to make image manipulation + // changes as we are rendering the image drawingHandler({context: context, tile: this, rendered: rendered}); context.drawImage( @@ -318,7 +314,6 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ this.element = null; this.imgElement = null; - this.image = null; this.loaded = false; this.loading = false; } diff --git a/src/tilecache.js b/src/tilecache.js index 45344a05..281709c8 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -63,10 +63,23 @@ ImageRecord.prototype = { }, getRenderedContext: function() { + if (!this._renderedContext) { + var canvas = document.createElement( 'canvas' ); + canvas.width = this._image.width; + canvas.height = this._image.height; + this._renderedContext = canvas.getContext('2d'); + this._renderedContext.drawImage( this._image, 0, 0 ); + //since we are caching the prerendered image on a canvas + //allow the image to not be held in memory + this._image = null; + } return this._renderedContext; }, setRenderedContext: function(renderedContext) { + $.console.error("ImageRecord.setRenderedContext is deprecated. " + + "The rendered context should be created by the ImageRecord " + + "itself when calling ImageRecord.getRenderedContext."); this._renderedContext = renderedContext; }, @@ -126,6 +139,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.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 {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 * function will release an old tile. The cutoff option specifies a tile level at or below which @@ -135,7 +149,6 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ $.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.image, "[TileCache.cacheTile] options.tile.image is required" ); $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" ); var cutoff = options.cutoff || 0; @@ -143,8 +156,9 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ var imageRecord = this._imagesLoaded[options.tile.url]; if (!imageRecord) { + $.console.assert( options.image, "[TileCache.cacheTile] options.image is required to create an ImageRecord" ); imageRecord = this._imagesLoaded[options.tile.url] = new ImageRecord({ - image: options.tile.image + image: options.image }); this._imagesLoadedCount++; @@ -158,6 +172,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ if ( this._imagesLoadedCount > this._maxImageCacheCount ) { var worstTile = null; var worstTileIndex = -1; + var worstTileRecord = null; var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord; for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) { @@ -169,6 +184,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ } else if ( !worstTile ) { worstTile = prevTile; worstTileIndex = i; + worstTileRecord = prevTileRecord; continue; } @@ -181,11 +197,12 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ ( prevTime == worstTime && prevLevel > worstLevel ) ) { worstTile = prevTile; worstTileIndex = i; + worstTileRecord = prevTileRecord; } } if ( worstTile && worstTileIndex >= 0 ) { - this._unloadTile(worstTile); + this._unloadTile(worstTileRecord); insertionIndex = worstTileIndex; } } @@ -206,7 +223,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ for ( var i = 0; i < this._tilesLoaded.length; ++i ) { tileRecord = this._tilesLoaded[ i ]; if ( tileRecord.tiledImage === tiledImage ) { - this._unloadTile(tileRecord.tile); + this._unloadTile(tileRecord); this._tilesLoaded.splice( i, 1 ); i--; } @@ -220,8 +237,11 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ }, // private - _unloadTile: function(tile) { - $.console.assert(tile, '[TileCache._unloadTile] tile is required'); + _unloadTile: function(tileRecord) { + $.console.assert(tileRecord, '[TileCache._unloadTile] tileRecord is required'); + var tile = tileRecord.tile; + var tiledImage = tileRecord.tiledImage; + tile.unload(); tile.cacheImageRecord = null; @@ -232,6 +252,20 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ delete this._imagesLoaded[tile.url]; this._imagesLoadedCount--; } + + /** + * Triggered when a tile has just been unloaded from memory. + * + * @event tile-unloaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the unloaded tile. + * @property {OpenSeadragon.Tile} tile - The tile which has been unloaded. + */ + tiledImage.viewer.raiseEvent("tile-unloaded", { + tile: tile, + tiledImage: tiledImage + }); } }; diff --git a/src/tiledimage.js b/src/tiledimage.js index 405989b7..4aaef1db 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -64,7 +64,9 @@ * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. * @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}. */ $.TiledImage = function( options ) { @@ -126,21 +128,23 @@ $.TiledImage = function( options ) { coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. lastDrawn: [], // An unordered list of Tiles drawn last frame. lastResetTime: 0, // Last time for which the tiledImage was reset. - _midDraw: false, // Is the tiledImage currently updating the viewport? - _needsDraw: true, // Does the tiledImage need to update the viewport again? + _midDraw: false, // Is the tiledImage currently updating the viewport? + _needsDraw: true, // Does the tiledImage need to update the viewport again? //configurable settings - springStiffness: $.DEFAULT_SETTINGS.springStiffness, - animationTime: $.DEFAULT_SETTINGS.animationTime, - minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, - wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, - wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, - immediateRender: $.DEFAULT_SETTINGS.immediateRender, - blendTime: $.DEFAULT_SETTINGS.blendTime, - alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, - minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, - debugMode: $.DEFAULT_SETTINGS.debugMode, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity }, options ); @@ -402,6 +406,83 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag ); }, + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToImageCoordinates: function( pixel ) { + var point = this.viewport.pointFromPixel( pixel, true ); + return this.viewportToImageCoordinates( point ); + }, + + /** + * Convert pixel coordinates relative to the image to + * viewer element coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( pixel ) { + var point = this.imageToViewportCoordinates( pixel ); + return this.viewport.pixelFromPoint( point, true ); + }, + + /** + * Convert pixel coordinates relative to the window to image coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToImageCoordinates: function( pixel ) { + var viewerCoordinates = pixel.minus( + OpenSeadragon.getElementPosition( this.viewer.element )); + return this.viewerElementToImageCoordinates( viewerCoordinates ); + }, + + /** + * Convert image coordinates to pixel coordinates relative to the window. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToWindowCoordinates: function( pixel ) { + var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); + return viewerCoordinates.plus( + OpenSeadragon.getElementPosition( this.viewer.element )); + }, + + /** + * Convert a viewport zoom to an image zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * @function + * @param {Number} viewportZoom The viewport zoom + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function( viewportZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return ratio * viewportZoom ; + }, + + /** + * Convert an image zoom to a viewport zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * Note: not accurate with multi-image. + * @function + * @param {Number} imageZoom The image zoom + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function( imageZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return imageZoom / ratio; + }, + /** * Sets the TiledImage's position in the world. * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. @@ -484,6 +565,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._needsDraw = true; }, + /** + * @returns {Number} The TiledImage's current opacity. + */ + getOpacity: function() { + return this.opacity; + }, + + /** + * @param {Number} opacity Opacity the tiled image should be drawn at. + */ + setOpacity: function(opacity) { + this.opacity = opacity; + this._needsDraw = true; + }, + // private _setScale: function(scale, immediately) { var sameTarget = (this._scaleSpring.target.value === scale); @@ -696,8 +792,6 @@ function updateViewport( tiledImage ) { // Load the new 'best' tile if ( best ) { loadTile( tiledImage, best, currentTime ); - // because we haven't finished drawing, so - tiledImage._needsDraw = true; } } @@ -843,13 +937,8 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity if (!tile.loaded) { var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); if (imageRecord) { - tile.loaded = true; - tile.image = imageRecord.getImage(); - - tiledImage._tileCache.cacheTile({ - tile: tile, - tiledImage: tiledImage - }); + var image = imageRecord.getImage(); + setTileLoaded(tiledImage, tile, image); } } @@ -922,8 +1011,8 @@ function loadTile( tiledImage, tile, time ) { tiledImage._imageLoader.addJob({ src: tile.url, crossOriginPolicy: tiledImage.crossOriginPolicy, - callback: function( image ){ - onTileLoad( tiledImage, tile, time, image ); + callback: function( image, errorMsg ){ + onTileLoad( tiledImage, tile, time, image, errorMsg ); }, abort: function() { tile.loading = false; @@ -931,9 +1020,9 @@ function loadTile( tiledImage, tile, time ) { }); } -function onTileLoad( tiledImage, tile, time, image ) { +function onTileLoad( tiledImage, tile, time, image, errorMsg ) { if ( !image ) { - $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + $.console.log( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); if( !tiledImage.debugMode ){ tile.loading = false; tile.exists = false; @@ -946,16 +1035,9 @@ function onTileLoad( tiledImage, tile, time, image ) { } var finish = function() { - tile.loading = false; - tile.loaded = true; - tile.image = image; - - var cutoff = Math.ceil( Math.log( tiledImage.source.getTileSize(tile.level) ) / Math.log( 2 ) ); - tiledImage._tileCache.cacheTile({ - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); + var cutoff = Math.ceil( Math.log( + tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) ); + setTileLoaded(tiledImage, tile, image, cutoff); }; // Check if we're mid-update; this can happen on IE8 because image load events for @@ -966,10 +1048,55 @@ function onTileLoad( tiledImage, tile, time, image ) { // Wait until after the update, in case caching unloads any tiles window.setTimeout( finish, 1); } - - tiledImage._needsDraw = true; } +function setTileLoaded(tiledImage, tile, image, cutoff) { + var increment = 0; + + function getCompletionCallback() { + increment++; + return completionCallback; + } + + function completionCallback() { + increment--; + if (increment === 0) { + tile.loading = false; + tile.loaded = true; + tiledImage._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); + tiledImage._needsDraw = true; + } + } + + /** + * Triggered when a tile has just been loaded in memory. That means that the + * image has been downloaded and can be modified before being drawn to the canvas. + * + * @event tile-loaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {Image} image - The image 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 {function} getCompletionCallback - A function giving a callback to call + * when the asynchronous processing of the image is done. The image will be + * marked as entirely loaded when the callback has been called once for each + * call to getCompletionCallback. + */ + tiledImage.viewer.raiseEvent("tile-loaded", { + tile: tile, + tiledImage: tiledImage, + image: image, + getCompletionCallback: getCompletionCallback + }); + // In case the completion callback is never called, we at least force it once. + getCompletionCallback()(); +} function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ var boundsTL = tile.bounds.getTopLeft(); @@ -1148,42 +1275,49 @@ function compareTiles( previousBest, tile ) { return previousBest; } -function drawTiles( tiledImage, lastDrawn ){ +function drawTiles( tiledImage, lastDrawn ) { var i, - tile, - tileKey, - viewer, - viewport, - position, - tileSource; + tile; + + if ( tiledImage.opacity <= 0 ) { + drawDebugInfo( tiledImage, lastDrawn ); + return; + } + var useSketch = tiledImage.opacity < 1; + if ( useSketch ) { + tiledImage._drawer._clear( true ); + } var usedClip = false; - if (tiledImage._clip) { - tiledImage._drawer.saveContext(); + if ( tiledImage._clip ) { + tiledImage._drawer.saveContext(useSketch); + var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); - var topLeft = tiledImage.viewport.pixelFromPoint(box.getTopLeft(), true); - var size = tiledImage.viewport.deltaPixelsFromPoints(box.getSize(), true); - box = new OpenSeadragon.Rect(topLeft.x * $.pixelDensityRatio, - topLeft.y * $.pixelDensityRatio, - size.x * $.pixelDensityRatio, - size.y * $.pixelDensityRatio); - tiledImage._drawer.setClip(box); + var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box); + tiledImage._drawer.setClip(clipRect, useSketch); + usedClip = true; } + if ( tiledImage.placeholderFillStyle && lastDrawn.length === 0 ) { + var placeholderRect = tiledImage._drawer.viewportToDrawerRectangle(tiledImage.getBounds(true)); + + var fillStyle = null; + if ( typeof tiledImage.placeholderFillStyle === "function" ) { + fillStyle = tiledImage.placeholderFillStyle(tiledImage, tiledImage._drawer.context); + } + else { + fillStyle = tiledImage.placeholderFillStyle; + } + + tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); + } + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { tile = lastDrawn[ i ]; - tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler ); + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch ); tile.beingDrawn = true; - if( tiledImage.debugMode ){ - try{ - tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); - }catch(e){ - $.console.error(e); - } - } - if( tiledImage.viewer ){ /** * - Needs documentation - @@ -1203,8 +1337,26 @@ function drawTiles( tiledImage, lastDrawn ){ } } - if (usedClip) { - tiledImage._drawer.restoreContext(); + if ( usedClip ) { + tiledImage._drawer.restoreContext( useSketch ); + } + + if ( useSketch ) { + tiledImage._drawer.blendSketch( tiledImage.opacity ); + } + drawDebugInfo( tiledImage, lastDrawn ); +} + +function drawDebugInfo( tiledImage, lastDrawn ) { + if( tiledImage.debugMode ) { + for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { + var tile = lastDrawn[ i ]; + try { + tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); + } catch(e) { + $.console.error(e); + } + } } } diff --git a/src/tilesource.js b/src/tilesource.js index 1c0d29d2..65841d25 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -73,6 +73,11 @@ * The size of the tiles to assumed to make up each pyramid layer in pixels. * Tile size determines the point at which the image pyramid must be * divided into a matrix of smaller images. + * Use options.tileWidth and options.tileHeight to support non-square tiles. + * @param {Number} [options.tileWidth] + * The width of the tiles to assumed to make up each pyramid layer in pixels. + * @param {Number} [options.tileHeight] + * The height of the tiles to assumed to make up each pyramid layer in pixels. * @param {Number} [options.tileOverlap] * The number of pixels each tile is expected to overlap touching tiles. * @param {Number} [options.minLevel] @@ -137,13 +142,6 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve * @member {OpenSeadragon.Point} dimensions * @memberof OpenSeadragon.TileSource# */ - /** - * The size of the image tiles used to compose the image. - * Please note that tileSize may be deprecated in a future release. - * Instead the getTileSize(level) function should be used. - * @member {Number} tileSize - * @memberof OpenSeadragon.TileSource# - */ /** * The overlap in pixels each tile shares with its adjacent neighbors. * @member {Number} tileOverlap @@ -174,7 +172,8 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve //async mechanism set some safe defaults first this.aspectRatio = 1; this.dimensions = new $.Point( 10, 10 ); - this.tileSize = 0; + this._tileWidth = 0; + this._tileHeight = 0; this.tileOverlap = 0; this.minLevel = 0; this.maxLevel = 0; @@ -191,7 +190,29 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve this.aspectRatio = ( options.width && options.height ) ? ( options.width / options.height ) : 1; this.dimensions = new $.Point( options.width, options.height ); - this.tileSize = options.tileSize ? options.tileSize : 0; + + if ( this.tileSize ){ + this._tileWidth = this._tileHeight = this.tileSize; + delete this.tileSize; + } else { + if( this.tileWidth ){ + // We were passed tileWidth in options, but we want to rename it + // with a leading underscore to make clear that it is not safe to directly modify it + this._tileWidth = this.tileWidth; + delete this.tileWidth; + } else { + this._tileWidth = 0; + } + + if( this.tileHeight ){ + // See note above about renaming this.tileWidth + this._tileHeight = this.tileHeight; + delete this.tileHeight; + } else { + this._tileHeight = 0; + } + } + this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0; this.minLevel = options.minLevel ? options.minLevel : 0; this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ? @@ -212,16 +233,42 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ + getTileSize: function( level ) { + $.console.error( + "[TileSource.getTileSize] is deprecated." + + "Use TileSource.getTileWidth() and TileSource.getTileHeight() instead" + ); + return this._tileWidth; + }, + /** - * Return the tileSize for a given level. - * Subclasses should override this if tileSizes can be different at different levels + * Return the tileWidth for a given level. + * Subclasses should override this if tileWidth can be different at different levels * such as in IIIFTileSource. Code should use this function rather than reading - * from .tileSize directly. tileSize may be deprecated in a future release. + * from ._tileWidth directly. * @function * @param {Number} level */ - getTileSize: function( level ) { - return this.tileSize; + getTileWidth: function( level ) { + if (!this._tileWidth) { + return this.getTileSize(level); + } + return this._tileWidth; + }, + + /** + * Return the tileHeight for a given level. + * Subclasses should override this if tileHeight can be different at different levels + * such as in IIIFTileSource. Code should use this function rather than reading + * from ._tileHeight directly. + * @function + * @param {Number} level + */ + getTileHeight: function( level ) { + if (!this._tileHeight) { + return this.getTileSize(level); + } + return this._tileHeight; }, /** @@ -250,8 +297,8 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ */ getNumTiles: function( level ) { var scale = this.getLevelScale( level ), - x = Math.ceil( scale * this.dimensions.x / this.getTileSize(level) ), - y = Math.ceil( scale * this.dimensions.y / this.getTileSize(level) ); + x = Math.ceil( scale * this.dimensions.x / this.getTileWidth(level) ), + y = Math.ceil( scale * this.dimensions.y / this.getTileHeight(level) ); return new $.Point( x, y ); }, @@ -277,10 +324,15 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ var i, tilesPerSide, tiles; + for( i = this.minLevel; i < this.maxLevel; i++ ){ tiles = this.getNumTiles( i ); - tilesPerSide = Math.floor( Math.max( rect.x, rect.y ) / this.getTileSize(i) ); - if( Math.max( tiles.x, tiles.y ) + 1 >= tilesPerSide ){ + 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 ){ break; } } @@ -293,9 +345,9 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ * @param {OpenSeadragon.Point} point */ getTileAtPoint: function( level, point ) { - var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ), - tx = Math.floor( pixel.x / this.getTileSize(level) ), - ty = Math.floor( pixel.y / this.getTileSize(level) ); + var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level) ), + tx = Math.floor( pixel.x / this.getTileWidth(level) ), + ty = Math.floor( pixel.y / this.getTileHeight(level) ); return new $.Point( tx, ty ); }, @@ -308,11 +360,12 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ */ getTileBounds: function( level, x, y ) { var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), - tileSize = this.getTileSize(level), - px = ( x === 0 ) ? 0 : tileSize * x - this.tileOverlap, - py = ( y === 0 ) ? 0 : tileSize * y - this.tileOverlap, - sx = tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap, - sy = tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap, + tileWidth = this.getTileWidth(level), + tileHeight = this.getTileHeight(level), + px = ( x === 0 ) ? 0 : tileWidth * x - this.tileOverlap, + py = ( y === 0 ) ? 0 : tileHeight * y - this.tileOverlap, + sx = tileWidth + ( x === 0 ? 1 : 2 ) * this.tileOverlap, + sy = tileHeight + ( y === 0 ? 1 : 2 ) * this.tileOverlap, scale = 1.0 / dimensionsScaled.x; sx = Math.min( sx, dimensionsScaled.x - px ); @@ -560,8 +613,7 @@ function processResponse( xhr ){ data = xhr.responseText; } }else if( responseText.match(/\s*[\{\[].*/) ){ - /*jshint evil:true*/ - data = eval( '('+responseText+')' ); + data = $.parseJSON(responseText); }else{ data = responseText; } diff --git a/src/viewer.js b/src/viewer.js index 7b269350..bca49e5c 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -374,7 +374,6 @@ $.Viewer = function( options ) { viewer: this, viewport: this.viewport, element: this.canvas, - opacity: this.opacity, debugGridColor: this.debugGridColor }); @@ -1073,6 +1072,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, }; /** * Raised when the viewer is about to change to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}). + * Note: the pre-full-screen event is not raised when the user is exiting + * full-screen mode by pressing the Esc key. In that case, consider using + * the full-screen, pre-full-page or full-page events. * * @event pre-full-screen * @memberof OpenSeadragon.Viewer @@ -1191,6 +1193,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * named 'getTileUrl', it is treated as a custom TileSource. * @param {Number} [options.index] The index of the item. Added on top of * all other items if not specified. + * @param {Boolean} [options.replace=false] If true, the item at options.index will be + * removed and the new item is added in its place. options.tileSource will be + * interpreted and fetched if necessary before the old item is removed to avoid leaving + * a gap in the world. * @param {Number} [options.x=0] The X position for the image in viewport coordinates. * @param {Number} [options.y=0] The Y position for the image in viewport coordinates. * @param {Number} [options.width=1] The width for the image in viewport coordinates. @@ -1198,6 +1204,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. + * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. * @param {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. @@ -1206,17 +1213,31 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * and "source" properties. * @param {Boolean} [options.collectionImmediately=false] If collectionMode is on, * specifies whether to snap to the new arrangement immediately or to animate to it. + * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. * @fires OpenSeadragon.World.event:add-item * @fires OpenSeadragon.Viewer.event:add-item-failed */ addTiledImage: function( options ) { $.console.assert(options, "[Viewer.addTiledImage] options is required"); $.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required"); + $.console.assert(!options.replace || (options.index > -1 && options.index < this.world.getItemCount()), + "[Viewer.addTiledImage] if options.replace is used, options.index must be a valid index in Viewer.world"); var _this = this; + if (options.replace) { + options.replaceItem = _this.world.getItemAt(options.index); + } + this._hideMessage(); + if (options.placeholderFillStyle === undefined) { + options.placeholderFillStyle = this.placeholderFillStyle; + } + if (options.opacity === undefined) { + options.opacity = this.opacity; + } + var myQueueItem = { options: options }; @@ -1272,6 +1293,14 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, _this._loadQueue.splice(0, 1); + if (queueItem.options.replace) { + var newIndex = _this.world.getIndexOfItem(options.replaceItem); + if (newIndex != -1) { + options.index = newIndex; + } + _this.world.removeItem(options.replaceItem); + } + tiledImage = new $.TiledImage({ viewer: _this, source: queueItem.tileSource, @@ -1284,6 +1313,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, width: queueItem.options.width, height: queueItem.options.height, clip: queueItem.options.clip, + placeholderFillStyle: queueItem.options.placeholderFillStyle, + opacity: queueItem.options.opacity, springStiffness: _this.springStiffness, animationTime: _this.animationTime, minZoomImageRatio: _this.minZoomImageRatio, @@ -1305,6 +1336,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, _this.world.arrange({ immediately: queueItem.options.collectionImmediately, rows: _this.collectionRows, + columns: _this.collectionColumns, layout: _this.collectionLayout, tileSize: _this.collectionTileSize, tileMargin: _this.collectionTileMargin @@ -1697,7 +1729,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * is closed which include when changing page. * @method * @param {Element|String|Object} element - A reference to an element or an id for - * the element which will overlayed. Or an Object specifying the configuration for the overlay + * the element which will be overlayed. Or an Object specifying the configuration for the overlay * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * rectangle which will be overlayed. * @param {OpenSeadragon.OverlayPlacement} placement - The position of the @@ -1757,6 +1789,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * Updates the overlay represented by the reference to the element or * element id moving it to the new location, relative to the new placement. * @method + * @param {Element|String} element - A reference to an element or an id for + * the element which is overlayed. * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * rectangle which will be overlayed. * @param {OpenSeadragon.OverlayPlacement} placement - The position of the @@ -1980,8 +2014,7 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, if ( tileSource.match( /\s*<.*/ ) ) { tileSource = $.parseXml( tileSource ); } else if ( tileSource.match( /\s*[\{\[].*/ ) ) { - /*jshint evil:true*/ - tileSource = eval( '(' + tileSource + ')' ); + tileSource = $.parseJSON(tileSource); } } @@ -2206,7 +2239,7 @@ function onCanvasKeyDown( event ) { if ( event.shift ) { this.viewport.zoomBy(1.1); } else { - this.viewport.panBy(new $.Point(0, -0.05)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(0, -40))); } this.viewport.applyConstraints(); return false; @@ -2214,16 +2247,16 @@ function onCanvasKeyDown( event ) { if ( event.shift ) { this.viewport.zoomBy(0.9); } else { - this.viewport.panBy(new $.Point(0, 0.05)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(0, 40))); } this.viewport.applyConstraints(); return false; case 37://left arrow - this.viewport.panBy(new $.Point(-0.05, 0)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(-40, 0))); this.viewport.applyConstraints(); return false; case 39://right arrow - this.viewport.panBy(new $.Point(0.05, 0)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(40, 0))); this.viewport.applyConstraints(); return false; default: @@ -2255,7 +2288,7 @@ function onCanvasKeyPress( event ) { if ( event.shift ) { this.viewport.zoomBy(1.1); } else { - this.viewport.panBy(new $.Point(0, -0.05)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(0, -40))); } this.viewport.applyConstraints(); return false; @@ -2264,16 +2297,16 @@ function onCanvasKeyPress( event ) { if ( event.shift ) { this.viewport.zoomBy(0.9); } else { - this.viewport.panBy(new $.Point(0, 0.05)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(0, 40))); } this.viewport.applyConstraints(); return false; case 97://a - this.viewport.panBy(new $.Point(-0.05, 0)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(-40, 0))); this.viewport.applyConstraints(); return false; case 100://d - this.viewport.panBy(new $.Point(0.05, 0)); + this.viewport.panBy(this.viewport.deltaPointsFromPixels(new $.Point(40, 0))); this.viewport.applyConstraints(); return false; default: @@ -2811,13 +2844,31 @@ function updateOnce( viewer ) { return; } + var containerSize; if ( viewer.autoResize ) { - var containerSize = _getSafeElemSize( viewer.container ); + containerSize = _getSafeElemSize( viewer.container ); if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { - // maintain image position - var oldBounds = viewer.viewport.getBounds(); - var oldCenter = viewer.viewport.getCenter(); - resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter); + if ( viewer.preserveImageSizeOnResize ) { + var prevContainerSize = THIS[ viewer.hash ].prevContainerSize; + var bounds = viewer.viewport.getBounds(true); + var deltaX = (containerSize.x - prevContainerSize.x); + var deltaY = (containerSize.y - prevContainerSize.y); + var viewportDiff = viewer.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(deltaX, deltaY), true); + viewer.viewport.resize(new OpenSeadragon.Point(containerSize.x, containerSize.y), false); + + // Keep the center of the image in the center and just adjust the amount of image shown + bounds.width += viewportDiff.x; + bounds.height += viewportDiff.y; + bounds.x -= (viewportDiff.x / 2); + bounds.y -= (viewportDiff.y / 2); + viewer.viewport.fitBoundsWithConstraints(bounds, true); + } + else { + // maintain image position + var oldBounds = viewer.viewport.getBounds(); + var oldCenter = viewer.viewport.getCenter(); + resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter); + } THIS[ viewer.hash ].prevContainerSize = containerSize; THIS[ viewer.hash ].forceRedraw = true; } @@ -2914,19 +2965,15 @@ function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter viewport.resize( containerSize, true ); - // We try to remove blanks as much as possible - var worldBounds = viewer.world.getHomeBounds(); - var newWidth = oldBounds.width <= worldBounds.width ? oldBounds.width : worldBounds.width; - var newHeight = oldBounds.height <= worldBounds.height ? - oldBounds.height : worldBounds.height; - var newBounds = new $.Rect( - oldCenter.x - ( newWidth / 2.0 ), - oldCenter.y - ( newHeight / 2.0 ), - newWidth, - newHeight - ); - viewport.fitBounds( newBounds, true ); + oldCenter.x - ( oldBounds.width / 2.0 ), + oldCenter.y - ( oldBounds.height / 2.0 ), + oldBounds.width, + oldBounds.height + ); + + // let the viewport decide if the bounds are too big or too small + viewport.fitBoundsWithConstraints( newBounds, true ); } function drawWorld( viewer ) { diff --git a/src/world.js b/src/world.js index c9e65225..73b28a67 100644 --- a/src/world.js +++ b/src/world.js @@ -281,6 +281,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W * @param {Boolean} [options.immediately=false] - Whether to animate to the new arrangement. * @param {String} [options.layout] - See collectionLayout in {@link OpenSeadragon.Options}. * @param {Number} [options.rows] - See collectionRows in {@link OpenSeadragon.Options}. + * @param {Number} [options.columns] - See collectionColumns in {@link OpenSeadragon.Options}. * @param {Number} [options.tileSize] - See collectionTileSize in {@link OpenSeadragon.Options}. * @param {Number} [options.tileMargin] - See collectionTileMargin in {@link OpenSeadragon.Options}. * @fires OpenSeadragon.World.event:metrics-change @@ -290,10 +291,16 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W var immediately = options.immediately || false; var layout = options.layout || $.DEFAULT_SETTINGS.collectionLayout; var rows = options.rows || $.DEFAULT_SETTINGS.collectionRows; + var columns = options.columns || $.DEFAULT_SETTINGS.collectionColumns; var tileSize = options.tileSize || $.DEFAULT_SETTINGS.collectionTileSize; var tileMargin = options.tileMargin || $.DEFAULT_SETTINGS.collectionTileMargin; var increment = tileSize + tileMargin; - var wrap = Math.ceil(this._items.length / rows); + var wrap; + if (!options.rows && columns) { + wrap = columns; + } else { + wrap = Math.ceil(this._items.length / rows); + } var x = 0; var y = 0; var item, box, width, height, position; diff --git a/test/coverage.html b/test/coverage.html index 9d61c48f..65cc2818 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -71,6 +71,7 @@ +