diff --git a/.gitignore b/.gitignore index 588aee54..3ebd08b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules build/ sftp-config.json +coverage/ +temp/ diff --git a/Gruntfile.js b/Gruntfile.js index 96162044..828f37a1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,5 @@ +/* global module */ + module.exports = function(grunt) { // ---------- @@ -5,7 +7,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks("grunt-contrib-concat"); grunt.loadNpmTasks("grunt-contrib-jshint"); grunt.loadNpmTasks("grunt-contrib-uglify"); - grunt.loadNpmTasks("grunt-contrib-qunit"); + grunt.loadNpmTasks("grunt-qunit-istanbul"); grunt.loadNpmTasks("grunt-contrib-connect"); grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-contrib-clean"); @@ -44,11 +46,14 @@ module.exports = function(grunt) { "src/referencestrip.js", "src/displayrectangle.js", "src/spring.js", - "src/imageLoader.js", + "src/imageloader.js", "src/tile.js", "src/overlay.js", "src/drawer.js", - "src/viewport.js" + "src/viewport.js", + "src/tiledimage.js", + "src/tilecache.js", + "src/world.js" ]; // ---------- @@ -69,6 +74,7 @@ module.exports = function(grunt) { clean: { build: ["build"], package: [packageDir], + coverage: ["coverage"], release: { src: [releaseRoot], options: { @@ -134,10 +140,26 @@ module.exports = function(grunt) { } }, qunit: { + normal: { + options: { + urls: [ "http://localhost:8000/test/test.html" ] + } + }, + coverage: { + options: { + urls: [ "http://localhost:8000/test/coverage.html" ], + coverage: { + src: ['src/*.js'], + htmlReport: 'coverage/html/', + instrumentedFiles: 'temp/', + baseUrl: '.', + disposeCollector: true + } + } + }, all: { options: { - timeout: 10000, - urls: [ "http://localhost:8000/test/test.html" ] + timeout: 10000 } } }, @@ -151,7 +173,7 @@ module.exports = function(grunt) { }, watch: { files: [ "Gruntfile.js", "src/*.js", "images/*" ], - tasks: "build" + tasks: "watchTask" }, jshint: { options: { @@ -206,6 +228,8 @@ module.exports = function(grunt) { }); // ---------- + // Bower task. + // Generates the Bower file for site-build. grunt.registerTask("bower", function() { var path = "../site-build/bower.json"; var data = grunt.file.readJSON(path); @@ -213,6 +237,18 @@ module.exports = function(grunt) { grunt.file.write(path, JSON.stringify(data, null, 2) + "\n"); }); + // ---------- + // Watch task. + // Called from the watch feature; does a full build or a minbuild, depending on + // whether you used --min on the command line. + grunt.registerTask("watchTask", function() { + if (grunt.option('min')) { + grunt.task.run("minbuild"); + } else { + grunt.task.run("build"); + } + }); + // ---------- // Build task. // Cleans out the build folder and builds the code and images into it, checking lint. @@ -221,10 +257,22 @@ module.exports = function(grunt) { "uglify", "replace:cleanPaths", "copy:build" ]); + // ---------- + // Minimal build task. + // For use during development as desired. Creates only the unminified version. + grunt.registerTask("minbuild", [ + "git-describe", "concat", "copy:build" + ]); + // ---------- // Test task. // Builds and runs unit tests. - grunt.registerTask("test", ["build", "connect", "qunit"]); + grunt.registerTask("test", ["build", "connect", "qunit:normal"]); + + // ---------- + // Coverage task. + // Outputs unit test code coverage report. + grunt.registerTask("coverage", ["clean:coverage", "connect", "qunit:coverage"]); // ---------- // Package task. diff --git a/README.md b/README.md index 71fafeca..75fc5c92 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ and open `http://localhost:8000/test/test.html` in your browser. Another good page, if you want to interactively test out your changes, is `http://localhost:8000/test/demo/basic.html`. +You can also get a report of the tests' code coverage: + + grunt coverage + +The report shows up at `coverage/html/index.html` viewable in a browser. + ### Contributing OpenSeadragon is truly a community project; we welcome your involvement! diff --git a/changelog.txt b/changelog.txt index 473b745f..999a2378 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,54 @@ OPENSEADRAGON CHANGELOG ======================= +1.3.0: (in progress) + +* 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 + * BREAKING CHANGE: Navigator no longer sends an open event when its viewer opens + * BREAKING CHANGE: Viewer.drawers and Viewer.drawersContainer no longer exist + * BREAKING CHANGE: A Viewer's Drawer and Viewport are now made once per Viewer and reused for every image that Viewer opens (rather than being recreated for every open); this means if you change Viewer options between opens, the behavior is different now. + * DEPRECATION: use Viewer.addTiledImage instead of Viewer.addLayer + * addTiledImage supports positioning config properties + * DEPRECATION: use World.getItemAt instead of Viewer.getLayerAtLevel + * DEPRECATION: use World.getIndexOfItem instead of Viewer.getLevelOfLayer + * DEPRECATION: use World.getItemCount instead of Viewer.getLayersCount + * DEPRECATION: use World.setItemIndex instead of Viewer.setLayerLevel + * DEPRECATION: use World.removeItem instead of Viewer.removeLayer + * DEPRECATION: use World.needsDraw instead of Drawer.needsUpdate + * DEPRECATION: use TileCache.numTilesLoaded instead of Drawer.numTilesLoaded + * DEPRECATION: use World.resetItems instead of Drawer.reset + * DEPRECATION: use Drawer.clear and World.draw instead of Drawer.update + * DEPRECATION: the layersAspectRatioEpsilon option is no longer necessary + * DEPRECATION: Viewer's add-layer event is now World's add-item event + * DEPRECATION: Viewer's layer-level-changed event is now World's item-index-change event + * DEPRECATION: Viewer's remove-layer event is now World's remove-item event + * DEPRECATION: Viewer's add-layer-failed event is now add-item-failed + * DEPRECATION: TileSourceCollection has been retired in favor of World + * DEPRECATION: collectionMode no longer draws outlines or reflections for items + * Drawer has been split into three classes: + * TiledImage, tile management and positioning for a single tiled image + * TileCache, tile caching for all images + * Drawer, tile drawing for all images + * New class: World, keeps track of multiple images in the scene + * Viewer now has world and tileCache properties + * Rect and Point now have clone functions + * New Viewport method for managing homeBounds as well as constraints: setHomeBounds + * Viewport.open supports positioning config properties +* 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) +* imageLoaderLimit now works (#544) +* Turning off scrollToZoom in gestureSettings now allows scroll events to propagate +* You can now set a minZoomLevel that's greater than the home zoom level +* Added union() to OpenSeadragon.Rect +* Fixed an error in fitBounds if the new and old bounds were extremely close in size +* Added ajaxWithCredentials option (#543) +* Added viewport-change event for after the viewport changes but before it's drawn +* A spring's current value is now updated immediately on reset (#524) +* Fixed an error in fitBounds that occurred sometimes with immediately = true +* Added support for HDPI (retina) displays (#583) + 1.2.2: (in progress) * Corrected IIIF tile source to use canonical syntax (#586) diff --git a/openseadragon.sublime-project b/openseadragon.sublime-project index 1d6484e7..4feb36d8 100644 --- a/openseadragon.sublime-project +++ b/openseadragon.sublime-project @@ -8,7 +8,8 @@ "*.sublime-workspace" ], "folder_exclude_patterns": [ - "node_modules" + "node_modules", + "coverage" ] } ], @@ -17,5 +18,5 @@ "tab_size": 4, "translate_tabs_to_spaces": true, "trim_trailing_white_space_on_save": true - } + } } diff --git a/package.json b/package.json index 992fd364..99e60e8a 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "grunt-git-describe": "^2.3.2", "grunt-contrib-uglify": "^0.4.0", "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-qunit": "^0.5.1", "grunt-contrib-jshint": "^0.10.0", "grunt-contrib-compress": "^0.9.1", "grunt-contrib-connect": "^0.7.1", - "qunitjs": "^1.14.0" + "qunitjs": "^1.14.0", + "grunt-qunit-istanbul": "^0.4.5" }, "scripts": { "test": "grunt test" diff --git a/src/drawer.js b/src/drawer.js index be1438f3..77f078ad 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -34,35 +34,25 @@ (function( $ ){ -var DEVICE_SCREEN = $.getWindowSize(), - BROWSER = $.Browser.vendor, - BROWSER_VERSION = $.Browser.version, - - SUBPIXEL_RENDERING = ( - ( BROWSER == $.BROWSERS.FIREFOX ) || - ( BROWSER == $.BROWSERS.OPERA ) || - ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) || - ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) || - ( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 ) - ); - - /** * @class Drawer - * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. - * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#drawer}). - * * @memberof OpenSeadragon - * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. - * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. - * @param {Element} element - Parent element. + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * @param {Object} options - Options for this Drawer. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. + * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. + * @param {Element} options.element - Parent element. + * @param {Number} [options.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" ); //backward compatibility for positional args while prefering more //idiomatic javascript options object as the only argument - var args = arguments, - i; + var args = arguments; if( !$.isPlainObject( options ) ){ options = { @@ -72,38 +62,17 @@ $.Drawer = function( options ) { }; } - $.extend( true, this, { + $.console.assert( options.viewport, "[Drawer] options.viewport is required" ); + $.console.assert( options.element, "[Drawer] options.element is required" ); - //internal state properties - viewer: null, - imageLoader: new $.ImageLoader(), - tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. - tilesLoaded: [], // An unordered list of Tiles with loaded images. - coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. - lastDrawn: [], // An unordered list of Tiles drawn last frame. - lastResetTime: 0, // Last time for which the drawer was reset. - midUpdate: false, // Is the drawer currently updating the viewport? - updateAgain: true, // Does the drawer need to update the viewort again? + if ( options.source ) { + $.console.error( "[Drawer] options.source is no longer accepted; use TiledImage instead" ); + } - - //internal state / configurable settings - collectionOverlays: {}, // For collection mode. Here an overlay is actually a viewer. - - //configurable settings - opacity: $.DEFAULT_SETTINGS.opacity, - maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, - 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, - timeout: $.DEFAULT_SETTINGS.timeout, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy - - }, 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; this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true ); /** @@ -112,7 +81,7 @@ $.Drawer = function( options ) { * @member {Element} container * @memberof OpenSeadragon.Drawer# */ - this.container = $.getElement( this.element ); + this.container = $.getElement( options.element ); /** * A <canvas> element if the browser supports them, otherwise a <div> element. * Child element of {@link OpenSeadragon.Drawer#container}. @@ -126,8 +95,7 @@ $.Drawer = function( options ) { * @memberof OpenSeadragon.Drawer# */ this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null; - // Ratio of zoomable image height to width. - this.normHeight = this.source.dimensions.y / this.source.dimensions.x; + /** * @member {Element} element * @memberof OpenSeadragon.Drawer# @@ -140,6 +108,13 @@ $.Drawer = function( options ) { // Note that this means overlays you want to be rtl need to be explicitly set to rtl. this.container.dir = 'ltr'; + // check canvas available width and height, set canvas width and height such that the canvas backing store is set to the proper pixel density + if (this.useCanvas) { + var viewportSize = this._calculateCanvasSize(); + this.canvas.width = viewportSize.x; + this.canvas.height = viewportSize.y; + } + this.canvas.style.width = "100%"; this.canvas.style.height = "100%"; this.canvas.style.position = "absolute"; @@ -148,79 +123,31 @@ $.Drawer = function( options ) { // explicit left-align this.container.style.textAlign = "left"; this.container.appendChild( this.canvas ); - - //this.profiler = new $.Profiler(); }; $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ - - /** - * Adds an html element as an overlay to the current viewport. Useful for - * highlighting words or areas of interest on an image or other zoomable - * interface. - * @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 - * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the - * viewport which the location coordinates will be treated as relative - * to. - * @param {function} onDraw - If supplied the callback is called when the overlay - * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning. - * It is passed position, size and element. - * @fires OpenSeadragon.Viewer.event:add-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#addOverlay} instead. - */ + // deprecated addOverlay: function( element, location, placement, onDraw ) { $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead."); this.viewer.addOverlay( element, location, placement, onDraw ); return this; }, - /** - * 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 {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the - * viewport which the location coordinates will be treated as relative - * to. - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:update-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#updateOverlay} instead. - */ + // deprecated updateOverlay: function( element, location, placement ) { $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead."); this.viewer.updateOverlay( element, location, placement ); return this; }, - /** - * Removes and overlay identified by the reference element or element id - * and schedules and update. - * @method - * @param {Element|String} element - A reference to the element or an - * element id which represent the ovelay content to be removed. - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:remove-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#removeOverlay} instead. - */ + // deprecated removeOverlay: function( element ) { $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead."); this.viewer.removeOverlay( element ); return this; }, - /** - * Removes all currently configured Overlays from this Drawer and schedules - * and update. - * @method - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:clear-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#clearOverlays} instead. - */ + // deprecated clearOverlays: function() { $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead."); this.viewer.clearOverlays(); @@ -229,7 +156,6 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Set the opacity of the drawer. - * @method * @param {Number} opacity * @return {OpenSeadragon.Drawer} Chainable. */ @@ -241,63 +167,40 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Get the opacity of the drawer. - * @method * @returns {Number} */ getOpacity: function() { return this.opacity; }, - /** - * Returns whether the Drawer is scheduled for an update at the - * soonest possible opportunity. - * @method - * @returns {Boolean} - Whether the Drawer is scheduled for an update at the - * soonest possible opportunity. - */ + + // deprecated needsUpdate: function() { - return this.updateAgain; + $.console.error( "[Drawer.needsUpdate] this function is deprecated. Use World.needsDraw instead." ); + return this.viewer.world.needsDraw(); }, - /** - * Returns the total number of tiles that have been loaded by this Drawer. - * @method - * @returns {Number} - The total number of tiles that have been loaded by - * this Drawer. - */ + // deprecated numTilesLoaded: function() { - return this.tilesLoaded.length; + $.console.error( "[Drawer.numTilesLoaded] this function is deprecated. Use TileCache.numTilesLoaded instead." ); + return this.viewer.tileCache.numTilesLoaded(); }, - /** - * Clears all tiles and triggers an update on the next call to - * Drawer.prototype.update(). - * @method - * @return {OpenSeadragon.Drawer} Chainable. - */ + // deprecated reset: function() { - clearTiles( this ); - this.lastResetTime = $.now(); - this.updateAgain = true; + $.console.error( "[Drawer.reset] this function is deprecated. Use World.resetItems instead." ); + this.viewer.world.resetItems(); return this; }, - /** - * Forces the Drawer to update. - * @method - * @return {OpenSeadragon.Drawer} Chainable. - */ + // deprecated update: function() { - //this.profiler.beginUpdate(); - this.midUpdate = true; - updateViewport( this ); - this.midUpdate = false; - //this.profiler.endUpdate(); + $.console.error( "[Drawer.update] this function is deprecated. Use Drawer.clear and World.draw instead." ); + this.clear(); + this.viewer.world.draw(); return this; }, /** - * Returns whether rotation is supported or not. - * @method * @return {Boolean} True if rotation is supported. */ canRotate: function() { @@ -306,917 +209,189 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Destroy the drawer (unload current loaded tiles) - * @method - * @return null */ destroy: function() { - //unload current loaded tiles (=empty TILE_CACHE) - for ( var i = 0; i < this.tilesLoaded.length; ++i ) { - this.tilesLoaded[i].unload(); - } - //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed) this.canvas.width = 1; this.canvas.height = 1; + }, + + /** + * Clears the Drawer so it's ready to draw another frame. + */ + clear: function() { + this.canvas.innerHTML = ""; + if ( this.useCanvas ) { + var viewportSize = this._calculateCanvasSize(); + if( this.canvas.width != viewportSize.x || + this.canvas.height != viewportSize.y ) { + this.canvas.width = viewportSize.x; + this.canvas.height = viewportSize.y; + } + this.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); + } + }, + + /** + * 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}) + * where rendered is the context with the pre-drawn image. + */ + drawTile: function( tile, drawingHandler ) { + $.console.assert(tile, '[Drawer.drawTile] tile is required'); + $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); + + if ( this.useCanvas ) { + // 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 ); + } else { + tile.drawCanvas( this.context, drawingHandler ); + } + } else { + tile.drawHTML( this.canvas ); + } + }, + + // 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(); + } + }, + + // 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; + + this.context.strokeRect( + rect.x * $.pixelDensityRatio, + rect.y * $.pixelDensityRatio, + rect.width * $.pixelDensityRatio, + rect.height * $.pixelDensityRatio + ); + + this.context.restore(); + } + }, + + // private + _offsetForRotation: function( tile, degrees ){ + var cx = this.canvas.width / 2, + cy = this.canvas.height / 2, + px = tile.position.x - cx, + py = tile.position.y - cy; + + this.context.save(); + + this.context.translate(cx, cy); + this.context.rotate( Math.PI / 180 * degrees); + tile.position.x = px; + tile.position.y = py; + }, + + // 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(); + }, + + // private + _calculateCanvasSize: function() { + var pixelDensityRatio = $.pixelDensityRatio; + var viewportSize = this.viewport.getContainerSize(); + return { + x: viewportSize.x * pixelDensityRatio, + y: viewportSize.y * pixelDensityRatio + }; } }; -/** - * @private - * @inner - * Pretty much every other line in this needs to be documented so it's clear - * how each piece of this routine contributes to the drawing process. That's - * why there are so many TODO's inside this function. - */ -function updateViewport( drawer ) { - - drawer.updateAgain = false; - - if( drawer.viewer ){ - /** - * - Needs documentation - - * - * @event update-viewport - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - drawer.viewer.raiseEvent( 'update-viewport', {} ); - } - - var tile, - level, - best = null, - haveDrawn = false, - currentTime = $.now(), - viewportSize = drawer.viewport.getContainerSize(), - viewportBounds = drawer.viewport.getBounds( true ), - viewportTL = viewportBounds.getTopLeft(), - viewportBR = viewportBounds.getBottomRight(), - zeroRatioC = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( 0 ), - true - ).x, - lowestLevel = Math.max( - drawer.source.minLevel, - Math.floor( - Math.log( drawer.minZoomImageRatio ) / - Math.log( 2 ) - ) - ), - highestLevel = Math.min( - Math.abs(drawer.source.maxLevel), - Math.abs(Math.floor( - Math.log( zeroRatioC / drawer.minPixelRatio ) / - Math.log( 2 ) - )) - ), - degrees = drawer.viewport.degrees, - renderPixelRatioC, - renderPixelRatioT, - zeroRatioT, - optimalRatio, - levelOpacity, - levelVisibility; - - // Reset tile's internal drawn state - while ( drawer.lastDrawn.length > 0 ) { - tile = drawer.lastDrawn.pop(); - tile.beingDrawn = false; - } - - // Clear canvas - drawer.canvas.innerHTML = ""; - if ( drawer.useCanvas ) { - if( drawer.canvas.width != viewportSize.x || - drawer.canvas.height != viewportSize.y ){ - drawer.canvas.width = viewportSize.x; - drawer.canvas.height = viewportSize.y; - } - drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); - } - - //Change bounds for rotation - if (degrees === 90 || degrees === 270) { - var rotatedBounds = viewportBounds.rotate( degrees ); - viewportTL = rotatedBounds.getTopLeft(); - viewportBR = rotatedBounds.getBottomRight(); - } - else if (degrees !== 0) { - // This is just an approximation. - var orthBounds = viewportBounds.rotate(90); - viewportBounds.x -= orthBounds.width / 2; - viewportBounds.y -= orthBounds.height / 2; - viewportBounds.width += orthBounds.width; - viewportBounds.height += orthBounds.height; - viewportTL = viewportBounds.getTopLeft(); - viewportBR = viewportBounds.getBottomRight(); - } - - //Don't draw if completely outside of the viewport - if ( !drawer.wrapHorizontal && - ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { - return; - } else if - ( !drawer.wrapVertical && - ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { - return; - } - - // Calculate viewport rect / bounds - if ( !drawer.wrapHorizontal ) { - viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, 1 ); - } - if ( !drawer.wrapVertical ) { - viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); - } - - // Calculations for the interval of levels to draw - // (above in initial var statement) - // can return invalid intervals; fix that here if necessary - lowestLevel = Math.min( lowestLevel, highestLevel ); - - // Update any level that will be drawn - var drawLevel; // FIXME: drawLevel should have a more explanatory name - for ( level = highestLevel; level >= lowestLevel; level-- ) { - drawLevel = false; - - //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( level ), - true - ).x; - - if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) || - ( level == lowestLevel ) ) { - drawLevel = true; - haveDrawn = true; - } else if ( !haveDrawn ) { - continue; - } - - //Perform calculations for draw if we haven't drawn this - renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( level ), - false - ).x; - - zeroRatioT = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( - Math.max( - drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1, - 0 - ) - ), - false - ).x; - - optimalRatio = drawer.immediateRender ? - 1 : - zeroRatioT; - - levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); - - levelVisibility = optimalRatio / Math.abs( - optimalRatio - renderPixelRatioT - ); - - // Update the level and keep track of 'best' tile to load - best = updateLevel( - drawer, - haveDrawn, - drawLevel, - level, - levelOpacity, - levelVisibility, - viewportTL, - viewportBR, - currentTime, - best - ); - - // Stop the loop if lower-res tiles would all be covered by - // already drawn tiles - if ( providesCoverage( drawer.coverage, level ) ) { - break; - } - } - - // Perform the actual drawing - drawTiles( drawer, drawer.lastDrawn ); - - // Load the new 'best' tile - if ( best ) { - loadTile( drawer, best, currentTime ); - // because we haven't finished drawing, so - drawer.updateAgain = true; - } - -} - - -function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ - - var x, y, - tileTL, - tileBR, - numberOfTiles, - viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ); - - - if( drawer.viewer ){ - /** - * - Needs documentation - - * - * @event update-level - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {Object} havedrawn - * @property {Object} level - * @property {Object} opacity - * @property {Object} visibility - * @property {Object} topleft - * @property {Object} bottomright - * @property {Object} currenttime - * @property {Object} best - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - drawer.viewer.raiseEvent( 'update-level', { - havedrawn: haveDrawn, - level: level, - opacity: levelOpacity, - visibility: levelVisibility, - topleft: viewportTL, - bottomright: viewportBR, - currenttime: currentTime, - best: best - }); - } - - //OK, a new drawing so do your calculations - tileTL = drawer.source.getTileAtPoint( level, viewportTL ); - tileBR = drawer.source.getTileAtPoint( level, viewportBR ); - numberOfTiles = drawer.source.getNumTiles( level ); - - resetCoverage( drawer.coverage, level ); - - if ( !drawer.wrapHorizontal ) { - tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); - } - if ( !drawer.wrapVertical ) { - tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); - } - - for ( x = tileTL.x; x <= tileBR.x; x++ ) { - for ( y = tileTL.y; y <= tileBR.y; y++ ) { - - best = updateTile( - drawer, - drawLevel, - haveDrawn, - x, y, - level, - levelOpacity, - levelVisibility, - viewportCenter, - numberOfTiles, - currentTime, - best - ); - - } - } - - return best; -} - -function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ - - var tile = getTile( - x, y, - level, - drawer.source, - drawer.tilesMatrix, - currentTime, - numberOfTiles, - drawer.normHeight - ), - drawTile = drawLevel; - - if( drawer.viewer ){ - /** - * - Needs documentation - - * - * @event update-tile - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - drawer.viewer.raiseEvent( 'update-tile', { - tile: tile - }); - } - - setCoverage( drawer.coverage, level, x, y, false ); - - if ( !tile.exists ) { - return best; - } - - if ( haveDrawn && !drawTile ) { - if ( isCovered( drawer.coverage, level, x, y ) ) { - setCoverage( drawer.coverage, level, x, y, true ); - } else { - drawTile = true; - } - } - - if ( !drawTile ) { - return best; - } - - positionTile( - tile, - drawer.source.tileOverlap, - drawer.viewport, - viewportCenter, - levelVisibility - ); - - if ( tile.loaded ) { - var needsUpdate = blendTile( - drawer, - tile, - x, y, - level, - levelOpacity, - currentTime - ); - - if ( needsUpdate ) { - drawer.updateAgain = true; - } - } else if ( tile.loading ) { - // the tile is already in the download queue - // thanks josh1093 for finally translating this typo - } else { - best = compareTiles( best, tile ); - } - - return best; -} - -function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) { - var xMod, - yMod, - bounds, - exists, - url, - tile; - - if ( !tilesMatrix[ level ] ) { - tilesMatrix[ level ] = {}; - } - if ( !tilesMatrix[ level ][ x ] ) { - tilesMatrix[ level ][ x ] = {}; - } - - if ( !tilesMatrix[ level ][ x ][ y ] ) { - xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; - yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - bounds = tileSource.getTileBounds( level, xMod, yMod ); - exists = tileSource.tileExists( level, xMod, yMod ); - url = tileSource.getTileUrl( level, xMod, yMod ); - - bounds.x += 1.0 * ( x - xMod ) / numTiles.x; - bounds.y += normHeight * ( y - yMod ) / numTiles.y; - - tilesMatrix[ level ][ x ][ y ] = new $.Tile( - level, - x, - y, - bounds, - exists, - url - ); - } - - tile = tilesMatrix[ level ][ x ][ y ]; - tile.lastTouchTime = time; - - return tile; -} - -function loadTile( drawer, tile, time ) { - if( drawer.viewport.collectionMode ){ - drawer.midUpdate = false; - onTileLoad( drawer, tile, time ); - } else { - tile.loading = true; - drawer.imageLoader.addJob({ - src: tile.url, - crossOriginPolicy: drawer.crossOriginPolicy, - callback: function( image ){ - onTileLoad( drawer, tile, time, image ); - } - }); - } -} - -function onTileLoad( drawer, tile, time, image ) { - - tile.loading = false; - - if ( !image && !drawer.viewport.collectionMode ) { - $.console.log( "Tile %s failed to load: %s", tile, tile.url ); - if( !drawer.debugMode ){ - tile.exists = false; - return; - } - } else if ( time < drawer.lastResetTime ) { - $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); - return; - } - - tile.loaded = true; - tile.image = image; - - if ( drawer.tilesLoaded.length < drawer.maxImageCacheCount ) { - // always safe to append things to cache - drawer.tilesLoaded[ drawer.tilesLoaded.length ] = tile; - } - else { - // need to remove something from cache, - // make sure this doesn't happen mid update - if ( !drawer.midUpdate ) { - updateTileCache( tile, drawer ); - } - else { - window.setTimeout( function() { - updateTileCache( tile, drawer ); - }, 1); - } - } - - drawer.updateAgain = true; -} - -function updateTileCache( newTile, drawer ) { - var i, prevTile, prevTime, worstTime, prevLevel, worstLevel, - insertionIndex = drawer.tilesLoaded.length, - cutoff = Math.ceil( Math.log( drawer.source.getTileSize(newTile.level) ) / Math.log( 2 ) ), - worstTile = null, - worstTileIndex = -1; - - for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) { - prevTile = drawer.tilesLoaded[ i ]; - - if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) { - continue; - } else if ( !worstTile ) { - worstTile = prevTile; - worstTileIndex = i; - continue; - } - - prevTime = prevTile.lastTouchTime; - worstTime = worstTile.lastTouchTime; - prevLevel = prevTile.level; - worstLevel = worstTile.level; - - if ( prevTime < worstTime || - ( prevTime == worstTime && prevLevel > worstLevel ) ) { - worstTile = prevTile; - worstTileIndex = i; - } - } - - if ( worstTile && worstTileIndex >= 0 ) { - worstTile.unload(); - insertionIndex = worstTileIndex; - } - - drawer.tilesLoaded[ insertionIndex ] = newTile; -} - - -function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){ - var boundsTL = tile.bounds.getTopLeft(), - boundsSize = tile.bounds.getSize(), - positionC = viewport.pixelFromPoint( boundsTL, true ), - positionT = viewport.pixelFromPoint( boundsTL, false ), - sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), - sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), - tileCenter = positionT.plus( sizeT.divide( 2 ) ), - tileDistance = viewportCenter.distanceTo( tileCenter ); - - if ( !overlap ) { - sizeC = sizeC.plus( new $.Point( 1, 1 ) ); - } - - tile.position = positionC; - tile.size = sizeC; - tile.distance = tileDistance; - tile.visibility = levelVisibility; -} - - -function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * drawer.blendTime, - deltaTime, - opacity; - - if ( !tile.blendStart ) { - tile.blendStart = currentTime; - } - - deltaTime = currentTime - tile.blendStart; - opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; - - if ( drawer.alwaysBlend ) { - opacity *= levelOpacity; - } - - tile.opacity = opacity; - - drawer.lastDrawn.push( tile ); - - if ( opacity == 1 ) { - setCoverage( drawer.coverage, level, x, y, true ); - } else if ( deltaTime < blendTimeMillis ) { - return true; - } - - return false; -} - - -function clearTiles( drawer ) { - drawer.tilesMatrix = {}; - drawer.tilesLoaded = []; -} - -/** - * @private - * @inner - * Returns true if the given tile provides coverage to lower-level tiles of - * lower resolution representing the same content. If neither x nor y is - * given, returns true if the entire visible level provides coverage. - * - * Note that out-of-bounds tiles provide coverage in this sense, since - * there's no content that they would need to cover. Tiles at non-existent - * levels that are within the image bounds, however, do not. - */ -function providesCoverage( coverage, level, x, y ) { - var rows, - cols, - i, j; - - if ( !coverage[ level ] ) { - return false; - } - - if ( x === undefined || y === undefined ) { - rows = coverage[ level ]; - for ( i in rows ) { - if ( rows.hasOwnProperty( i ) ) { - cols = rows[ i ]; - for ( j in cols ) { - if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { - return false; - } - } - } - } - - return true; - } - - return ( - coverage[ level ][ x] === undefined || - coverage[ level ][ x ][ y ] === undefined || - coverage[ level ][ x ][ y ] === true - ); -} - -/** - * @private - * @inner - * Returns true if the given tile is completely covered by higher-level - * tiles of higher resolution representing the same content. If neither x - * nor y is given, returns true if the entire visible level is covered. - */ -function isCovered( coverage, level, x, y ) { - if ( x === undefined || y === undefined ) { - return providesCoverage( coverage, level + 1 ); - } else { - return ( - providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && - providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && - providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && - providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) - ); - } -} - -/** - * @private - * @inner - * Sets whether the given tile provides coverage or not. - */ -function setCoverage( coverage, level, x, y, covers ) { - if ( !coverage[ level ] ) { - $.console.warn( - "Setting coverage for a tile before its level's coverage has been reset: %s", - level - ); - return; - } - - if ( !coverage[ level ][ x ] ) { - coverage[ level ][ x ] = {}; - } - - coverage[ level ][ x ][ y ] = covers; -} - -/** - * @private - * @inner - * Resets coverage information for the given level. This should be called - * after every draw routine. Note that at the beginning of the next draw - * routine, coverage for every visible tile should be explicitly set. - */ -function resetCoverage( coverage, level ) { - coverage[ level ] = {}; -} - -/** - * @private - * @inner - * Determines whether the 'last best' tile for the area is better than the - * tile in question. - */ -function compareTiles( previousBest, tile ) { - if ( !previousBest ) { - return tile; - } - - if ( tile.visibility > previousBest.visibility ) { - return tile; - } else if ( tile.visibility == previousBest.visibility ) { - if ( tile.distance < previousBest.distance ) { - return tile; - } - } - - return previousBest; -} - -function drawTiles( drawer, lastDrawn ){ - var i, - tile, - tileKey, - viewer, - viewport, - position, - tileSource, - collectionTileSource; - - // We need a callback to give image manipulation a chance to happen - var drawingHandler = function(args) { - if (drawer.viewer) { - /** - * This event is fired just before the tile is drawn giving the application a chance to alter the image. - * - * NOTE: This event is only fired when the drawer is using a . - * - * @event tile-drawing - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - 'context', 'tile' and 'rendered'. - */ - drawer.viewer.raiseEvent('tile-drawing', args); - } - }; - - for ( i = lastDrawn.length - 1; i >= 0; i-- ) { - tile = lastDrawn[ i ]; - - //We dont actually 'draw' a collection tile, rather its used to house - //an overlay which does the drawing in its own viewport - if( drawer.viewport.collectionMode ){ - - tileKey = tile.x + '/' + tile.y; - viewport = drawer.viewport; - collectionTileSource = viewport.collectionTileSource; - - if( !drawer.collectionOverlays[ tileKey ] ){ - - position = collectionTileSource.layout == 'horizontal' ? - tile.y + ( tile.x * collectionTileSource.rows ) : - tile.x + ( tile.y * collectionTileSource.rows ); - - if (position < collectionTileSource.tileSources.length) { - tileSource = collectionTileSource.tileSources[ position ]; - } else { - tileSource = null; - } - - //$.console.log("Rendering collection tile %s | %s | %s", tile.y, tile.y, position); - if( tileSource ){ - drawer.collectionOverlays[ tileKey ] = viewer = new $.Viewer({ - hash: viewport.viewer.hash + "-" + tileKey, - element: $.makeNeutralElement( "div" ), - mouseNavEnabled: false, - showNavigator: false, - showSequenceControl: false, - showNavigationControl: false, - tileSources: [ - tileSource - ] - }); - - //TODO: IE seems to barf on this, not sure if its just the border - // but we probably need to clear this up with a better - // test of support for various css features - if( SUBPIXEL_RENDERING ){ - viewer.element.style.border = '1px solid rgba(255,255,255,0.38)'; - viewer.element.style['-webkit-box-reflect'] = - 'below 0px -webkit-gradient('+ - 'linear,left '+ - 'top,left '+ - 'bottom,from(transparent),color-stop(62%,transparent),to(rgba(255,255,255,0.62))'+ - ')'; - } - - drawer.viewer.addOverlay( - viewer.element, - tile.bounds - ); - } - - }else{ - viewer = drawer.collectionOverlays[ tileKey ]; - if( viewer.viewport ){ - viewer.viewport.resize( tile.size, true ); - viewer.viewport.goHome( true ); - } - } - - } else { - - if ( drawer.useCanvas ) { - // TODO do this in a more performant way - // specifically, don't save,rotate,restore every time we draw a tile - if( drawer.viewport.degrees !== 0 ) { - offsetForRotation( tile, drawer.canvas, drawer.context, drawer.viewport.degrees ); - tile.drawCanvas( drawer.context, drawingHandler ); - restoreRotationChanges( tile, drawer.canvas, drawer.context ); - } else { - tile.drawCanvas( drawer.context, drawingHandler ); - } - } else { - tile.drawHTML( drawer.canvas ); - } - - - tile.beingDrawn = true; - } - - if( drawer.debugMode ){ - try{ - drawDebugInfo( drawer, tile, lastDrawn.length, i ); - }catch(e){ - $.console.error(e); - } - } - - if( drawer.viewer ){ - /** - * - Needs documentation - - * - * @event tile-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - drawer.viewer.raiseEvent( 'tile-drawn', { - tile: tile - }); - } - } -} - -function offsetForRotation( tile, canvas, context, degrees ){ - var cx = canvas.width / 2, - cy = canvas.height / 2, - px = tile.position.x - cx, - py = tile.position.y - cy; - - context.save(); - - context.translate(cx, cy); - context.rotate( Math.PI / 180 * degrees); - tile.position.x = px; - tile.position.y = py; -} - -function restoreRotationChanges( tile, canvas, context ){ - var cx = canvas.width / 2, - cy = canvas.height / 2, - px = tile.position.x + cx, - py = tile.position.y + cy; - - tile.position.x = px; - tile.position.y = py; - - context.restore(); -} - - -function drawDebugInfo( drawer, tile, count, i ){ - - if ( drawer.useCanvas ) { - drawer.context.save(); - drawer.context.lineWidth = 2; - drawer.context.font = 'small-caps bold 13px ariel'; - drawer.context.strokeStyle = drawer.debugGridColor; - drawer.context.fillStyle = drawer.debugGridColor; - - offsetForRotation( tile, drawer.canvas, drawer.context, drawer.viewport.degrees ); - - drawer.context.strokeRect( - tile.position.x, - tile.position.y, - tile.size.x, - tile.size.y - ); - - var tileCenterX = tile.position.x + (tile.size.x / 2); - var tileCenterY = tile.position.y + (tile.size.y / 2); - - // Rotate the text the right way around. - drawer.context.translate( tileCenterX, tileCenterY ); - drawer.context.rotate( Math.PI / 180 * -drawer.viewport.degrees ); - drawer.context.translate( -tileCenterX, -tileCenterY ); - - if( tile.x === 0 && tile.y === 0 ){ - drawer.context.fillText( - "Zoom: " + drawer.viewport.getZoom(), - tile.position.x, - tile.position.y - 30 - ); - drawer.context.fillText( - "Pan: " + drawer.viewport.getBounds().toString(), - tile.position.x, - tile.position.y - 20 - ); - } - drawer.context.fillText( - "Level: " + tile.level, - tile.position.x + 10, - tile.position.y + 20 - ); - drawer.context.fillText( - "Column: " + tile.x, - tile.position.x + 10, - tile.position.y + 30 - ); - drawer.context.fillText( - "Row: " + tile.y, - tile.position.x + 10, - tile.position.y + 40 - ); - drawer.context.fillText( - "Order: " + i + " of " + count, - tile.position.x + 10, - tile.position.y + 50 - ); - drawer.context.fillText( - "Size: " + tile.size.toString(), - tile.position.x + 10, - tile.position.y + 60 - ); - drawer.context.fillText( - "Position: " + tile.position.toString(), - tile.position.x + 10, - tile.position.y + 70 - ); - restoreRotationChanges( tile, drawer.canvas, drawer.context ); - drawer.context.restore(); - } -} - - }( OpenSeadragon )); diff --git a/src/imageLoader.js b/src/imageloader.js similarity index 89% rename from src/imageLoader.js rename to src/imageloader.js index 9426fefb..d1f9e785 100644 --- a/src/imageLoader.js +++ b/src/imageloader.js @@ -3,7 +3,7 @@ * * 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: @@ -34,23 +34,14 @@ (function( $ ){ -/** - * @private - * @class ImageJob - * @classdesc Handles loading a single image for use in a single {@link OpenSeadragon.Tile}. - * - * @memberof OpenSeadragon - * @param {String} source - URL of image to download. - * @param {String} crossOriginPolicy - CORS policy to use for downloads - * @param {Function} callback - Called once image has finished downloading. - */ +// private class function ImageJob ( options ) { - + $.extend( true, this, { timeout: $.DEFAULT_SETTINGS.timeout, jobId: null }, options ); - + /** * Image object which will contain downloaded image. * @member {Image} image @@ -60,11 +51,6 @@ function ImageJob ( options ) { } ImageJob.prototype = { - - /** - * Initiates downloading of associated image. - * @method - */ start: function(){ var _this = this; @@ -104,21 +90,25 @@ ImageJob.prototype = { }; /** - * @class + * @class ImageLoader + * @memberof OpenSeadragon * @classdesc Handles downloading of a set of images using asynchronous queue pattern. + * You generally won't have to interact with the ImageLoader directly. + * @param {Object} options - Options for this ImageLoader. + * @param {Number} [options.jobLimit] - The number of concurrent image requests. See imageLoaderLimit in {@link OpenSeadragon.Options} for details. */ -$.ImageLoader = function() { - +$.ImageLoader = function( options ) { + $.extend( true, this, { jobLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, jobQueue: [], jobsInProgress: 0 - }); + }, options ); }; -$.ImageLoader.prototype = { - +$.ImageLoader.prototype = /** @lends OpenSeadragon.ImageLoader.prototype */{ + /** * Add an unloaded image to the loader queue. * @method @@ -143,7 +133,7 @@ $.ImageLoader.prototype = { this.jobsInProgress++; } else { - this.jobQueue.push( newJob ); + this.jobQueue.push( newJob ); } }, @@ -172,10 +162,10 @@ function completeJob( loader, job, callback ) { if ( (!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) { nextJob = loader.jobQueue.shift(); nextJob.start(); + loader.jobsInProgress++; } callback( job.image ); } }( OpenSeadragon )); - diff --git a/src/navigator.js b/src/navigator.js index 464cd988..23809e9d 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -82,7 +82,7 @@ $.Navigator = function( options ){ options.controlOptions.width = options.width; } } - + } else { this.element = document.getElementById( options.id ); options.controlOptions = { @@ -173,7 +173,10 @@ $.Navigator = function( options ){ options.controlOptions ); - if ( options.controlOptions.anchor != $.ControlAnchor.ABSOLUTE && options.controlOptions.anchor != $.ControlAnchor.NONE ) { + this._resizeWithViewer = options.controlOptions.anchor != $.ControlAnchor.ABSOLUTE && + options.controlOptions.anchor != $.ControlAnchor.NONE; + + if ( this._resizeWithViewer ) { if ( options.width && options.height ) { this.element.style.height = typeof ( options.height ) == "number" ? ( options.height + 'px' ) : options.height; this.element.style.width = typeof ( options.width ) == "number" ? ( options.width + 'px' ) : options.width; @@ -194,8 +197,7 @@ $.Navigator = function( options ){ this.displayRegionContainer.appendChild(this.displayRegion); this.element.getElementsByTagName('div')[0].appendChild(this.displayRegionContainer); - if (options.navigatorRotate) - { + if (options.navigatorRotate) { options.viewer.addHandler("rotate", function (args) { _setTransformRotate(_this.displayRegionContainer, args.degrees); _setTransformRotate(_this.displayRegion, -args.degrees); @@ -213,12 +215,38 @@ $.Navigator = function( options ){ scrollHandler: $.delegate( this, onCanvasScroll ) }); + this.addHandler("reset-size", function() { + if (_this.viewport) { + _this.viewport.goHome(true); + } + }); + + this.addHandler("reset-size", function() { + if (_this.viewport) { + _this.viewport.goHome(true); + } + }); + + viewer.world.addHandler("item-index-change", function(event) { + var item = _this.world.getItemAt(event.previousIndex); + _this.world.setItemIndex(item, event.newIndex); + }); + + viewer.world.addHandler("remove-item", function(event) { + var theirItem = event.item; + var myItem = _this._getMatchingItem(theirItem); + if (myItem) { + _this.world.removeItem(myItem); + } + }); + + this.update(viewer.viewport); }; $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.Navigator.prototype */{ /** - * Used to notify the navigator when its size has changed. + * Used to notify the navigator when its size has changed. * Especially useful when {@link OpenSeadragon.Options}.navigatorAutoResize is set to false and the navigator is resizable. * @function */ @@ -228,23 +256,13 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* (this.container.clientWidth === 0 ? 1 : this.container.clientWidth), (this.container.clientHeight === 0 ? 1 : this.container.clientHeight) ); + if ( !containerSize.equals( this.oldContainerSize ) ) { - var oldBounds = this.viewport.getBounds(); - var oldCenter = this.viewport.getCenter(); this.viewport.resize( containerSize, true ); - var imageHeight = 1 / this.source.aspectRatio; - var newWidth = oldBounds.width <= 1 ? oldBounds.width : 1; - var newHeight = oldBounds.height <= imageHeight ? - oldBounds.height : imageHeight; - var newBounds = new $.Rect( - oldCenter.x - ( newWidth / 2.0 ), - oldCenter.y - ( newHeight / 2.0 ), - newWidth, - newHeight - ); - this.viewport.fitBounds( newBounds, true ); + this.viewport.goHome(true); this.oldContainerSize = containerSize; - this.drawer.update(); + this.drawer.clear(); + this.world.draw(); } } }, @@ -264,55 +282,91 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* bottomright; viewerSize = $.getElementSize( this.viewer.element ); - if ( !viewerSize.equals( this.oldViewerSize ) ) { + if ( this._resizeWithViewer && !viewerSize.equals( this.oldViewerSize ) ) { this.oldViewerSize = viewerSize; - if ( this.maintainSizeRatio ) { + + if ( this.maintainSizeRatio || !this.elementArea) { newWidth = viewerSize.x * this.sizeRatio; newHeight = viewerSize.y * this.sizeRatio; - } - else { + } else { newWidth = Math.sqrt(this.elementArea * (viewerSize.x / viewerSize.y)); newHeight = this.elementArea / newWidth; } + this.element.style.width = Math.round( newWidth ) + 'px'; this.element.style.height = Math.round( newHeight ) + 'px'; + + if (!this.elementArea) { + this.elementArea = newWidth * newHeight; + } + this.updateSize(); } if( viewport && this.viewport ) { bounds = viewport.getBounds( true ); topleft = this.viewport.pixelFromPoint( bounds.getTopLeft(), false ); - bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight(), false ).minus( this.totalBorderWidths ); + bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight(), false ) + .minus( this.totalBorderWidths ); //update style for navigator-box - (function(style) { + var style = this.displayRegion.style; + style.display = this.world.getItemCount() ? 'block' : 'none'; - style.top = Math.round( topleft.y ) + 'px'; - style.left = Math.round( topleft.x ) + 'px'; + style.top = Math.round( topleft.y ) + 'px'; + style.left = Math.round( topleft.x ) + 'px'; - var width = Math.abs( topleft.x - bottomright.x ); - var height = Math.abs( topleft.y - bottomright.y ); - // make sure width and height are non-negative so IE doesn't throw - style.width = Math.round( Math.max( width, 0 ) ) + 'px'; - style.height = Math.round( Math.max( height, 0 ) ) + 'px'; - - }( this.displayRegion.style )); + var width = Math.abs( topleft.x - bottomright.x ); + var height = Math.abs( topleft.y - bottomright.y ); + // make sure width and height are non-negative so IE doesn't throw + style.width = Math.round( Math.max( width, 0 ) ) + 'px'; + style.height = Math.round( Math.max( height, 0 ) ) + 'px'; } }, - open: function( source ) { - this.updateSize(); - var containerSize = this.viewer.viewport.containerSize.times( this.sizeRatio ); - var ts = source.getTileSize(source.maxLevel); - if ( ts > containerSize.x || ts > containerSize.y ) { - this.minPixelRatio = Math.min( containerSize.x, containerSize.y ) / ts; - } else { - this.minPixelRatio = this.viewer.minPixelRatio; - } - return $.Viewer.prototype.open.apply( this, [ source ] ); - } + // overrides Viewer.addTiledImage + addTiledImage: function(options) { + var _this = this; + var original = options.originalTiledImage; + delete options.original; + + var optionsClone = $.extend({}, options, { + success: function(event) { + var myItem = event.item; + myItem._originalForNavigator = original; + _this._matchBounds(myItem, original, true); + + original.addHandler('bounds-change', function() { + _this._matchBounds(myItem, original); + }); + } + }); + + return $.Viewer.prototype.addTiledImage.apply(this, [optionsClone]); + }, + + // private + _getMatchingItem: function(theirItem) { + var count = this.world.getItemCount(); + var item; + for (var i = 0; i < count; i++) { + item = this.world.getItemAt(i); + if (item._originalForNavigator === theirItem) { + return item; + } + } + + return null; + }, + + // private + _matchBounds: function(myItem, theirItem, immediately) { + var bounds = theirItem.getBounds(); + myItem.setPosition(bounds.getTopLeft(), immediately); + myItem.setWidth(bounds.width, immediately); + } }); /** diff --git a/src/openseadragon.js b/src/openseadragon.js index f56f10da..702d6976 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -126,19 +126,13 @@ * The element to append the viewer's container element to. If not provided, the 'id' property must be provided. * If both the element and id properties are specified, the viewer is appended to the element provided in the element property. * - * @property {Number} [tabIndex=0] - * Tabbing order index to assign to the viewer element. Positive values are selected in increasing order. When tabIndex is 0 - * source order is used. A negative value omits the viewer from the tabbing order. + * @property {Array|String|Function|Object} [tileSources=null] + * Tile source(s) to open initially. This is a complex parameter; see + * {@link OpenSeadragon.Viewer#open} for details. * - * @property {Array|String|Function|Object[]|Array[]|String[]|Function[]} [tileSources=null] - * As an Array, the tileSource can hold either Objects or mixed - * types of Arrays of Objects, Strings, or Functions. When a value is a String, - * the tileSource is used to create a {@link OpenSeadragon.DziTileSource}. - * When a value is a Function, the function is used to create a new - * {@link OpenSeadragon.TileSource} whose abstract method - * getUrl( level, x, y ) is implemented by the function. Finally, when it - * is an Array of objects, it is used to create a - * {@link OpenSeadragon.LegacyTileSource}. + * @property {Number} [tabIndex=0] + * Tabbing order index to assign to the viewer element. Positive values are selected in increasing order. When tabIndex is 0 + * source order is used. A negative value omits the viewer from the tabbing order. * * @property {Array} overlays Array of objects defining permanent overlays of * the viewer. The overlays added via this option and later removed with @@ -215,9 +209,6 @@ * @property {Number} [opacity=1] * Opacity of the drawer (1=opaque, 0=transparent) * - * @property {Number} [layersAspectRatioEpsilon=0.0001] - * Maximum aspectRatio mismatch between 2 layers. - * * @property {Number} [degrees=0] * Initial rotation. * @@ -265,10 +256,14 @@ * @property {Number} [visibilityRatio=0.5] * The percentage ( as a number from 0 to 1 ) of the source image which * must be kept within the viewport. If the image is dragged beyond that - * limit, it will 'bounce' back until the minimum visibility ration is + * limit, it will 'bounce' back until the minimum visibility ratio is * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to * true will provide the effect of an infinitely scrolling viewport. * + * @property {Object} [viewportMargins={}] + * Pushes the "home" region in from the sides by the specified amounts. + * 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 * it is set to 0 allowing the browser to make the maximum number of @@ -455,8 +450,8 @@ * this setting when set to false. * * @property {Boolean} [showSequenceControl=true] - * If the viewer has been configured with a sequence of tile sources, then - * provide buttons for navigating forward and backward through the images. + * If sequenceMode is true, then provide buttons for navigating forward and + * backward through the images. * * @property {OpenSeadragon.ControlAnchor} [sequenceControlAnchor=TOP_LEFT] * Placement of the default sequence controls. @@ -514,26 +509,29 @@ * To only change the button images, consider using * {@link OpenSeadragon.Options.navImages} * + * @property {Boolean} [sequenceMode=false] + * Set to true to have the viewer treat your tilesources as a sequence of images to + * be opened one at a time rather than all at once. + * * @property {Number} [initialPage=0] - * If the viewer has been configured with a sequence of tile sources, display this page initially. + * If sequenceMode is true, display this page initially. * * @property {Boolean} [preserveViewport=false] - * If the viewer has been configured with a sequence of tile sources, then - * normally navigating through each image resets the viewport to 'home' - * position. If preserveViewport is set to true, then the viewport position - * is preserved when navigating between images in the sequence. + * If sequenceMode is true, then normally navigating to through each image resets the + * viewport to 'home' position. If preserveViewport is set to true, then the viewport + * position is preserved when navigating between images in the sequence. * * @property {Boolean} [preserveOverlays=false] - * If the viewer has been configured with a sequence of tile sources, then - * normally navigating through each image resets the overlays. + * If sequenceMode is true, then normally navigating to through each image + * resets the overlays. * If preserveOverlays is set to true, then the overlays * are preserved when navigating between images in the sequence. * Note: setting preserveOverlays overrides any overlays specified in the * "overlays" property. * * @property {Boolean} [showReferenceStrip=false] - * If the viewer has been configured with a sequence of tile sources, then - * display a scrolling strip of image thumbnails for navigating through the images. + * If sequenceMode is true, then display a scrolling strip of image thumbnails for + * navigating through the images. * * @property {String} [referenceStripScroll='horizontal'] * @@ -548,16 +546,29 @@ * @property {Number} [referenceStripSizeRatio=0.2] * * @property {Boolean} [collectionMode=false] + * Set to true to have the viewer arrange your TiledImages in a grid or line. * * @property {Number} [collectionRows=3] + * 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 {String} [collectionLayout='horizontal'] + * If collectionMode is true, specifies whether to arrange vertically or horizontally. * * @property {Number} [collectionTileSize=800] + * If collectionMode is true, specifies the size, in viewport coordinates, for each TiledImage to fit into. + * The TiledImage will be centered within a square of the specified size. + * + * @property {Number} [collectionTileMargin=80] + * If collectionMode is true, specifies the margin, in viewport coordinates, between each TiledImage. * * @property {String|Boolean} [crossOriginPolicy=false] - * Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will - * not use CORS, and the canvas will be tainted. + * Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will + * not use CORS, and the canvas will be tainted. + * + * @property {Boolean} [ajaxWithCredentials=false] + * Whether to set the withCredentials XHR flag for AJAX requests (when loading tile sources). + * Note that this can be overridden at the {@link OpenSeadragon.TileSource} level. * */ @@ -809,6 +820,25 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ canvasElement.getContext( '2d' ) ); }()); + /** + * 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 + * @memberof OpenSeadragon + */ + $.pixelDensityRatio = (function () { + if ( $.supportsCanvas ) { + var context = document.createElement('canvas').getContext('2d'); + var devicePixelRatio = window.devicePixelRatio || 1; + var backingStoreRatio = context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + return devicePixelRatio / backingStoreRatio; + } else { + return 1; + } + }()); }( OpenSeadragon )); @@ -916,6 +946,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ tileHost: null, initialPage: 0, crossOriginPolicy: false, + ajaxWithCredentials: false, //PAN AND ZOOM SETTINGS AND CONSTRAINTS panHorizontal: true, @@ -988,9 +1019,6 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ // APPEARANCE opacity: 1, - // LAYERS SETTINGS - layersAspectRatioEpsilon: 0.0001, - //REFERENCE STRIP SETTINGS showReferenceStrip: false, referenceStripScroll: 'horizontal', @@ -1005,6 +1033,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ collectionLayout: 'horizontal', //vertical collectionMode: false, collectionTileSize: 800, + collectionTileMargin: 80, //PERFORMANCE SETTINGS imageLoaderLimit: 0, @@ -1924,13 +1953,25 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ /** * Makes an AJAX request. - * @function - * @param {String} url - the url to request - * @param {Function} onSuccess - a function to call on a successful response - * @param {Function} onError - a function to call on when an error occurs + * @param {Object} options + * @param {String} options.url - the url to request + * @param {Function} options.success - a function to call on a successful response + * @param {Function} options.error - a function to call on when an error occurs + * @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials * @throws {Error} */ makeAjaxRequest: function( url, onSuccess, onError ) { + var withCredentials; + + // Note that our preferred API is that you pass in a single object; the named + // arguments are for legacy support. + if( $.isPlainObject( url ) ){ + onSuccess = url.success; + onError = url.error; + withCredentials = url.withCredentials; + url = url.url; + } + var protocol = $.getUrlProtocol( url ); var request = $.createAjaxRequest( protocol === "file:" ); @@ -1957,6 +1998,10 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ } }; + if (withCredentials) { + request.withCredentials = true; + } + try { request.open( "GET", url, true ); request.send( null ); @@ -2271,7 +2316,8 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ debug: nullfunction, info: nullfunction, warn: nullfunction, - error: nullfunction + error: nullfunction, + assert: nullfunction }; diff --git a/src/overlay.js b/src/overlay.js index c59d8c35..0bdbbf34 100644 --- a/src/overlay.js +++ b/src/overlay.js @@ -106,7 +106,7 @@ placement: placement }; } - + this.element = options.element; this.scales = options.location instanceof $.Rect; this.bounds = new $.Rect( diff --git a/src/point.js b/src/point.js index 38aad1f1..1ceef296 100644 --- a/src/point.js +++ b/src/point.js @@ -60,6 +60,13 @@ $.Point = function( x, y ) { }; $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ + /** + * @function + * @returns {OpenSeadragon.Point} a duplicate of this Point + */ + clone: function() { + return new $.Point(this.x, this.y); + }, /** * Add another Point to this point and return a new Point. @@ -189,7 +196,7 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ * @returns {String} A string representation of this point. */ toString: function() { - return "(" + Math.round(this.x) + "," + Math.round(this.y) + ")"; + return "(" + (Math.round(this.x * 100) / 100) + "," + (Math.round(this.y * 100) / 100) + ")"; } }; diff --git a/src/rectangle.js b/src/rectangle.js index 4507eb08..5d3495af 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -75,6 +75,13 @@ $.Rect = function( x, y, width, height ) { }; $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ + /** + * @function + * @returns {OpenSeadragon.Rect} a duplicate of this Rect + */ + clone: function() { + return new $.Rect(this.x, this.y, this.width, this.height); + }, /** * The aspect ratio is simply the ratio of width to height. @@ -194,6 +201,21 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ ); }, + /** + * Returns the smallest rectangle that will contain this and the given rectangle. + * @param {OpenSeadragon.Rect} rect + * @return {OpenSeadragon.Rect} The new rectangle. + */ + // ---------- + union: function(rect) { + var left = Math.min(this.x, rect.x); + var top = Math.min(this.y, rect.y); + var right = Math.max(this.x + this.width, rect.x + rect.width); + var bottom = Math.max(this.y + this.height, rect.y + rect.height); + + return new OpenSeadragon.Rect(left, top, right - left, bottom - top); + }, + /** * Rotates a rectangle around a point. Currently only 90, 180, and 270 * degrees are supported. @@ -257,10 +279,10 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ */ toString: function() { return "[" + - Math.round(this.x*100) + "," + - Math.round(this.y*100) + "," + - Math.round(this.width*100) + "x" + - Math.round(this.height*100) + + (Math.round(this.x*100) / 100) + "," + + (Math.round(this.y*100) / 100) + "," + + (Math.round(this.width*100) / 100) + "x" + + (Math.round(this.height*100) / 100) + "]"; } }; diff --git a/src/referencestrip.js b/src/referencestrip.js index 480009ec..97833e58 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -290,6 +290,13 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp return true; } return false; + }, + + // Overrides Viewer.destroy + destroy: function() { + if (this.element) { + this.element.parentNode.removeChild(this.element); + } } } ); @@ -470,7 +477,7 @@ function loadPanels( strip, viewerSize, scroll ) { */ function onStripEnter( event ) { var element = event.eventSource.element; - + //$.setElementOpacity(element, 0.8); //element.style.border = '1px solid #555'; @@ -498,7 +505,7 @@ function onStripEnter( event ) { */ function onStripExit( event ) { var element = event.eventSource.element; - + if ( 'horizontal' == this.scroll ) { //element.style.paddingTop = "10px"; diff --git a/src/spring.js b/src/spring.js index 4f92dfcb..3320ecc2 100644 --- a/src/spring.js +++ b/src/spring.js @@ -117,10 +117,8 @@ $.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{ * @param {Number} target */ resetTo: function( target ) { - this.target.value = target; - this.target.time = this.current.time; - this.start.value = this.target.value; - this.start.time = this.target.time; + this.start.value = this.target.value = this.current.value = target; + this.start.time = this.target.time = this.current.time = $.now(); }, /** diff --git a/src/tile.js b/src/tile.js index 9aa4bde4..756d9d4a 100644 --- a/src/tile.js +++ b/src/tile.js @@ -33,7 +33,7 @@ */ (function( $ ){ - var TILE_CACHE = {}; + /** * @class Tile * @memberof OpenSeadragon @@ -231,7 +231,8 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ * Renders the tile in a canvas-based context. * @function * @param {Canvas} context - * @param {Function} method for firing the drawing event. drawingHandler({context, tile, rendered}) + * @param {Function} drawingHandler - Method for firing the drawing event. + * drawingHandler({context, tile, rendered}) * where rendered is the context with the pre-drawn image. */ drawCanvas: function( context, drawingHandler ) { @@ -241,16 +242,23 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ rendered, canvas; - if ( !this.loaded || !( this.image || TILE_CACHE[ this.url ] ) ){ + if (!this.cacheImageRecord) { + $.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) ){ $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); + return; } - context.globalAlpha = this.opacity; - //context.save(); + context.globalAlpha = this.opacity; //if we are supposed to be rendering fully opaque rectangle, //ie its done fading or fading is turned off, and if we are drawing @@ -260,46 +268,40 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ //clearing only the inside of the rectangle occupied //by the png prevents edge flikering context.clearRect( - position.x+1, - position.y+1, - size.x-2, - size.y-2 + (position.x * $.pixelDensityRatio)+1, + (position.y * $.pixelDensityRatio)+1, + (size.x * $.pixelDensityRatio)-2, + (size.y * $.pixelDensityRatio)-2 ); } - if( !TILE_CACHE[ this.url ] ){ + 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 ); - TILE_CACHE[ this.url ] = rendered; + 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; } - rendered = TILE_CACHE[ this.url ]; - // This gives the application a chance to make image manipulation changes as we are rendering the image drawingHandler({context: context, tile: this, rendered: rendered}); - //rendered.save(); context.drawImage( rendered.canvas, 0, 0, rendered.canvas.width, rendered.canvas.height, - position.x, - position.y, - size.x, - size.y + position.x * $.pixelDensityRatio, + position.y * $.pixelDensityRatio, + size.x * $.pixelDensityRatio, + size.y * $.pixelDensityRatio ); - //rendered.restore(); - - //context.restore(); }, /** @@ -313,9 +315,6 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ if ( this.element && this.element.parentNode ) { this.element.parentNode.removeChild( this.element ); } - if ( TILE_CACHE[ this.url ]){ - delete TILE_CACHE[ this.url ]; - } this.element = null; this.imgElement = null; diff --git a/src/tilecache.js b/src/tilecache.js new file mode 100644 index 00000000..45344a05 --- /dev/null +++ b/src/tilecache.js @@ -0,0 +1,238 @@ +/* + * OpenSeadragon - TileCache + * + * 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( $ ){ + +// private class +var TileRecord = function( options ) { + $.console.assert( options, "[TileCache.cacheTile] options is required" ); + $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" ); + $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" ); + this.tile = options.tile; + this.tiledImage = options.tiledImage; +}; + +// private class +var ImageRecord = function(options) { + $.console.assert( options, "[ImageRecord] options is required" ); + $.console.assert( options.image, "[ImageRecord] options.image is required" ); + this._image = options.image; + this._tiles = []; +}; + +ImageRecord.prototype = { + destroy: function() { + this._image = null; + this._renderedContext = null; + this._tiles = null; + }, + + getImage: function() { + return this._image; + }, + + getRenderedContext: function() { + return this._renderedContext; + }, + + setRenderedContext: function(renderedContext) { + this._renderedContext = renderedContext; + }, + + addTile: function(tile) { + $.console.assert(tile, '[ImageRecord.addTile] tile is required'); + this._tiles.push(tile); + }, + + removeTile: function(tile) { + for (var i = 0; i < this._tiles.length; i++) { + if (this._tiles[i] === tile) { + this._tiles.splice(i, 1); + return; + } + } + + $.console.warn('[ImageRecord.removeTile] trying to remove unknown tile', tile); + }, + + getTileCount: function() { + return this._tiles.length; + } +}; + +/** + * @class TileCache + * @memberof OpenSeadragon + * @classdesc Stores all the tiles displayed in a {@link OpenSeadragon.Viewer}. + * You generally won't have to interact with the TileCache directly. + * @param {Object} options - Configuration for this TileCache. + * @param {Number} [options.maxImageCacheCount] - See maxImageCacheCount in + * {@link OpenSeadragon.Options} for details. + */ +$.TileCache = function( options ) { + options = options || {}; + + this._maxImageCacheCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount; + this._tilesLoaded = []; + this._imagesLoaded = []; + this._imagesLoadedCount = 0; +}; + +$.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ + /** + * @returns {Number} The total number of tiles that have been loaded by + * this TileCache. + */ + numTilesLoaded: function() { + return this._tilesLoaded.length; + }, + + /** + * Caches the specified tile, removing an old tile if necessary to stay under the + * maxImageCacheCount specified on construction. Note that if multiple tiles reference + * the same image, there may be more tiles than maxImageCacheCount; the goal is to keep + * the number of images below that number. Note, as well, that even the number of images + * 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 {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 + * tiles will not be released. + */ + cacheTile: function( options ) { + $.console.assert( options, "[TileCache.cacheTile] options is required" ); + $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" ); + $.console.assert( options.tile.url, "[TileCache.cacheTile] options.tile.url is required" ); + $.console.assert( options.tile.image, "[TileCache.cacheTile] options.tile.image is required" ); + $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" ); + + var cutoff = options.cutoff || 0; + var insertionIndex = this._tilesLoaded.length; + + var imageRecord = this._imagesLoaded[options.tile.url]; + if (!imageRecord) { + imageRecord = this._imagesLoaded[options.tile.url] = new ImageRecord({ + image: options.tile.image + }); + + this._imagesLoadedCount++; + } + + imageRecord.addTile(options.tile); + options.tile.cacheImageRecord = imageRecord; + + // Note that just because we're unloading a tile doesn't necessarily mean + // we're unloading an image. With repeated calls it should sort itself out, though. + if ( this._imagesLoadedCount > this._maxImageCacheCount ) { + var worstTile = null; + var worstTileIndex = -1; + var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord; + + for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) { + prevTileRecord = this._tilesLoaded[ i ]; + prevTile = prevTileRecord.tile; + + if ( prevTile.level <= cutoff || prevTile.beingDrawn ) { + continue; + } else if ( !worstTile ) { + worstTile = prevTile; + worstTileIndex = i; + continue; + } + + prevTime = prevTile.lastTouchTime; + worstTime = worstTile.lastTouchTime; + prevLevel = prevTile.level; + worstLevel = worstTile.level; + + if ( prevTime < worstTime || + ( prevTime == worstTime && prevLevel > worstLevel ) ) { + worstTile = prevTile; + worstTileIndex = i; + } + } + + if ( worstTile && worstTileIndex >= 0 ) { + this._unloadTile(worstTile); + insertionIndex = worstTileIndex; + } + } + + this._tilesLoaded[ insertionIndex ] = new TileRecord({ + tile: options.tile, + tiledImage: options.tiledImage + }); + }, + + /** + * Clears all tiles associated with the specified tiledImage. + * @param {OpenSeadragon.TiledImage} tiledImage + */ + clearTilesFor: function( tiledImage ) { + $.console.assert(tiledImage, '[TileCache.clearTilesFor] tiledImage is required'); + var tileRecord; + for ( var i = 0; i < this._tilesLoaded.length; ++i ) { + tileRecord = this._tilesLoaded[ i ]; + if ( tileRecord.tiledImage === tiledImage ) { + this._unloadTile(tileRecord.tile); + this._tilesLoaded.splice( i, 1 ); + i--; + } + } + }, + + // private + getImageRecord: function(url) { + $.console.assert(url, '[TileCache.getImageRecord] url is required'); + return this._imagesLoaded[url]; + }, + + // private + _unloadTile: function(tile) { + $.console.assert(tile, '[TileCache._unloadTile] tile is required'); + tile.unload(); + tile.cacheImageRecord = null; + + var imageRecord = this._imagesLoaded[tile.url]; + imageRecord.removeTile(tile); + if (!imageRecord.getTileCount()) { + imageRecord.destroy(); + delete this._imagesLoaded[tile.url]; + this._imagesLoadedCount--; + } + } +}; + +}( OpenSeadragon )); diff --git a/src/tiledimage.js b/src/tiledimage.js new file mode 100644 index 00000000..ba587d07 --- /dev/null +++ b/src/tiledimage.js @@ -0,0 +1,1149 @@ +/* + * OpenSeadragon - TiledImage + * + * 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( $ ){ + +/** + * You shouldn't have to create a TiledImage directly; use {@link OpenSeadragon.Viewer#open} + * or {@link OpenSeadragon.Viewer#addTiledImage} instead. + * @class TiledImage + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * A new instance is created for each TileSource opened. + * @param {Object} options - Configuration for this TiledImage. + * @param {OpenSeadragon.TileSource} options.source - The TileSource that defines this TiledImage. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this TiledImage. + * @param {OpenSeadragon.TileCache} options.tileCache - The TileCache for this TiledImage to use. + * @param {OpenSeadragon.Drawer} options.drawer - The Drawer for this TiledImage to draw onto. + * @param {OpenSeadragon.ImageLoader} options.imageLoader - The ImageLoader for this TiledImage to use. + * @param {Number} [options.x=0] - Left position, in viewport coordinates. + * @param {Number} [options.y=0] - Top position, in viewport coordinates. + * @param {Number} [options.width=1] - Width, in viewport coordinates. + * @param {Number} [options.height] - Height, in viewport coordinates. + * @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.immediateRender] - See {@link OpenSeadragon.Options}. + * @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 {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. + * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. + */ +$.TiledImage = function( options ) { + var _this = this; + + $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); + $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); + $.console.assert( options.viewer, "[TiledImage] options.viewer is required" ); + $.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" ); + $.console.assert( options.source, "[TiledImage] options.source is required" ); + + $.EventSource.call( this ); + + this._tileCache = options.tileCache; + delete options.tileCache; + + this._drawer = options.drawer; + delete options.drawer; + + this._imageLoader = options.imageLoader; + delete options.imageLoader; + + var x = options.x || 0; + delete options.x; + var y = options.y || 0; + delete options.y; + + // Ratio of zoomable image height to width. + this.normHeight = options.source.dimensions.y / options.source.dimensions.x; + this.contentAspectX = options.source.dimensions.x / options.source.dimensions.y; + + var scale = 1; + if ( options.width ) { + scale = options.width; + delete options.width; + + if ( options.height ) { + $.console.error( "specifying both width and height to a tiledImage is not supported" ); + delete options.height; + } + } else if ( options.height ) { + scale = options.height / this.normHeight; + delete options.height; + } + + $.extend( true, this, { + + //internal state properties + viewer: null, + tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. + 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? + + //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 + + }, options ); + + this._xSpring = new $.Spring({ + initial: x, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._ySpring = new $.Spring({ + initial: y, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._scaleSpring = new $.Spring({ + initial: scale, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._updateForScale(); + + // We need a callback to give image manipulation a chance to happen + this._drawingHandler = function(args) { + /** + * This event is fired just before the tile is drawn giving the application a chance to alter the image. + * + * NOTE: This event is only fired when the drawer is using a . + * + * @event tile-drawing + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Tile} tile - The Tile being drawn. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. + * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.viewer.raiseEvent('tile-drawing', $.extend({ + tiledImage: _this + }, args)); + }; +}; + +$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + needsDraw: function() { + return this._needsDraw; + }, + + /** + * Clears all tiles and triggers an update on the next call to + * {@link OpenSeadragon.TiledImage#update}. + */ + reset: function() { + this._tileCache.clearTilesFor(this); + this.lastResetTime = $.now(); + this._needsDraw = true; + }, + + /** + * Updates the TiledImage's bounds, animating if needed. + * @returns {Boolean} Whether the TiledImage animated. + */ + update: function() { + var oldX = this._xSpring.current.value; + var oldY = this._ySpring.current.value; + var oldScale = this._scaleSpring.current.value; + + this._xSpring.update(); + this._ySpring.update(); + this._scaleSpring.update(); + + if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY || + this._scaleSpring.current.value !== oldScale) { + this._updateForScale(); + this._needsDraw = true; + return true; + } + + return false; + }, + + /** + * Draws the TiledImage to its Drawer. + */ + draw: function() { + this._midDraw = true; + updateViewport( this ); + this._midDraw = false; + }, + + /** + * Destroy the TiledImage (unload current loaded tiles). + */ + destroy: function() { + this.reset(); + }, + + /** + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + * @param {Boolean} [current=false] - Pass true for the current location; false for target location. + */ + getBounds: function(current) { + if (current) { + return new $.Rect( this._xSpring.current.value, this._ySpring.current.value, + this._worldWidthCurrent, this._worldHeightCurrent ); + } + + return new $.Rect( this._xSpring.target.value, this._ySpring.target.value, + this._worldWidthTarget, this._worldHeightTarget ); + }, + + // deprecated + getWorldBounds: function() { + $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); + return this.getBounds(); + }, + + /** + * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. + */ + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + }, + + // private + _viewportToImageDelta: function( viewerX, viewerY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point(viewerX * (this.source.dimensions.x / scale), + viewerY * ((this.source.dimensions.y * this.contentAspectX) / scale)); + }, + + /** + * Translates from OpenSeadragon viewer coordinate system to image coordinate system. + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system. + * @param {Number} [viewerY] - The Y coordinate in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the image. + */ + viewportToImageCoordinates: function( viewerX, viewerY, current ) { + if (viewerX instanceof $.Point) { + //they passed a point instead of individual components + current = viewerY; + viewerY = viewerX.y; + viewerX = viewerX.x; + } + + if (current) { + return this._viewportToImageDelta(viewerX - this._xSpring.current.value, + viewerY - this._ySpring.current.value); + } + + return this._viewportToImageDelta(viewerX - this._xSpring.target.value, + viewerY - this._ySpring.target.value); + }, + + // private + _imageToViewportDelta: function( imageX, imageY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point((imageX / this.source.dimensions.x) * scale, + (imageY / this.source.dimensions.y / this.contentAspectX) * scale); + }, + + /** + * Translates from image coordinate system to OpenSeadragon viewer coordinate system + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system. + * @param {Number} [imageY] - The Y coordinate in image coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function( imageX, imageY, current ) { + if (imageX instanceof $.Point) { + //they passed a point instead of individual components + current = imageY; + imageY = imageX.y; + imageX = imageX.x; + } + + var point = this._imageToViewportDelta(imageX, imageY); + if (current) { + point.x += this._xSpring.current.value; + point.y += this._ySpring.current.value; + } else { + point.x += this._xSpring.target.value; + point.y += this._ySpring.target.value; + } + + return point; + }, + + /** + * Translates from a rectangle which describes a portion of the image in + * pixel coordinates to OpenSeadragon viewport rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system. + * @param {Number} [imageY] - The top coordinate in image coordinate system. + * @param {Number} [pixelWidth] - The width in pixel of the rectangle. + * @param {Number} [pixelHeight] - The height in pixel of the rectangle. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. + */ + imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) { + if (imageX instanceof $.Rect) { + //they passed a rect instead of individual components + current = imageY; + pixelWidth = imageX.width; + pixelHeight = imageX.height; + imageY = imageX.y; + imageX = imageX.x; + } + + var coordA = this.imageToViewportCoordinates(imageX, imageY, current); + var coordB = this._imageToViewportDelta(pixelWidth, pixelHeight, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y + ); + }, + + /** + * Translates from a rectangle which describes a portion of + * the viewport in point coordinates to image rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system. + * @param {Number} [viewerY] - The top coordinate in viewport coordinate system. + * @param {Number} [pointWidth] - The width in viewport coordinate system. + * @param {Number} [pointHeight] - The height in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Rect} A rect representing the coordinates in the image. + */ + viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight, current ) { + if (viewerX instanceof $.Rect) { + //they passed a rect instead of individual components + current = viewerY; + pointWidth = viewerX.width; + pointHeight = viewerX.height; + viewerY = viewerX.y; + viewerX = viewerX.x; + } + + var coordA = this.viewportToImageCoordinates(viewerX, viewerY, current); + var coordB = this._viewportToImageDelta(pointWidth, pointHeight, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y + ); + }, + + /** + * Sets the TiledImage's position in the world. + * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setPosition: function(position, immediately) { + var sameTarget = (this._xSpring.target.value === position.x && + this._ySpring.target.value === position.y); + + if (immediately) { + if (sameTarget && this._xSpring.current.value === position.x && + this._ySpring.current.value === position.y) { + return; + } + + this._xSpring.resetTo(position.x); + this._ySpring.resetTo(position.y); + } else { + if (sameTarget) { + return; + } + + this._xSpring.springTo(position.x); + this._ySpring.springTo(position.y); + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + /** + * Sets the TiledImage's width in the world, adjusting the height to match based on aspect ratio. + * @param {Number} width - The new width, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setWidth: function(width, immediately) { + this._setScale(width, immediately); + }, + + /** + * Sets the TiledImage's height in the world, adjusting the width to match based on aspect ratio. + * @param {Number} height - The new height, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setHeight: function(height, immediately) { + this._setScale(height / this.normHeight, immediately); + }, + + // private + _setScale: function(scale, immediately) { + var sameTarget = (this._scaleSpring.target.value === scale); + if (immediately) { + if (sameTarget && this._scaleSpring.current.value === scale) { + return; + } + + this._scaleSpring.resetTo(scale); + this._updateForScale(); + } else { + if (sameTarget) { + return; + } + + this._scaleSpring.springTo(scale); + this._updateForScale(); + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + // private + _updateForScale: function() { + this._worldWidthTarget = this._scaleSpring.target.value; + this._worldHeightTarget = this.normHeight * this._scaleSpring.target.value; + this._worldWidthCurrent = this._scaleSpring.current.value; + this._worldHeightCurrent = this.normHeight * this._scaleSpring.current.value; + }, + + // private + _raiseBoundsChange: function() { + /** + * Raised when the TiledImage's bounds are changed. + * Note that this event is triggered only when the animation target is changed; + * not for every frame of animation. + * @event bounds-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('bounds-change'); + } +}); + +/** + * @private + * @inner + * Pretty much every other line in this needs to be documented so it's clear + * how each piece of this routine contributes to the drawing process. That's + * why there are so many TODO's inside this function. + */ +function updateViewport( tiledImage ) { + + tiledImage._needsDraw = false; + + var tile, + level, + best = null, + haveDrawn = false, + currentTime = $.now(), + viewportBounds = tiledImage.viewport.getBoundsWithMargins( true ), + zeroRatioC = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( 0 ), + true + ).x * tiledImage._scaleSpring.current.value, + lowestLevel = Math.max( + tiledImage.source.minLevel, + Math.floor( + Math.log( tiledImage.minZoomImageRatio ) / + Math.log( 2 ) + ) + ), + highestLevel = Math.min( + Math.abs(tiledImage.source.maxLevel), + Math.abs(Math.floor( + Math.log( zeroRatioC / tiledImage.minPixelRatio ) / + Math.log( 2 ) + )) + ), + degrees = tiledImage.viewport.degrees, + renderPixelRatioC, + renderPixelRatioT, + zeroRatioT, + optimalRatio, + levelOpacity, + levelVisibility; + + viewportBounds.x -= tiledImage._xSpring.current.value; + viewportBounds.y -= tiledImage._ySpring.current.value; + + // Reset tile's internal drawn state + while ( tiledImage.lastDrawn.length > 0 ) { + tile = tiledImage.lastDrawn.pop(); + tile.beingDrawn = false; + } + + //Change bounds for rotation + if (degrees === 90 || degrees === 270) { + viewportBounds = viewportBounds.rotate( degrees ); + } else if (degrees !== 0 && degrees !== 180) { + // This is just an approximation. + var orthBounds = viewportBounds.rotate(90); + viewportBounds.x -= orthBounds.width / 2; + viewportBounds.y -= orthBounds.height / 2; + viewportBounds.width += orthBounds.width; + viewportBounds.height += orthBounds.height; + } + + var viewportTL = viewportBounds.getTopLeft(); + var viewportBR = viewportBounds.getBottomRight(); + + //Don't draw if completely outside of the viewport + if ( !tiledImage.wrapHorizontal && (viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidthCurrent ) ) { + return; + } + + if ( !tiledImage.wrapVertical && ( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeightCurrent ) ) { + return; + } + + // Calculate viewport rect / bounds + if ( !tiledImage.wrapHorizontal ) { + viewportTL.x = Math.max( viewportTL.x, 0 ); + viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidthCurrent ); + } + + if ( !tiledImage.wrapVertical ) { + viewportTL.y = Math.max( viewportTL.y, 0 ); + viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeightCurrent ); + } + + // Calculations for the interval of levels to draw + // (above in initial var statement) + // can return invalid intervals; fix that here if necessary + lowestLevel = Math.min( lowestLevel, highestLevel ); + + // Update any level that will be drawn + var drawLevel; // FIXME: drawLevel should have a more explanatory name + for ( level = highestLevel; level >= lowestLevel; level-- ) { + drawLevel = false; + + //Avoid calculations for draw if we have already drawn this + renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( level ), + true + ).x * tiledImage._scaleSpring.current.value; + + if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) || + ( level == lowestLevel ) ) { + drawLevel = true; + haveDrawn = true; + } else if ( !haveDrawn ) { + continue; + } + + //Perform calculations for draw if we haven't drawn this + renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( level ), + false + ).x * tiledImage._scaleSpring.current.value; + + zeroRatioT = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( + Math.max( + tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, + 0 + ) + ), + false + ).x * tiledImage._scaleSpring.current.value; + + optimalRatio = tiledImage.immediateRender ? + 1 : + zeroRatioT; + + levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); + + levelVisibility = optimalRatio / Math.abs( + optimalRatio - renderPixelRatioT + ); + + // Update the level and keep track of 'best' tile to load + best = updateLevel( + tiledImage, + haveDrawn, + drawLevel, + level, + levelOpacity, + levelVisibility, + viewportTL, + viewportBR, + currentTime, + best + ); + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if ( providesCoverage( tiledImage.coverage, level ) ) { + break; + } + } + + // Perform the actual drawing + drawTiles( tiledImage, tiledImage.lastDrawn ); + + // Load the new 'best' tile + if ( best ) { + loadTile( tiledImage, best, currentTime ); + // because we haven't finished drawing, so + tiledImage._needsDraw = true; + } + +} + + +function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ + + var x, y, + tileTL, + tileBR, + numberOfTiles, + viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() ); + + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event update-level + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {Object} havedrawn + * @property {Object} level + * @property {Object} opacity + * @property {Object} visibility + * @property {Object} topleft + * @property {Object} bottomright + * @property {Object} currenttime + * @property {Object} best + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'update-level', { + tiledImage: tiledImage, + havedrawn: haveDrawn, + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + topleft: viewportTL, + bottomright: viewportBR, + currenttime: currentTime, + best: best + }); + } + + //OK, a new drawing so do your calculations + tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scaleSpring.current.value )); + tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scaleSpring.current.value )); + numberOfTiles = tiledImage.source.getNumTiles( level ); + + resetCoverage( tiledImage.coverage, level ); + + if ( !tiledImage.wrapHorizontal ) { + tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); + } + if ( !tiledImage.wrapVertical ) { + tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); + } + + for ( x = tileTL.x; x <= tileBR.x; x++ ) { + for ( y = tileTL.y; y <= tileBR.y; y++ ) { + + best = updateTile( + tiledImage, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + + return best; +} + +function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + tiledImage.source, + tiledImage.tilesMatrix, + currentTime, + numberOfTiles, + tiledImage._worldWidthCurrent, + tiledImage._worldHeightCurrent + ), + drawTile = drawLevel; + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event update-tile + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'update-tile', { + tiledImage: tiledImage, + tile: tile + }); + } + + setCoverage( tiledImage.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( tiledImage.coverage, level, x, y ) ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + tiledImage.source.tileOverlap, + tiledImage.viewport, + viewportCenter, + levelVisibility, + tiledImage + ); + + 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 + }); + } + } + + if ( tile.loaded ) { + var needsDraw = blendTile( + tiledImage, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + if ( needsDraw ) { + tiledImage._needsDraw = true; + } + } else if ( tile.loading ) { + // the tile is already in the download queue + // thanks josh1093 for finally translating this typo + } else { + best = compareTiles( best, tile ); + } + + return best; +} + +function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) { + var xMod, + yMod, + bounds, + exists, + url, + tile; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = tileSource.getTileBounds( level, xMod, yMod ); + exists = tileSource.tileExists( level, xMod, yMod ); + url = tileSource.getTileUrl( level, xMod, yMod ); + + bounds.x += ( x - xMod ) / numTiles.x; + bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +} + +function loadTile( tiledImage, tile, time ) { + tile.loading = true; + tiledImage._imageLoader.addJob({ + src: tile.url, + crossOriginPolicy: tiledImage.crossOriginPolicy, + callback: function( image ){ + onTileLoad( tiledImage, tile, time, image ); + } + }); +} + +function onTileLoad( tiledImage, tile, time, image ) { + if ( !image ) { + $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + if( !tiledImage.debugMode ){ + tile.loading = false; + tile.exists = false; + return; + } + } else if ( time < tiledImage.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + tile.loading = false; + return; + } + + 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 + }); + }; + + // Check if we're mid-update; this can happen on IE8 because image load events for + // cached images happen immediately there + if ( !tiledImage._midDraw ) { + finish(); + } else { + // Wait until after the update, in case caching unloads any tiles + window.setTimeout( finish, 1); + } + + tiledImage._needsDraw = true; +} + + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ + var boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= tiledImage._scaleSpring.current.value; + boundsTL.y *= tiledImage._scaleSpring.current.value; + boundsTL.x += tiledImage._xSpring.current.value; + boundsTL.y += tiledImage._ySpring.current.value; + + var boundsSize = tile.bounds.getSize(); + + boundsSize.x *= tiledImage._scaleSpring.current.value; + boundsSize.y *= tiledImage._scaleSpring.current.value; + + var positionC = viewport.pixelFromPoint( boundsTL, true ), + positionT = viewport.pixelFromPoint( boundsTL, false ), + sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), + sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileDistance = viewportCenter.distanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +} + + +function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( tiledImage.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + tiledImage.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +} + +/** + * @private + * @inner + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + */ +function providesCoverage( coverage, level, x, y ) { + var rows, + cols, + i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); +} + +/** + * @private + * @inner + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +} + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; +} + +/** + * @private + * @inner + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +} + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +} + +function drawTiles( tiledImage, lastDrawn ){ + var i, + tile, + tileKey, + viewer, + viewport, + position, + tileSource; + + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { + tile = lastDrawn[ i ]; + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler ); + tile.beingDrawn = true; + + if( tiledImage.debugMode ){ + try{ + tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); + }catch(e){ + $.console.error(e); + } + } + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } +} + +}( OpenSeadragon )); diff --git a/src/tilesource.js b/src/tilesource.js index deecc742..1c0d29d2 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -38,44 +38,52 @@ /** * @class TileSource * @classdesc The TileSource contains the most basic implementation required to create a - * smooth transition between layer in an image pyramid. It has only a single key - * interface that must be implemented to complete it key functionality: + * smooth transition between layers in an image pyramid. It has only a single key + * interface that must be implemented to complete its key functionality: * 'getTileUrl'. It also has several optional interfaces that can be * implemented if a new TileSource wishes to support configuration via a simple * object or array ('configure') and if the tile source supports or requires - * configuration via retreival of a document on the network ala AJAX or JSONP, + * configuration via retrieval of a document on the network ala AJAX or JSONP, * ('getImageInfo'). * - * By default the image pyramid is split into N layers where the images longest + * By default the image pyramid is split into N layers where the image's longest * side in M (in pixels), where N is the smallest integer which satisfies * 2^(N+1) >= M. * * @memberof OpenSeadragon * @extends OpenSeadragon.EventSource - * @param {Number|Object|Array|String} width - * If more than a single argument is supplied, the traditional use of - * positional parameters is supplied and width is expected to be the width - * source image at its max resolution in pixels. If a single argument is supplied and - * it is an Object or Array, the construction is assumed to occur through - * the extending classes implementation of 'configure'. Finally if only a - * single argument is supplied and it is a String, the extending class is - * expected to implement 'getImageInfo' and 'configure'. - * @param {Number} height + * @param {Object} options + * You can either specify a URL, or literally define the TileSource (by specifying + * width, height, tileSize, tileOverlap, minLevel, and maxLevel). For the former, + * the extending class is expected to implement 'getImageInfo' and 'configure'. + * For the latter, the construction is assumed to occur through + * the extending classes implementation of 'configure'. + * @param {String} [options.url] + * The URL for the data necessary for this TileSource. + * @param {Function} [options.success] + * A function to be called upon successful creation. + * @param {Boolean} [options.ajaxWithCredentials] + * If this TileSource needs to make an AJAX call, this specifies whether to set + * the XHR's withCredentials (for accessing secure data). + * @param {Number} [options.width] * Width of the source image at max resolution in pixels. - * @param {Number} tileSize + * @param {Number} [options.height] + * Height of the source image at max resolution in pixels. + * @param {Number} [options.tileSize] * 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. - * @param {Number} tileOverlap + * @param {Number} [options.tileOverlap] * The number of pixels each tile is expected to overlap touching tiles. - * @param {Number} minLevel + * @param {Number} [options.minLevel] * The minimum level to attempt to load. - * @param {Number} maxLevel + * @param {Number} [options.maxLevel] * The maximum level to attempt to load. */ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { - var callback = null, - args = arguments, + var _this = this; + + var args = arguments, options, i; @@ -102,19 +110,23 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve //source $.extend( true, this, options ); - //Any functions that are passed as arguments are bound to the ready callback - /*jshint loopfunc:true*/ - for ( i = 0; i < arguments.length; i++ ) { - if ( $.isFunction( arguments[ i ] ) ) { - callback = arguments[ i ]; - this.addHandler( 'ready', function ( event ) { - callback( event ); - } ); - //only one callback per constructor - break; + if (!this.success) { + //Any functions that are passed as arguments are bound to the ready callback + for ( i = 0; i < arguments.length; i++ ) { + if ( $.isFunction( arguments[ i ] ) ) { + this.success = arguments[ i ]; + //only one callback per constructor + break; + } } } + if (this.success) { + this.addHandler( 'ready', function ( event ) { + _this.success( event ); + } ); + } + /** * Ratio of width to height * @member {Number} aspectRatio @@ -127,7 +139,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve */ /** * The size of the image tiles used to compose the image. - * Please note that tileSize may be deprecated in a future release. + * 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# @@ -148,12 +160,16 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve * @memberof OpenSeadragon.TileSource# */ /** - * + * * @member {Boolean} ready * @memberof OpenSeadragon.TileSource# */ if( 'string' == $.type( arguments[ 0 ] ) ){ + this.url = arguments[0]; + } + + if (this.url) { //in case the getImageInfo method is overriden and/or implies an //async mechanism set some safe defaults first this.aspectRatio = 1; @@ -165,7 +181,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve this.ready = false; //configuration via url implies the extending class //implements and 'configure' - this.getImageInfo( arguments[ 0 ] ); + this.getImageInfo( this.url ); } else { @@ -185,8 +201,8 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve Math.log( 2 ) ) : 0 ); - if( callback && $.isFunction( callback ) ){ - callback( this ); + if( this.success && $.isFunction( this.success ) ){ + this.success( this ); } } @@ -197,7 +213,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ /** - * Return the tileSize for a given level. + * Return the tileSize for a given level. * Subclasses should override this if tileSizes 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. @@ -355,6 +371,10 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ } options = $TileSource.prototype.configure.apply( _this, [ data, url ]); + if (options.ajaxWithCredentials === undefined) { + options.ajaxWithCredentials = _this.ajaxWithCredentials; + } + readySource = new $TileSource( options ); _this.ready = true; /** @@ -383,45 +403,50 @@ $.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ }); } else { // request info via xhr asynchronously. - $.makeAjaxRequest( url, function( xhr ) { - var data = processResponse( xhr ); - callback( data ); - }, function ( xhr, exc ) { - var msg; + $.makeAjaxRequest( { + url: url, + withCredentials: this.ajaxWithCredentials, + success: function( xhr ) { + var data = processResponse( xhr ); + callback( data ); + }, + error: function ( xhr, exc ) { + var msg; - /* - IE < 10 will block XHR requests to different origins. Any property access on the request - object will raise an exception which we'll attempt to handle by formatting the original - exception rather than the second one raised when we try to access xhr.status - */ - try { - msg = "HTTP " + xhr.status + " attempting to load TileSource"; - } catch ( e ) { - var formattedExc; - if ( typeof( exc ) == "undefined" || !exc.toString ) { - formattedExc = "Unknown error"; - } else { - formattedExc = exc.toString(); + /* + IE < 10 will block XHR requests to different origins. Any property access on the request + object will raise an exception which we'll attempt to handle by formatting the original + exception rather than the second one raised when we try to access xhr.status + */ + try { + msg = "HTTP " + xhr.status + " attempting to load TileSource"; + } catch ( e ) { + var formattedExc; + if ( typeof( exc ) == "undefined" || !exc.toString ) { + formattedExc = "Unknown error"; + } else { + formattedExc = exc.toString(); + } + + msg = formattedExc + " attempting to load TileSource"; } - msg = formattedExc + " attempting to load TileSource"; + /*** + * 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: msg, + source: url + }); } - - /*** - * 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: msg, - source: url - }); }); } diff --git a/src/tilesourcecollection.js b/src/tilesourcecollection.js index 52dcbd67..a180aa52 100644 --- a/src/tilesourcecollection.js +++ b/src/tilesourcecollection.js @@ -34,110 +34,9 @@ (function( $ ){ -/** - * @class TileSourceCollection - * @memberof OpenSeadragon - * @extends OpenSeadragon.TileSource - */ +// deprecated $.TileSourceCollection = function( tileSize, tileSources, rows, layout ) { - var options; - - if( $.isPlainObject( tileSize ) ){ - options = tileSize; - }else{ - options = { - tileSize: arguments[ 0 ], - tileSources: arguments[ 1 ], - rows: arguments[ 2 ], - layout: arguments[ 3 ] - }; - } - - if( !options.layout ){ - options.layout = 'horizontal'; - } - - var minLevel = 0, - levelSize = 1.0, - tilesPerRow = Math.ceil( options.tileSources.length / options.rows ), - longSide = tilesPerRow >= options.rows ? - tilesPerRow : - options.rows; - - if( 'horizontal' == options.layout ){ - options.width = ( options.tileSize ) * tilesPerRow; - options.height = ( options.tileSize ) * options.rows; - } else { - options.height = ( options.tileSize ) * tilesPerRow; - options.width = ( options.tileSize ) * options.rows; - } - - options.tileOverlap = -options.tileMargin; - options.tilesPerRow = tilesPerRow; - - //Set min level to avoid loading sublevels since collection is a - //different kind of abstraction - - while( levelSize < ( options.tileSize ) * longSide ){ - //$.console.log( '%s levelSize %s minLevel %s', options.tileSize * longSide, levelSize, minLevel ); - levelSize = levelSize * 2.0; - minLevel++; - } - options.minLevel = minLevel; - - //for( var name in options ){ - // $.console.log( 'Collection %s %s', name, options[ name ] ); - //} - - $.TileSource.apply( this, [ options ] ); - + $.console.error('TileSourceCollection is deprecated; use World instead'); }; -$.extend( $.TileSourceCollection.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.TileSourceCollection.prototype */{ - - /** - * @function - * @param {Number} level - * @param {Number} x - * @param {Number} y - */ - getTileBounds: function( level, x, y ) { - var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), - px = this.tileSize * x - this.tileOverlap, - py = this.tileSize * y - this.tileOverlap, - sx = this.tileSize + 1 * this.tileOverlap, - sy = this.tileSize + 1 * this.tileOverlap, - scale = 1.0 / dimensionsScaled.x; - - sx = Math.min( sx, dimensionsScaled.x - px ); - sy = Math.min( sy, dimensionsScaled.y - py ); - - return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); - }, - - /** - * - * @function - */ - configure: function( data, url ){ - return; - }, - - - /** - * @function - * @param {Number} level - * @param {Number} x - * @param {Number} y - */ - getTileUrl: function( level, x, y ) { - //$.console.log([ level, '/', x, '_', y ].join( '' )); - return null; - } - - - -}); - - }( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index 85ec594a..60e64e8a 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -35,10 +35,8 @@ (function( $ ){ // dictionary from hash to private properties -var THIS = {}, -// We keep a list of viewers so we can 'wake-up' each viewer on -// a page after toggling between fullpage modes - VIEWERS = {}; +var THIS = {}; +var nextHash = 1; /** * @@ -92,7 +90,7 @@ $.Viewer = function( options ) { //internal state and dom identifiers id: options.id, - hash: options.hash || options.id, + hash: options.hash || nextHash++, //dom nodes /** @@ -112,7 +110,7 @@ $.Viewer = function( options ) { * A <div> element, the element where user-input events are handled for panning and zooming. * Child element of {@link OpenSeadragon.Viewer#container}, * positioned on top of {@link OpenSeadragon.Viewer#keyboardCommandArea}. - * The parent of {@link OpenSeadragon.Drawer#canvas} instances. + * The parent of {@link OpenSeadragon.Drawer#canvas} instances. * @member {Element} canvas * @memberof OpenSeadragon.Viewer# */ @@ -143,9 +141,7 @@ $.Viewer = function( options ) { * @memberof OpenSeadragon.Viewer# */ drawer: null, - drawers: [], - // Container inside the canvas where drawers (layers) are drawn. - drawersContainer: null, + world: null, /** * Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened. * @member {OpenSeadragon.Viewport} viewport @@ -197,14 +193,14 @@ $.Viewer = function( options ) { // how much we should be continuously zooming by "zoomFactor": null, "lastZoomTime": null, - // did we decide this viewer has a sequence of tile sources - "sequenced": false, - "sequence": 0, "fullPage": false, "onfullscreenchange": null }; + this._sequenceIndex = 0; + this._firstOpen = true; this._updateRequestId = null; + this._loadQueue = []; this.currentOverlays = []; //Inherit some behaviors and properties @@ -218,45 +214,13 @@ $.Viewer = function( options ) { $.ControlDock.call( this, options ); //Deal with tile sources - var initialTileSource; - if ( this.xmlPath ){ //Deprecated option. Now it is preferred to use the tileSources option this.tileSources = [ this.xmlPath ]; } - if ( this.tileSources ){ - // tileSources is a complex option... - // - // It can be a string, object, or an array of any of strings and objects. - // At this point we only care about if it is an Array or not. - // - if( $.isArray( this.tileSources ) ){ - - //must be a sequence of tileSource since the first item - //is a legacy tile source - if( this.tileSources.length > 1 ){ - THIS[ this.hash ].sequenced = true; - } - - //Keeps the initial page within bounds - if ( this.initialPage > this.tileSources.length - 1 ){ - this.initialPage = this.tileSources.length - 1; - } - - initialTileSource = this.tileSources[ this.initialPage ]; - - //Update the sequence (aka currrent page) property - THIS[ this.hash ].sequence = this.initialPage; - } else { - initialTileSource = this.tileSources; - } - } - this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); - this.drawersContainer = $.makeNeutralElement( "div" ); - this.overlaysContainer = $.makeNeutralElement( "div" ); this.canvas.className = "openseadragon-canvas"; (function( style ){ @@ -284,8 +248,6 @@ $.Viewer = function( options ) { this.container.insertBefore( this.canvas, this.container.firstChild ); this.element.appendChild( this.container ); - this.canvas.appendChild( this.drawersContainer ); - this.canvas.appendChild( this.overlaysContainer ); //Used for toggling between fullscreen and default container size //TODO: these can be closure private and shared across Viewer @@ -334,16 +296,137 @@ $.Viewer = function( options ) { } this.bindStandardControls(); - this.bindSequenceControls(); - if ( initialTileSource ) { - this.open( initialTileSource ); + THIS[ this.hash ].prevContainerSize = _getSafeElemSize( this.container ); - if ( this.tileSources.length > 1 ) { - this._updateSequenceButtons( this.initialPage ); + // Create the world + this.world = new $.World({ + viewer: this + }); + + this.world.addHandler('add-item', function(event) { + // For backwards compatibility, we maintain the source property + _this.source = _this.world.getItemAt(0).source; + + THIS[ _this.hash ].forceRedraw = true; + + if (!_this._updateRequestId) { + _this._updateRequestId = scheduleUpdate( _this, updateMulti ); + } + }); + + this.world.addHandler('remove-item', function(event) { + // For backwards compatibility, we maintain the source property + if (_this.world.getItemCount()) { + _this.source = _this.world.getItemAt(0).source; + } else { + _this.source = null; + } + + THIS[ _this.hash ].forceRedraw = true; + }); + + this.world.addHandler('metrics-change', function(event) { + if (_this.viewport) { + _this.viewport.setHomeBounds(_this.world.getHomeBounds(), _this.world.getContentFactor()); + } + }); + + this.world.addHandler('item-index-change', function(event) { + // For backwards compatibility, we maintain the source property + _this.source = _this.world.getItemAt(0).source; + }); + + // Create the viewport + this.viewport = new $.Viewport({ + containerSize: THIS[ this.hash ].prevContainerSize, + springStiffness: this.springStiffness, + animationTime: this.animationTime, + minZoomImageRatio: this.minZoomImageRatio, + maxZoomPixelRatio: this.maxZoomPixelRatio, + visibilityRatio: this.visibilityRatio, + wrapHorizontal: this.wrapHorizontal, + wrapVertical: this.wrapVertical, + defaultZoomLevel: this.defaultZoomLevel, + minZoomLevel: this.minZoomLevel, + maxZoomLevel: this.maxZoomLevel, + viewer: this, + degrees: this.degrees, + navigatorRotate: this.navigatorRotate, + homeFillsViewer: this.homeFillsViewer, + margins: this.viewportMargins + }); + + this.viewport.setHomeBounds(this.world.getHomeBounds(), this.world.getContentFactor()); + + // Create the image loader + this.imageLoader = new $.ImageLoader({ + jobLimit: this.imageLoaderLimit + }); + + // Create the tile cache + this.tileCache = new $.TileCache({ + maxImageCacheCount: this.maxImageCacheCount + }); + + // Create the drawer + this.drawer = new $.Drawer({ + viewer: this, + viewport: this.viewport, + element: this.canvas, + opacity: this.opacity, + debugGridColor: this.debugGridColor + }); + + // Overlay container + this.overlaysContainer = $.makeNeutralElement( "div" ); + this.canvas.appendChild( this.overlaysContainer ); + + // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons + if (!this.drawer.canRotate()) { + // Disable/remove the rotate left/right buttons since they aren't supported + if (this.rotateLeft) { + i = this.buttons.buttons.indexOf(this.rotateLeft); + this.buttons.buttons.splice(i, 1); + this.buttons.element.removeChild(this.rotateLeft.element); + } + if (this.rotateRight) { + i = this.buttons.buttons.indexOf(this.rotateRight); + this.buttons.buttons.splice(i, 1); + this.buttons.element.removeChild(this.rotateRight.element); } } + //Instantiate a navigator if configured + if ( this.showNavigator){ + this.navigator = new $.Navigator({ + id: this.navigatorId, + position: this.navigatorPosition, + sizeRatio: this.navigatorSizeRatio, + maintainSizeRatio: this.navigatorMaintainSizeRatio, + top: this.navigatorTop, + left: this.navigatorLeft, + width: this.navigatorWidth, + height: this.navigatorHeight, + autoResize: this.navigatorAutoResize, + tileHost: this.tileHost, + prefixUrl: this.prefixUrl, + viewer: this, + navigatorRotate: this.navigatorRotate + }); + } + + // Sequence mode + if (this.sequenceMode) { + this.bindSequenceControls(); + } + + // Open initial tilesources + if (this.tileSources) { + this.open( this.tileSources ); + } + + // Add custom controls for ( i = 0; i < this.customControls.length; i++ ) { this.addControl( this.customControls[ i ].id, @@ -351,10 +434,10 @@ $.Viewer = function( options ) { ); } + // Initial fade out $.requestAnimationFrame( function(){ beginControlsAutoHide( _this ); - } ); // initial fade out - + } ); }; $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** @lends OpenSeadragon.Viewer.prototype */{ @@ -365,77 +448,197 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @return {Boolean} */ isOpen: function () { - return !!this.source; + return !!this.world.getItemCount(); }, - /** - * A deprecated function, renamed to 'open' to match event name and - * match current 'close' method. - * @function - * @param {String} dzi xml string or the url to a DZI xml document. - * @return {OpenSeadragon.Viewer} Chainable. - * - * @deprecated - use {@link OpenSeadragon.Viewer#open} instead. - */ + // deprecated openDzi: function ( dzi ) { + $.console.error( "[Viewer.openDzi] this function is deprecated; use Viewer.open() instead." ); return this.open( dzi ); }, - /** - * A deprecated function, renamed to 'open' to match event name and - * match current 'close' method. - * @function - * @param {String|Object|Function} See OpenSeadragon.Viewer.prototype.open - * @return {OpenSeadragon.Viewer} Chainable. - * - * @deprecated - use {@link OpenSeadragon.Viewer#open} instead. - */ + // deprecated openTileSource: function ( tileSource ) { + $.console.error( "[Viewer.openTileSource] this function is deprecated; use Viewer.open() instead." ); return this.open( tileSource ); }, /** - * Open a TileSource object into the viewer. - * - * tileSources is a complex option... - * - * It can be a string, object, function, or an array of any of these: - * - * - A String implies a url used to determine the tileSource implementation - * based on the file extension of url. JSONP is implied by *.js, - * otherwise the url is retrieved as text and the resulting text is - * introspected to determine if its json, xml, or text and parsed. - * - An Object implies an inline configuration which has a single - * property sufficient for being able to determine tileSource - * implementation. If the object has a property which is a function - * named 'getTileUrl', it is treated as a custom TileSource. + * Open tiled images into the viewer, closing any others. * @function - * @param {String|Object|Function} + * @param {Array|String|Object|Function} tileSources - This can be a TiledImage + * specifier, a TileSource specifier, or an array of either. A TiledImage specifier + * is the same as the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}, + * except for the index property; images are added in sequence. + * A TileSource specifier is anything you could pass as the tileSource property + * of the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}. * @return {OpenSeadragon.Viewer} Chainable. * @fires OpenSeadragon.Viewer.event:open * @fires OpenSeadragon.Viewer.event:open-failed */ - open: function ( tileSource ) { + open: function (tileSources) { var _this = this; - _this._hideMessage(); + this.close(); - getTileSourceImplementation( _this, tileSource, function( tileSource ) { - openTileSource( _this, tileSource ); - }, function( event ) { - /** - * Raised when an error occurs loading a TileSource. - * - * @event open-failed - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {String} message - * @property {String} source - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.raiseEvent( 'open-failed', event ); - }); + if (!tileSources) { + return; + } + + if (this.sequenceMode && $.isArray(tileSources)) { + if (this.referenceStrip) { + this.referenceStrip.destroy(); + this.referenceStrip = null; + } + + this.tileSources = tileSources; + this._sequenceIndex = Math.max(0, Math.min(this.tileSources.length - 1, this.initialPage)); + if (this.tileSources.length) { + this.open(this.tileSources[this._sequenceIndex]); + + if ( this.showReferenceStrip ){ + this.referenceStrip = new $.ReferenceStrip({ + id: this.referenceStripElement, + position: this.referenceStripPosition, + sizeRatio: this.referenceStripSizeRatio, + scroll: this.referenceStripScroll, + height: this.referenceStripHeight, + width: this.referenceStripWidth, + tileSources: this.tileSources, + tileHost: this.tileHost, + prefixUrl: this.prefixUrl, + viewer: this + }); + } + } + + this._updateSequenceButtons( this._sequenceIndex ); + return; + } + + if (!$.isArray(tileSources)) { + tileSources = [tileSources]; + } + + if (!tileSources.length) { + return; + } + + var expected = tileSources.length; + var successes = 0; + var failures = 0; + var failEvent; + + var checkCompletion = function() { + if (successes + failures === expected) { + if (successes) { + if (_this._firstOpen || !_this.preserveViewport) { + _this.viewport.goHome( true ); + _this.viewport.update(); + } + + _this._firstOpen = false; + + var source = tileSources[0]; + if (source.tileSource) { + source = source.tileSource; + } + + // Global overlays + if( _this.overlays && !_this.preserveOverlays ){ + for ( var i = 0; i < _this.overlays.length; i++ ) { + _this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] ); + } + } + + _this._drawOverlays(); + + /** + * Raised when the viewer has opened and loaded one or more TileSources. + * + * @event open + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TileSource} source - The tile source that was opened. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + // TODO: what if there are multiple sources? + _this.raiseEvent( 'open', { source: source } ); + } else { + /** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {String} message - Information about what failed. + * @property {String} source - The tile source that failed. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'open-failed', failEvent ); + } + } + }; + + var doOne = function(options) { + if (!$.isPlainObject(options) || !options.tileSource) { + options = { + tileSource: options + }; + } + + if (options.index !== undefined) { + $.console.error('[Viewer.open] setting indexes here is not supported; use addTiledImage instead'); + delete options.index; + } + + if (options.collectionImmediately === undefined) { + options.collectionImmediately = true; + } + + var originalSuccess = options.success; + options.success = function(event) { + successes++; + + if (originalSuccess) { + originalSuccess(event); + } + + checkCompletion(); + }; + + var originalError = options.error; + options.error = function(event) { + failures++; + + if (!failEvent) { + failEvent = event; + } + + if (originalError) { + originalError(event); + } + + checkCompletion(); + }; + + _this.addTiledImage(options); + + // TODO: now that options has other things besides tileSource, the overlays + // should probably be at the options level, not the tileSource level. + if (options.tileSource.overlays) { + for (var i = 0; i < options.tileSource.overlays.length; i++) { + _this.addOverlay(options.tileSource.overlays[i]); + } + } + }; + + // TileSources + for (var i = 0; i < tileSources.length; i++) { + doOne(tileSources[i]); + } return this; }, @@ -447,16 +650,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @fires OpenSeadragon.Viewer.event:close */ close: function ( ) { - if ( !THIS[ this.hash ] ) { //this viewer has already been destroyed: returning immediately return this; } - - if ( this._updateRequestId !== null ) { - $.cancelAnimationFrame( this._updateRequestId ); - this._updateRequestId = null; - } if ( this.navigator ) { this.navigator.close(); @@ -468,21 +665,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.overlaysContainer.innerHTML = ""; } - this.drawersContainer.innerHTML = ""; - - if ( this.drawer ) { - this.drawer.destroy(); - } - - this.source = null; - this.drawer = null; - this.drawers = []; - - this.viewport = this.preserveViewport ? this.viewport : null; - - - VIEWERS[ this.hash ] = null; - delete VIEWERS[ this.hash ]; + THIS[ this.hash ].animating = false; + this.world.removeAll(); /** * Raised when the viewer is closed (see {@link OpenSeadragon.Viewer#close}). @@ -501,7 +685,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** * Function to destroy the viewer and clean up everything created by OpenSeadragon. - * + * * Example: * var viewer = OpenSeadragon({ * [...] @@ -514,12 +698,31 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @function */ destroy: function( ) { + if ( !THIS[ this.hash ] ) { + //this viewer has already been destroyed: returning immediately + return; + } + this.close(); //TODO: implement this... //this.unbindSequenceControls() - //this.unbindStandardControls() - + //this.unbindStandardControls() + + if (this.referenceStrip) { + this.referenceStrip.destroy(); + this.referenceStrip = null; + } + + if ( this._updateRequestId !== null ) { + $.cancelAnimationFrame( this._updateRequestId ); + this._updateRequestId = null; + } + + if ( this.drawer ) { + this.drawer.destroy(); + } + this.removeAllHandlers(); // Go through top element (passed to us) and remove all children @@ -550,7 +753,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.element = null; }, - /** * @function * @return {Boolean} @@ -961,250 +1163,220 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, }, /** - * Add a layer. + * Add a tiled image to the viewer. * options.tileSource can be anything that {@link OpenSeadragon.Viewer#open} - * supports except arrays of images as layers cannot be sequences. + * supports except arrays of images. + * Note that you can specify options.width or options.height, but not both. + * The other dimension will be calculated according to the item's aspect ratio. + * If collectionMode is on (see {@link OpenSeadragon.Options}), the new image is + * automatically arranged with the others. * @function * @param {Object} options - * @param {String|Object|Function} options.tileSource The TileSource of the layer. - * @param {Number} [options.opacity=1] The opacity of the layer. - * @param {Number} [options.level] The level of the layer. Added on top of - * all other layers if not specified. - * @returns {OpenSeadragon.Viewer} Chainable. - * @fires OpenSeadragon.Viewer.event:add-layer - * @fires OpenSeadragon.Viewer.event:add-layer-failed + * @param {String|Object|Function} options.tileSource - The TileSource specifier. + * A String implies a url used to determine the tileSource implementation + * based on the file extension of url. JSONP is implied by *.js, + * otherwise the url is retrieved as text and the resulting text is + * introspected to determine if its json, xml, or text and parsed. + * An Object implies an inline configuration which has a single + * property sufficient for being able to determine tileSource + * implementation. If the object has a property which is a function + * 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 {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. + * @param {Number} [options.height] The height for the image in viewport coordinates. + * @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. + * @param {Function} [options.error] A function that gets called if the image is + * unable to be added. It's passed the error event object, which contains "message" + * 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. + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.Viewer.event:add-item-failed */ - addLayer: function( options ) { - var _this = this, - tileSource = options.tileSource; + addTiledImage: function( options ) { + $.console.assert(options, "[Viewer.addTiledImage] options is required"); + $.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required"); - if ( !this.isOpen() ) { - throw new Error( "An image must be loaded before adding layers." ); - } - if ( !tileSource ) { - throw new Error( "No tile source provided as new layer." ); - } - if ( this.collectionMode ) { - throw new Error( "Layers not supported in collection mode." ); - } + var _this = this; + + this._hideMessage(); + + var myQueueItem = { + options: options + }; + + function raiseAddItemFailed( event ) { + for (var i = 0; i < _this._loadQueue; i++) { + if (_this._loadQueue[i] === myQueueItem) { + _this._loadQueue.splice(i, 1); + break; + } + } - function raiseAddLayerFailed( event ) { /** - * Raised when an error occurs while adding a layer. - * @event add-layer-failed + * Raised when an error occurs while adding a item. + * @event add-item-failed * @memberOf OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. * @property {String} message * @property {String} source - * @property {Object} options The options passed to the addLayer method. + * @property {Object} options The options passed to the addTiledImage method. * @property {?Object} userData - Arbitrary subscriber-defined object. */ - _this.raiseEvent( 'add-layer-failed', event ); + _this.raiseEvent( 'add-item-failed', event ); + + if (options.error) { + options.error(event); + } } - getTileSourceImplementation( this, tileSource, function( tileSource ) { + this._loadQueue.push(myQueueItem); + + getTileSourceImplementation( this, options.tileSource, function( tileSource ) { if ( tileSource instanceof Array ) { - raiseAddLayerFailed({ - message: "Sequences can not be added as layers.", + raiseAddItemFailed({ + message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", source: tileSource, options: options }); return; } - for ( var i = 0; i < _this.drawers.length; i++ ) { - var otherAspectRatio = _this.drawers[ i ].source.aspectRatio; - var diff = otherAspectRatio - tileSource.aspectRatio; - if ( Math.abs( diff ) > _this.layersAspectRatioEpsilon ) { - raiseAddLayerFailed({ - message: "Aspect ratio mismatch with layer " + i + ".", - source: tileSource, - options: options + myQueueItem.tileSource = tileSource; + + // add everybody at the front of the queue that's ready to go + var queueItem, tiledImage, optionsClone; + while (_this._loadQueue.length) { + queueItem = _this._loadQueue[0]; + if (!queueItem.tileSource) { + break; + } + + _this._loadQueue.splice(0, 1); + + tiledImage = new $.TiledImage({ + viewer: _this, + source: queueItem.tileSource, + viewport: _this.viewport, + drawer: _this.drawer, + tileCache: _this.tileCache, + imageLoader: _this.imageLoader, + x: queueItem.options.x, + y: queueItem.options.y, + width: queueItem.options.width, + height: queueItem.options.height, + springStiffness: _this.springStiffness, + animationTime: _this.animationTime, + minZoomImageRatio: _this.minZoomImageRatio, + wrapHorizontal: _this.wrapHorizontal, + wrapVertical: _this.wrapVertical, + immediateRender: _this.immediateRender, + blendTime: _this.blendTime, + alwaysBlend: _this.alwaysBlend, + minPixelRatio: _this.minPixelRatio, + debugMode: _this.debugMode + }); + + _this.world.addItem( tiledImage, { + index: queueItem.options.index + }); + + if (_this.collectionMode) { + _this.world.arrange({ + immediately: queueItem.options.collectionImmediately, + rows: _this.collectionRows, + layout: _this.collectionLayout, + tileSize: _this.collectionTileSize, + tileMargin: _this.collectionTileMargin + }); + } + + if (_this.world.getItemCount() === 1 && !_this.preserveViewport) { + _this.viewport.goHome(true); + } + + if (_this.navigator) { + optionsClone = $.extend({}, queueItem.options, { + originalTiledImage: tiledImage, + tileSource: queueItem.tileSource + }); + + _this.navigator.addTiledImage(optionsClone); + } + + if (queueItem.options.success) { + queueItem.options.success({ + item: tiledImage }); - return; } } - - var drawer = new $.Drawer({ - viewer: _this, - source: tileSource, - viewport: _this.viewport, - element: _this.drawersContainer, - opacity: options.opacity !== undefined ? - options.opacity : _this.opacity, - maxImageCacheCount: _this.maxImageCacheCount, - imageLoaderLimit: _this.imageLoaderLimit, - minZoomImageRatio: _this.minZoomImageRatio, - wrapHorizontal: _this.wrapHorizontal, - wrapVertical: _this.wrapVertical, - immediateRender: _this.immediateRender, - blendTime: _this.blendTime, - alwaysBlend: _this.alwaysBlend, - minPixelRatio: _this.minPixelRatio, - timeout: _this.timeout, - debugMode: _this.debugMode, - debugGridColor: _this.debugGridColor - }); - _this.drawers.push( drawer ); - if ( options.level !== undefined ) { - _this.setLayerLevel( drawer, options.level ); - } - THIS[ _this.hash ].forceRedraw = true; - /** - * Raised when a layer is successfully added. - * @event add-layer - * @memberOf OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {Object} options The options passed to the addLayer method. - * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.raiseEvent( 'add-layer', { - options: options, - drawer: drawer - }); }, function( event ) { event.options = options; - raiseAddLayerFailed(event); + raiseAddItemFailed(event); } ); + }, + // deprecated + addLayer: function( options ) { + var _this = this; + + $.console.error( "[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead." ); + + var optionsClone = $.extend({}, options, { + success: function(event) { + _this.raiseEvent("add-layer", { + options: options, + drawer: event.item + }); + }, + error: function(event) { + _this.raiseEvent("add-layer-failed", event); + } + }); + + this.addTiledImage(optionsClone); return this; }, - /** - * Get the layer at the specified level. - * @param {Number} level The layer to retrieve level. - * @returns {OpenSeadragon.Drawer} The layer at the specified level. - */ + // deprecated getLayerAtLevel: function( level ) { - if ( level >= this.drawers.length ) { - throw new Error( "Level bigger than number of layers." ); - } - return this.drawers[ level ]; + $.console.error( "[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead." ); + return this.world.getItemAt(level); }, - /** - * Get the level of the layer associated with the given drawer or -1 if not - * present. - * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer. - * @returns {Number} The level of the layer or -1 if not present. - */ + // deprecated getLevelOfLayer: function( drawer ) { - return $.indexOf( this.drawers, drawer ); + $.console.error( "[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead." ); + return this.world.getIndexOfItem(drawer); }, - /** - * Get the number of layers used. - * @returns {Number} The number of layers used. - */ + // deprecated getLayersCount: function() { - return this.drawers.length; + $.console.error( "[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead." ); + return this.world.getItemCount(); }, - /** - * Change the level of a layer so that it appears over or under others. - * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the changing - * level layer. - * @param {Number} level The new level - * @returns {OpenSeadragon.Viewer} Chainable. - * @fires OpenSeadragon.Viewer.event:layer-level-changed - */ + // deprecated setLayerLevel: function( drawer, level ) { - var oldLevel = this.getLevelOfLayer( drawer ); - - if ( level >= this.drawers.length ) { - throw new Error( "Level bigger than number of layers." ); - } - if ( level === oldLevel || oldLevel === -1 ) { - return this; - } - if ( level === 0 || oldLevel === 0 ) { - if ( THIS[ this.hash ].sequenced ) { - throw new Error( "Cannot reassign base level when in sequence mode." ); - } - // We need to re-assign the base drawer and the source - this.drawer = level === 0 ? drawer : this.getLayerAtLevel( level ); - this.source = this.drawer.source; - } - this.drawers.splice( oldLevel, 1 ); - this.drawers.splice( level, 0, drawer ); - this.drawersContainer.removeChild( drawer.canvas ); - if ( level === 0 ) { - var nextLevelCanvas = this.drawers[ 1 ].canvas; - nextLevelCanvas.parentNode.insertBefore( drawer.canvas, - nextLevelCanvas ); - } else { - // Insert right after layer at level - 1 - var prevLevelCanvas = this.drawers[level - 1].canvas; - prevLevelCanvas.parentNode.insertBefore( drawer.canvas, - prevLevelCanvas.nextSibling ); - } - - /** - * Raised when the order of the layers has been changed. - * @event layer-level-changed - * @memberOf OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Drawer} drawer - The drawer which level has - * been changed - * @property {Number} previousLevel - The previous level of the drawer - * @property {Number} newLevel - The new level of the drawer - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent( 'layer-level-changed', { - drawer: drawer, - previousLevel: oldLevel, - newLevel: level - } ); - - return this; + $.console.error( "[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead." ); + return this.world.setItemIndex(drawer, level); }, - /** - * Remove a layer. If there is only one layer, close the viewer. - * @function - * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer - * to remove - * @returns {OpenSeadragon.Viewer} Chainable. - * @fires OpenSeadragon.Viewer.event:remove-layer - */ + // deprecated removeLayer: function( drawer ) { - var index = this.drawers.indexOf( drawer ); - if ( index === -1 ) { - return this; - } - if ( index === 0 ) { - if ( THIS[ this.hash ].sequenced ) { - throw new Error( "Cannot remove base layer when in sequence mode." ); - } - if ( this.drawers.length === 1 ) { - this.close(); - return this; - } - this.drawer = this.drawers[ 1 ]; - } - - this.drawers.splice( index, 1 ); - this.drawersContainer.removeChild( drawer.canvas ); - /** - * Raised when a layer is removed. - * @event remove-layer - * @memberOf OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent( 'remove-layer', { drawer: drawer } ); - return this; + $.console.error( "[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead." ); + return this.world.removeItem(drawer); }, /** - * Force the viewer to redraw its drawers. + * Force the viewer to redraw its contents. * @returns {OpenSeadragon.Viewer} Chainable. */ forceRedraw: function() { @@ -1228,7 +1400,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, navImages = this.navImages, useGroup = true ; - if( this.showSequenceControl && THIS[ this.hash ].sequenced ){ + if( this.showSequenceControl ){ if( this.previousButton || this.nextButton ){ //if we are binding to custom buttons then layout and @@ -1268,6 +1440,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.previousButton.disable(); } + if (!this.tileSources || !this.tileSources.length) { + this.nextButton.disable(); + } + if( useGroup ){ this.paging = new $.ButtonGroup({ buttons: [ @@ -1458,14 +1634,14 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } return this; }, - + /** * Gets the active page of a sequence * @function * @return {Number} */ currentPage: function() { - return THIS[ this.hash ].sequence; + return this._sequenceIndex; }, /** @@ -1474,7 +1650,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @fires OpenSeadragon.Viewer.event:page */ goToPage: function( page ){ - if( page >= 0 && page < this.tileSources.length ){ + if( this.tileSources && page >= 0 && page < this.tileSources.length ){ /** * Raised when the page is changed on a viewer configured with multiple image sources (see {@link OpenSeadragon.Viewer#goToPage}). * @@ -1487,7 +1663,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, */ this.raiseEvent( 'page', { page: page } ); - THIS[ this.hash ].sequence = page; + this._sequenceIndex = page; this._updateSequenceButtons( page ); @@ -1539,8 +1715,11 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, // they're trying to add a duplicate overlay return this; } - this.currentOverlays.push( getOverlayObject( this, options ) ); - THIS[ this.hash ].forceRedraw = true; + + var overlay = getOverlayObject( this, options); + this.currentOverlays.push(overlay); + overlay.drawHTML( this.overlaysContainer, this.viewport ); + /** * Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}). * @@ -1677,7 +1856,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, _updateSequenceButtons: function( page ) { if ( this.nextButton ) { - if( ( this.tileSources.length - 1 ) === page ) { + if(!this.tileSources || this.tileSources.length - 1 === page) { //Disable next button if ( !this.navPrevNextWrap ) { this.nextButton.disable(); @@ -1697,7 +1876,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } } }, - + /** * Display a message in the viewport * @function OpenSeadragon.Viewer.prototype._showMessage @@ -1747,15 +1926,22 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, default: return this.gestureSettingsUnknown; } - } + }, + // private + _drawOverlays: function() { + var i, + length = this.currentOverlays.length; + for ( i = 0; i < length; i++ ) { + this.currentOverlays[ i ].drawHTML( this.overlaysContainer, this.viewport ); + } + } }); /** * _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y, - * which was causing some calling operations in updateOnce and openTileSource to - * return NaN. + * which was causing some calling operations to return NaN. * @returns {Point} * @private */ @@ -1789,14 +1975,22 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, setTimeout( function() { if ( $.type( tileSource ) == 'string' ) { //If its still a string it means it must be a url at this point - tileSource = new $.TileSource( tileSource, function( event ) { - successCallback( event.tileSource ); + tileSource = new $.TileSource({ + url: tileSource, + ajaxWithCredentials: viewer.ajaxWithCredentials, + success: function( event ) { + successCallback( event.tileSource ); + } }); tileSource.addHandler( 'open-failed', function( event ) { failCallback( event ); } ); } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + if (tileSource.ajaxWithCredentials === undefined) { + tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials; + } + if ( $.isFunction( tileSource.getTileUrl ) ) { //Custom tile source var customTileSource = new $.TileSource( tileSource ); @@ -1823,195 +2017,6 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, }, 1 ); } -/** - * @function - * @private - */ -function openTileSource( viewer, source ) { - var i, - _this = viewer; - - if ( _this.source ) { - _this.close( ); - } - - THIS[ _this.hash ].prevContainerSize = _getSafeElemSize( _this.container ); - - - if( _this.collectionMode ){ - _this.source = new $.TileSourceCollection({ - rows: _this.collectionRows, - layout: _this.collectionLayout, - tileSize: _this.collectionTileSize, - tileSources: _this.tileSources, - tileMargin: _this.collectionTileMargin - }); - _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({ - collectionMode: true, - collectionTileSource: _this.source, - containerSize: THIS[ _this.hash ].prevContainerSize, - contentSize: _this.source.dimensions, - springStiffness: _this.springStiffness, - animationTime: _this.animationTime, - showNavigator: false, - minZoomImageRatio: 1, - maxZoomPixelRatio: 1, - viewer: _this, - degrees: _this.degrees //, - //TODO: figure out how to support these in a way that makes sense - //minZoomLevel: this.minZoomLevel, - //maxZoomLevel: this.maxZoomLevel, - //homeFillsViewer: this.homeFillsViewer - }); - } else { - if( source ){ - _this.source = source; - } - _this.viewport = _this.viewport ? _this.viewport : new $.Viewport({ - containerSize: THIS[ _this.hash ].prevContainerSize, - contentSize: _this.source.dimensions, - springStiffness: _this.springStiffness, - animationTime: _this.animationTime, - minZoomImageRatio: _this.minZoomImageRatio, - maxZoomPixelRatio: _this.maxZoomPixelRatio, - visibilityRatio: _this.visibilityRatio, - wrapHorizontal: _this.wrapHorizontal, - wrapVertical: _this.wrapVertical, - defaultZoomLevel: _this.defaultZoomLevel, - minZoomLevel: _this.minZoomLevel, - maxZoomLevel: _this.maxZoomLevel, - viewer: _this, - degrees: _this.degrees, - navigatorRotate: _this.navigatorRotate, - homeFillsViewer: _this.homeFillsViewer - }); - } - - if( _this.preserveViewport ){ - _this.viewport.resetContentSize( _this.source.dimensions ); - } - - if( _this.preserveOverlays ){ - _this.overlays = _this.currentOverlays; - } - - _this.source.overlays = _this.source.overlays || []; - - _this.drawer = new $.Drawer({ - viewer: _this, - source: _this.source, - viewport: _this.viewport, - element: _this.drawersContainer, - opacity: _this.opacity, - maxImageCacheCount: _this.maxImageCacheCount, - imageLoaderLimit: _this.imageLoaderLimit, - minZoomImageRatio: _this.minZoomImageRatio, - wrapHorizontal: _this.wrapHorizontal, - wrapVertical: _this.wrapVertical, - immediateRender: _this.immediateRender, - blendTime: _this.blendTime, - alwaysBlend: _this.alwaysBlend, - minPixelRatio: _this.collectionMode ? 0 : _this.minPixelRatio, - timeout: _this.timeout, - debugMode: _this.debugMode, - debugGridColor: _this.debugGridColor, - crossOriginPolicy: _this.crossOriginPolicy - }); - _this.drawers = [_this.drawer]; - - // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons - if (!_this.drawer.canRotate()) { - // Disable/remove the rotate left/right buttons since they aren't supported - if (_this.rotateLeft) { - i = _this.buttons.buttons.indexOf(_this.rotateLeft); - _this.buttons.buttons.splice(i, 1); - _this.buttons.element.removeChild(_this.rotateLeft.element); - } - if (_this.rotateRight) { - i = _this.buttons.buttons.indexOf(_this.rotateRight); - _this.buttons.buttons.splice(i, 1); - _this.buttons.element.removeChild(_this.rotateRight.element); - } - } - - //Instantiate a navigator if configured - if ( _this.showNavigator && !_this.collectionMode ){ - // Note: By passing the fully parsed source, the navigator doesn't - // have to load it again. - if ( _this.navigator ) { - _this.navigator.open( source ); - } else { - _this.navigator = new $.Navigator({ - id: _this.navigatorId, - position: _this.navigatorPosition, - sizeRatio: _this.navigatorSizeRatio, - maintainSizeRatio: _this.navigatorMaintainSizeRatio, - top: _this.navigatorTop, - left: _this.navigatorLeft, - width: _this.navigatorWidth, - height: _this.navigatorHeight, - autoResize: _this.navigatorAutoResize, - tileSources: source, - tileHost: _this.tileHost, - prefixUrl: _this.prefixUrl, - viewer: _this, - navigatorRotate: _this.navigatorRotate - }); - } - } - - //Instantiate a referencestrip if configured - if ( _this.showReferenceStrip && !_this.referenceStrip ){ - _this.referenceStrip = new $.ReferenceStrip({ - id: _this.referenceStripElement, - position: _this.referenceStripPosition, - sizeRatio: _this.referenceStripSizeRatio, - scroll: _this.referenceStripScroll, - height: _this.referenceStripHeight, - width: _this.referenceStripWidth, - tileSources: _this.tileSources, - tileHost: _this.tileHost, - prefixUrl: _this.prefixUrl, - viewer: _this - }); - } - - //this.profiler = new $.Profiler(); - - THIS[ _this.hash ].animating = false; - THIS[ _this.hash ].forceRedraw = true; - _this._updateRequestId = scheduleUpdate( _this, updateMulti ); - - VIEWERS[ _this.hash ] = _this; - - loadOverlays( _this ); - - /** - * Raised when the viewer has opened and loaded one or more TileSources. - * - * @event open - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TileSource} source - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.raiseEvent( 'open', { source: source } ); - - return _this; -} - -function loadOverlays( _this ) { - _this.currentOverlays = []; - for ( var i = 0; i < _this.overlays.length; i++ ) { - _this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] ); - } - for ( var j = 0; j < _this.source.overlays.length; j++ ) { - _this.currentOverlays[ i + j ] = - getOverlayObject( _this, _this.source.overlays[ j ] ); - } -} - function getOverlayObject( viewer, overlay ) { if ( overlay instanceof $.Overlay ) { return overlay; @@ -2096,14 +2101,6 @@ function getOverlayIndex( overlays, element ) { return -1; } -function drawOverlays( viewport, overlays, container ) { - var i, - length = overlays.length; - for ( i = 0; i < length; i++ ) { - overlays[ i ].drawHTML( container, viewport ); - } -} - /////////////////////////////////////////////////////////////////////////////// // Schedulers provide the general engine for animation /////////////////////////////////////////////////////////////////////////////// @@ -2706,8 +2703,11 @@ function onCanvasScroll( event ) { shift: event.shift, originalEvent: event.originalEvent }); - //cancels event - return false; + + if (gestureSettings && gestureSettings.scrollToZoom) { + //cancels event + return false; + } } function onContainerEnter( event ) { @@ -2780,32 +2780,22 @@ function onContainerExit( event ) { /////////////////////////////////////////////////////////////////////////////// function updateMulti( viewer ) { - if ( !viewer.source ) { - viewer._updateRequestId = null; - return; - } - updateOnce( viewer ); - // Request the next frame, unless we've been closed during the updateOnce() - if ( viewer.source ) { + // Request the next frame, unless we've been closed + if ( viewer.isOpen() ) { viewer._updateRequestId = scheduleUpdate( viewer, updateMulti ); + } else { + viewer._updateRequestId = false; } } function updateOnce( viewer ) { - var containerSize, - animated; - - if ( !viewer.source ) { - return; - } - //viewer.profiler.beginUpdate(); if ( viewer.autoResize ) { - containerSize = _getSafeElemSize( viewer.container ); + var containerSize = _getSafeElemSize( viewer.container ); if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { // maintain image position var oldBounds = viewer.viewport.getBounds(); @@ -2816,7 +2806,22 @@ function updateOnce( viewer ) { } } - animated = viewer.viewport.update(); + var viewportChange = viewer.viewport.update(); + var animated = viewer.world.update() || viewportChange; + + if (viewportChange) { + /** + * Raised when any spring animation update occurs (zoom, pan, etc.), + * before the viewer has drawn the new location. + * + * @event viewport-change + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent('viewport-change'); + } if( viewer.referenceStrip ){ animated = viewer.referenceStrip.update( viewer.viewport ) || animated; @@ -2836,29 +2841,28 @@ function updateOnce( viewer ) { abortControlsAutoHide( viewer ); } - if ( animated ) { - updateDrawers( viewer ); - drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); - if( viewer.navigator ){ - viewer.navigator.update( viewer.viewport ); - } - /** - * Raised when any spring animation update occurs (zoom, pan, etc.). - * - * @event animation - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - viewer.raiseEvent( "animation" ); - } else if ( THIS[ viewer.hash ].forceRedraw || drawersNeedUpdate( viewer ) ) { - updateDrawers( viewer ); - drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); + if ( animated || THIS[ viewer.hash ].forceRedraw || viewer.world.needsDraw() ) { + drawWorld( viewer ); + viewer._drawOverlays(); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); } + THIS[ viewer.hash ].forceRedraw = false; + + if (animated) { + /** + * Raised when any spring animation update occurs (zoom, pan, etc.), + * after the viewer has drawn the new location. + * + * @event animation + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent( "animation" ); + } } if ( THIS[ viewer.hash ].animating && !animated ) { @@ -2894,10 +2898,10 @@ function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter viewport.resize( containerSize, true ); // We try to remove blanks as much as possible - var imageHeight = 1 / viewer.source.aspectRatio; - var newWidth = oldBounds.width <= 1 ? oldBounds.width : 1; - var newHeight = oldBounds.height <= imageHeight ? - oldBounds.height : imageHeight; + 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 ), @@ -2908,19 +2912,20 @@ function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter viewport.fitBounds( newBounds, true ); } -function updateDrawers( viewer ) { - for (var i = 0; i < viewer.drawers.length; i++ ) { - viewer.drawers[i].update(); - } -} +function drawWorld( viewer ) { + viewer.drawer.clear(); + viewer.world.draw(); -function drawersNeedUpdate( viewer ) { - for (var i = 0; i < viewer.drawers.length; i++ ) { - if (viewer.drawers[i].needsUpdate()) { - return true; - } - } - return false; + /** + * - Needs documentation - + * + * @event update-viewport + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + viewer.raiseEvent( 'update-viewport', {} ); } /////////////////////////////////////////////////////////////////////////////// @@ -3062,7 +3067,7 @@ function onRotateRight() { function onPrevious(){ - var previous = THIS[ this.hash ].sequence - 1; + var previous = this._sequenceIndex - 1; if(this.navPrevNextWrap && previous < 0){ previous += this.tileSources.length; } @@ -3071,7 +3076,7 @@ function onPrevious(){ function onNext(){ - var next = THIS[ this.hash ].sequence + 1; + var next = this._sequenceIndex + 1; if(this.navPrevNextWrap && next >= this.tileSources.length){ next = 0; } diff --git a/src/viewport.js b/src/viewport.js index fafaff6d..b6b57e27 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -37,10 +37,23 @@ /** * @class Viewport - * @classdesc Handles coordinate-related functionality (zoom, pan, rotation, etc.) for an {@link OpenSeadragon.Viewer}. - * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#viewport}). - * * @memberof OpenSeadragon + * @classdesc Handles coordinate-related functionality (zoom, pan, rotation, etc.) + * for an {@link OpenSeadragon.Viewer}. + * @param {Object} options - Options for this Viewport. + * @param {Object} [options.margins] - See viewportMargins in {@link OpenSeadragon.Options}. + * @param {Number} [options.springStiffness] - See springStiffness in {@link OpenSeadragon.Options}. + * @param {Number} [options.animationTime] - See animationTime in {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See minZoomImageRatio in {@link OpenSeadragon.Options}. + * @param {Number} [options.maxZoomPixelRatio] - See maxZoomPixelRatio in {@link OpenSeadragon.Options}. + * @param {Number} [options.visibilityRatio] - See visibilityRatio in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See wrapHorizontal in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See wrapVertical in {@link OpenSeadragon.Options}. + * @param {Number} [options.defaultZoomLevel] - See defaultZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomLevel] - See minZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.maxZoomLevel] - See maxZoomLevel in {@link OpenSeadragon.Options}. + * @param {Number} [options.degrees] - See degrees in {@link OpenSeadragon.Options}. + * @param {Boolean} [options.homeFillsViewer] - See homeFillsViewer in {@link OpenSeadragon.Options}. */ $.Viewport = function( options ) { @@ -63,6 +76,15 @@ $.Viewport = function( options ) { delete options.config; } + this._margins = $.extend({ + left: 0, + top: 0, + right: 0, + bottom: 0 + }, options.margins || {}); + + delete options.margins; + $.extend( true, this, { //required settings @@ -89,6 +111,11 @@ $.Viewport = function( options ) { }, options ); + this._containerInnerSize = new $.Point( + Math.max(1, this.containerSize.x - (this._margins.left + this._margins.right)), + Math.max(1, this.containerSize.y - (this._margins.top + this._margins.bottom)) + ); + this.centerSpringX = new $.Spring({ initial: 0, springStiffness: this.springStiffness, @@ -105,44 +132,77 @@ $.Viewport = function( options ) { animationTime: this.animationTime }); - this.resetContentSize( this.contentSize ); + this._oldCenterX = this.centerSpringX.current.value; + this._oldCenterY = this.centerSpringY.current.value; + this._oldZoom = this.zoomSpring.current.value; + + if (this.contentSize) { + this.resetContentSize( this.contentSize ); + } else { + this.setHomeBounds(new $.Rect(0, 0, 1, 1), 1); + } + this.goHome( true ); this.update(); }; $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ - /** + * Updates the viewport's home bounds and constraints for the given content size. * @function + * @param {OpenSeadragon.Point} contentSize - size of the content in content units * @return {OpenSeadragon.Viewport} Chainable. * @fires OpenSeadragon.Viewer.event:reset-size */ resetContentSize: function( contentSize ){ - this.contentSize = contentSize; + $.console.assert(contentSize, "[Viewport.resetContentSize] contentSize is required"); + $.console.assert(contentSize instanceof $.Point, "[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point"); + $.console.assert(contentSize.x > 0, "[Viewport.resetContentSize] contentSize.x must be greater than 0"); + $.console.assert(contentSize.y > 0, "[Viewport.resetContentSize] contentSize.y must be greater than 0"); + + this.setHomeBounds(new $.Rect(0, 0, 1, contentSize.y / contentSize.x), contentSize.x); + return this; + }, + + /** + * Updates the viewport's home bounds and constraints. + * @function + * @param {OpenSeadragon.Rect} bounds - the new bounds in viewport coordinates + * @param {Number} contentFactor - how many content units per viewport unit + * @fires OpenSeadragon.Viewer.event:reset-size + */ + setHomeBounds: function(bounds, contentFactor) { + $.console.assert(bounds, "[Viewport.setHomeBounds] bounds is required"); + $.console.assert(bounds instanceof $.Rect, "[Viewport.setHomeBounds] bounds must be an OpenSeadragon.Rect"); + $.console.assert(bounds.width > 0, "[Viewport.setHomeBounds] bounds.width must be greater than 0"); + $.console.assert(bounds.height > 0, "[Viewport.setHomeBounds] bounds.height must be greater than 0"); + + this.homeBounds = bounds.clone(); + this.contentSize = this.homeBounds.getSize().times(contentFactor); this.contentAspectX = this.contentSize.x / this.contentSize.y; this.contentAspectY = this.contentSize.y / this.contentSize.x; - this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); - this.fitHeightBounds = new $.Rect( 0, 0, this.contentAspectY, this.contentAspectY); - - this.homeBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); if( this.viewer ){ /** - * Raised when the viewer's content size is reset (see {@link OpenSeadragon.Viewport#resetContentSize}). + * Raised when the viewer's content size or home bounds are reset + * (see {@link OpenSeadragon.Viewport#resetContentSize}, + * {@link OpenSeadragon.Viewport#setHomeBounds}). * * @event reset-size * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.Point} contentSize + * @property {OpenSeadragon.Rect} homeBounds + * @property {Number} contentFactor * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.viewer.raiseEvent( 'reset-size', { - contentSize: contentSize + contentSize: this.contentSize.clone(), + contentFactor: contentFactor, + homeBounds: this.homeBounds.clone() }); } - - return this; }, /** @@ -155,15 +215,18 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ var aspectFactor = this.contentAspectX / this.getAspectRatio(); + var output; if( this.homeFillsViewer ){ // fill the viewer and clip the image - return ( aspectFactor >= 1) ? + output = ( aspectFactor >= 1) ? aspectFactor : 1; } else { - return ( aspectFactor >= 1 ) ? + output = ( aspectFactor >= 1 ) ? 1 : aspectFactor; } + + return output / this.homeBounds.width; } }, @@ -216,16 +279,18 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ this.minZoomLevel : this.minZoomImageRatio * homeZoom; - return Math.min( zoom, homeZoom ); + return zoom; }, /** * @function */ getMaxZoom: function() { - var zoom = this.maxZoomLevel ? - this.maxZoomLevel : - ( this.contentSize.x * this.maxZoomPixelRatio / this.containerSize.x ); + var zoom = this.maxZoomLevel; + if (!zoom) { + zoom = this.contentSize.x * this.maxZoomPixelRatio / this._containerInnerSize.x; + zoom /= this.homeBounds.width; + } return Math.max( zoom, this.getHomeZoom() ); }, @@ -234,11 +299,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @function */ getAspectRatio: function() { - return this.containerSize.x / this.containerSize.y; + return this._containerInnerSize.x / this._containerInnerSize.y; }, /** * @function + * @returns {OpenSeadragon.Point} The size of the container, in screen coordinates. */ getContainerSize: function() { return new $.Point( @@ -250,6 +316,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * @function * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates. */ getBounds: function( current ) { var center = this.getCenter( current ), @@ -264,6 +331,22 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ ); }, + /** + * @function + * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, + * including the space taken by margins, in viewport coordinates. + */ + getBoundsWithMargins: function( current ) { + var bounds = this.getBounds(current); + var factor = this._containerInnerSize.x * this.getZoom(current); + bounds.x -= this._margins.left / factor; + bounds.y -= this._margins.top / factor; + bounds.width += (this._margins.left + this._margins.right) / factor; + bounds.height += (this._margins.top + this._margins.bottom) / factor; + return bounds; + }, + /** * @function * @param {Boolean} current - Pass true for the current location; defaults to false (target location). @@ -304,13 +387,9 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ height ); - newZoomPixel = this.zoomPoint.minus( - bounds.getTopLeft() - ).times( - this.containerSize.x / bounds.width - ); + newZoomPixel = this._pixelFromPoint(this.zoomPoint, bounds); deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); - deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom ); + deltaZoomPoints = deltaZoomPixels.divide( this._containerInnerSize.x * zoom ); return centerTarget.plus( deltaZoomPoints ); }, @@ -335,13 +414,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @return {OpenSeadragon.Rect} constrained bounds. */ _applyBoundaryConstraints: function( bounds, immediately ) { - var horizontalThreshold, - verticalThreshold, - left, - right, - top, - bottom, - dx = 0, + var dx = 0, dy = 0, newBounds = new $.Rect( bounds.x, @@ -350,49 +423,52 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ bounds.height ); - horizontalThreshold = this.visibilityRatio * newBounds.width; - verticalThreshold = this.visibilityRatio * newBounds.height; - - left = newBounds.x + newBounds.width; - right = 1 - newBounds.x; - top = newBounds.y + newBounds.height; - bottom = this.contentAspectY - newBounds.y; + var horizontalThreshold = this.visibilityRatio * newBounds.width; + var verticalThreshold = this.visibilityRatio * newBounds.height; if ( this.wrapHorizontal ) { //do nothing } else { - if ( left < horizontalThreshold ) { - dx = horizontalThreshold - left; + var thresholdLeft = newBounds.x + (newBounds.width - horizontalThreshold); + if (this.homeBounds.x > thresholdLeft) { + dx = this.homeBounds.x - thresholdLeft; } - if ( right < horizontalThreshold ) { - dx = dx ? - ( dx + right - horizontalThreshold ) / 2 : - ( right - horizontalThreshold ); + + var homeRight = this.homeBounds.x + this.homeBounds.width; + var thresholdRight = newBounds.x + horizontalThreshold; + if (homeRight < thresholdRight) { + var newDx = homeRight - thresholdRight; + if (dx) { + dx = (dx + newDx) / 2; + } else { + dx = newDx; + } } } if ( this.wrapVertical ) { //do nothing } else { - if ( top < verticalThreshold ) { - dy = ( verticalThreshold - top ); + var thresholdTop = newBounds.y + (newBounds.height - verticalThreshold); + if (this.homeBounds.y > thresholdTop) { + dy = this.homeBounds.y - thresholdTop; } - if ( bottom < verticalThreshold ) { - dy = dy ? - ( dy + bottom - verticalThreshold ) / 2 : - ( bottom - verticalThreshold ); + + var homeBottom = this.homeBounds.y + this.homeBounds.height; + var thresholdBottom = newBounds.y + verticalThreshold; + if (homeBottom < thresholdBottom) { + var newDy = homeBottom - thresholdBottom; + if (dy) { + dy = (dy + newDy) / 2; + } else { + dy = newDy; + } } } - if ( dx || dy || immediately ) { + if ( dx || dy ) { newBounds.x += dx; newBounds.y += dy; - if( newBounds.width > 1 ){ - newBounds.x = 0.5 - newBounds.width/2; - } - if( newBounds.height > this.contentAspectY ){ - newBounds.y = this.contentAspectY/2 - newBounds.height/2; - } } if( this.viewer ){ @@ -512,21 +588,28 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ } newBounds = this._applyBoundaryConstraints( newBounds, immediately ); + center = newBounds.getCenter(); } - if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) { - return this.panTo( constraints ? newBounds.getCenter() : center, immediately ); + if (immediately) { + this.panTo( center, true ); + return this.zoomTo(newZoom, null, true); + } + + if (Math.abs(newZoom - oldZoom) < 0.00000000001 || + Math.abs(newBounds.width - oldBounds.width) < 0.00000000001) { + return this.panTo( center, immediately ); } referencePoint = oldBounds.getTopLeft().times( - this.containerSize.x / oldBounds.width + this._containerInnerSize.x / oldBounds.width ).minus( newBounds.getTopLeft().times( - this.containerSize.x / newBounds.width + this._containerInnerSize.x / newBounds.width ) ).divide( - this.containerSize.x / oldBounds.width - - this.containerSize.x / newBounds.width + this._containerInnerSize.x / oldBounds.width - + this._containerInnerSize.x / newBounds.width ); return this.zoomTo( newZoom, referencePoint, immediately ); @@ -559,53 +642,27 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ }, /** - * @function + * Zooms so the image just fills the viewer vertically. * @param {Boolean} immediately * @return {OpenSeadragon.Viewport} Chainable. */ fitVertically: function( immediately ) { - var center = this.getCenter(); + var box = new $.Rect(this.homeBounds.x + (this.homeBounds.width / 2), this.homeBounds.y, + 0, this.homeBounds.height); - if ( this.wrapHorizontal ) { - center.x = ( 1 + ( center.x % 1 ) ) % 1; - this.centerSpringX.resetTo( center.x ); - this.centerSpringX.update(); - } - - if ( this.wrapVertical ) { - center.y = ( - this.contentAspectY + ( center.y % this.contentAspectY ) - ) % this.contentAspectY; - this.centerSpringY.resetTo( center.y ); - this.centerSpringY.update(); - } - - return this.fitBounds( this.fitHeightBounds, immediately ); + return this.fitBounds( box, immediately ); }, /** - * @function + * Zooms so the image just fills the viewer horizontally. * @param {Boolean} immediately * @return {OpenSeadragon.Viewport} Chainable. */ fitHorizontally: function( immediately ) { - var center = this.getCenter(); + var box = new $.Rect(this.homeBounds.x, this.homeBounds.y + (this.homeBounds.height / 2), + this.homeBounds.width, 0); - if ( this.wrapHorizontal ) { - center.x = ( - this.contentAspectX + ( center.x % this.contentAspectX ) - ) % this.contentAspectX; - this.centerSpringX.resetTo( center.x ); - this.centerSpringX.update(); - } - - if ( this.wrapVertical ) { - center.y = ( 1 + ( center.y % 1 ) ) % 1; - this.centerSpringY.resetTo( center.y ); - this.centerSpringY.update(); - } - - return this.fitBounds( this.fitWidthBounds, immediately ); + return this.fitBounds( box, immediately ); }, @@ -736,7 +793,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Raised when rotation has been changed. * - * @event update-viewport + * @event rotate * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. @@ -769,12 +826,16 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ newBounds = oldBounds, widthDeltaFactor; - this.containerSize = new $.Point( - newContainerSize.x, - newContainerSize.y + this.containerSize.x = newContainerSize.x; + this.containerSize.y = newContainerSize.y; + + this._containerInnerSize = new $.Point( + Math.max(1, newContainerSize.x - (this._margins.left + this._margins.right)), + Math.max(1, newContainerSize.y - (this._margins.top + this._margins.bottom)) ); if ( maintain ) { + // TODO: widthDeltaFactor will always be 1; probably not what's intended widthDeltaFactor = newContainerSize.x / this.containerSize.x; newBounds.width = oldBounds.width * widthDeltaFactor; newBounds.height = newBounds.width / this.getAspectRatio(); @@ -805,10 +866,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @function */ update: function() { - var oldCenterX = this.centerSpringX.current.value, - oldCenterY = this.centerSpringY.current.value, - oldZoom = this.zoomSpring.current.value, - oldZoomPixel, + var oldZoomPixel, newZoomPixel, deltaZoomPixels, deltaZoomPoints; @@ -819,7 +877,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ this.zoomSpring.update(); - if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) { + if (this.zoomPoint && this.zoomSpring.current.value != this._oldZoom) { newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true ); @@ -833,9 +891,15 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ this.centerSpringX.update(); this.centerSpringY.update(); - return this.centerSpringX.current.value != oldCenterX || - this.centerSpringY.current.value != oldCenterY || - this.zoomSpring.current.value != oldZoom; + var changed = this.centerSpringX.current.value != this._oldCenterX || + this.centerSpringY.current.value != this._oldCenterY || + this.zoomSpring.current.value != this._oldZoom; + + this._oldCenterX = this.centerSpringX.current.value; + this._oldCenterY = this.centerSpringY.current.value; + this._oldZoom = this.zoomSpring.current.value; + + return changed; }, @@ -846,7 +910,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ */ deltaPixelsFromPoints: function( deltaPoints, current ) { return deltaPoints.times( - this.containerSize.x * this.getZoom( current ) + this._containerInnerSize.x * this.getZoom( current ) ); }, @@ -857,7 +921,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ */ deltaPointsFromPixels: function( deltaPixels, current ) { return deltaPixels.divide( - this.containerSize.x * this.getZoom( current ) + this._containerInnerSize.x * this.getZoom( current ) ); }, @@ -867,11 +931,17 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @param {Boolean} current - Pass true for the current location; defaults to false (target location). */ pixelFromPoint: function( point, current ) { - var bounds = this.getBounds( current ); + return this._pixelFromPoint(point, this.getBounds( current )); + }, + + // private + _pixelFromPoint: function( point, bounds ) { return point.minus( bounds.getTopLeft() ).times( - this.containerSize.x / bounds.width + this._containerInnerSize.x / bounds.width + ).plus( + new $.Point(this._margins.left, this._margins.top) ); }, @@ -882,17 +952,27 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ */ pointFromPixel: function( pixel, current ) { var bounds = this.getBounds( current ); - return pixel.divide( - this.containerSize.x / bounds.width + return pixel.minus( + new $.Point(this._margins.left, this._margins.top) + ).divide( + this._containerInnerSize.x / bounds.width ).plus( bounds.getTopLeft() ); }, + // private + _viewportToImageDelta: function( viewerX, viewerY ) { + var scale = this.homeBounds.width; + return new $.Point(viewerX * (this.contentSize.x / scale), + viewerY * ((this.contentSize.y * this.contentAspectX) / scale)); + }, + /** * Translates from OpenSeadragon viewer coordinate system to image coordinate system. * This method can be called either by passing X,Y coordinates or an * OpenSeadragon.Point + * Note: not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead. * @function * @param {OpenSeadragon.Point} viewerX the point in viewport coordinate system. * @param {Number} viewerX X coordinate in viewport coordinate system. @@ -904,13 +984,26 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ //they passed a point instead of individual components return this.viewportToImageCoordinates( viewerX.x, viewerX.y ); } - return new $.Point( viewerX * this.contentSize.x, viewerY * this.contentSize.y * this.contentAspectX ); + + if (this.viewer && this.viewer.world.getItemCount() > 1) { + $.console.error('[Viewport.viewportToImageCoordinates] is not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead.'); + } + + return this._viewportToImageDelta(viewerX - this.homeBounds.x, viewerY - this.homeBounds.y); + }, + + // private + _imageToViewportDelta: function( imageX, imageY ) { + var scale = this.homeBounds.width; + return new $.Point((imageX / this.contentSize.x) * scale, + (imageY / this.contentSize.y / this.contentAspectX) * scale); }, /** * Translates from image coordinate system to OpenSeadragon viewer coordinate system * This method can be called either by passing X,Y coordinates or an * OpenSeadragon.Point + * Note: not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead. * @function * @param {OpenSeadragon.Point} imageX the point in image coordinate system. * @param {Number} imageX X coordinate in image coordinate system. @@ -922,7 +1015,15 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ //they passed a point instead of individual components return this.imageToViewportCoordinates( imageX.x, imageX.y ); } - return new $.Point( imageX / this.contentSize.x, imageY / this.contentSize.y / this.contentAspectX ); + + if (this.viewer && this.viewer.world.getItemCount() > 1) { + $.console.error('[Viewport.imageToViewportCoordinates] is not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead.'); + } + + var point = this._imageToViewportDelta(imageX, imageY); + point.x += this.homeBounds.x; + point.y += this.homeBounds.y; + return point; }, /** @@ -930,6 +1031,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * pixel coordinates to OpenSeadragon viewport rectangle coordinates. * This method can be called either by passing X,Y,width,height or an * OpenSeadragon.Rect + * Note: not accurate with multi-image; use TiledImage.imageToViewportRectangle instead. * @function * @param {OpenSeadragon.Rect} imageX the rectangle in image coordinate system. * @param {Number} imageX the X coordinate of the top left corner of the rectangle @@ -950,10 +1052,11 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ rect.x, rect.y, rect.width, rect.height ); } + coordA = this.imageToViewportCoordinates( imageX, imageY ); - coordB = this.imageToViewportCoordinates( + coordB = this._imageToViewportDelta( pixelWidth, pixelHeight ); return new $.Rect( @@ -969,6 +1072,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * the viewport in point coordinates to image rectangle coordinates. * This method can be called either by passing X,Y,width,height or an * OpenSeadragon.Rect + * Note: not accurate with multi-image; use TiledImage.viewportToImageRectangle instead. * @function * @param {OpenSeadragon.Rect} viewerX the rectangle in viewport coordinate system. * @param {Number} viewerX the X coordinate of the top left corner of the rectangle @@ -989,8 +1093,9 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ rect.x, rect.y, rect.width, rect.height ); } + coordA = this.viewportToImageCoordinates( viewerX, viewerY ); - coordB = this.viewportToImageCoordinates( pointWidth, pointHeight ); + coordB = this._viewportToImageDelta(pointWidth, pointHeight); return new $.Rect( coordA.x, coordA.y, @@ -1002,6 +1107,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Convert pixel coordinates relative to the viewer element to image * coordinates. + * Note: not accurate with multi-image. * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ @@ -1013,6 +1119,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Convert pixel coordinates relative to the image to * viewer element coordinates. + * Note: not accurate with multi-image. * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ @@ -1023,6 +1130,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Convert pixel coordinates relative to the window to image coordinates. + * Note: not accurate with multi-image. * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ @@ -1034,6 +1142,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Convert image coordinates to pixel coordinates relative to the window. + * Note: not accurate with multi-image. * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ @@ -1091,15 +1200,21 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * 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} viewportZoom The viewport zoom * target zoom. * @returns {Number} imageZoom The image zoom */ viewportToImageZoom: function( viewportZoom ) { - var imageWidth = this.viewer.source.dimensions.x; - var containerWidth = this.getContainerSize().x; - var viewportToImageZoomRatio = containerWidth / imageWidth; + if (this.viewer && this.viewer.world.getItemCount() > 1) { + $.console.error('[Viewport.viewportToImageZoom] is not accurate with multi-image.'); + } + + var imageWidth = this.contentSize.x; + var containerWidth = this._containerInnerSize.x; + var scale = this.homeBounds.width; + var viewportToImageZoomRatio = (containerWidth / imageWidth) * scale; return viewportZoom * viewportToImageZoomRatio; }, @@ -1109,15 +1224,21 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * 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 * target zoom. * @returns {Number} viewportZoom The viewport zoom */ imageToViewportZoom: function( imageZoom ) { - var imageWidth = this.viewer.source.dimensions.x; - var containerWidth = this.getContainerSize().x; - var viewportToImageZoomRatio = imageWidth / containerWidth; + if (this.viewer && this.viewer.world.getItemCount() > 1) { + $.console.error('[Viewport.imageToViewportZoom] is not accurate with multi-image.'); + } + + var imageWidth = this.contentSize.x; + var containerWidth = this._containerInnerSize.x; + var scale = this.homeBounds.width; + var viewportToImageZoomRatio = (imageWidth / containerWidth) / scale; return imageZoom * viewportToImageZoomRatio; } }; diff --git a/src/world.js b/src/world.js new file mode 100644 index 00000000..4b1c9826 --- /dev/null +++ b/src/world.js @@ -0,0 +1,393 @@ +/* + * OpenSeadragon - World + * + * 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 World + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Keeps track of all of the tiled images in the scene. + * @param {Object} options - World options. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this World. + **/ +$.World = function( options ) { + var _this = this; + + $.console.assert( options.viewer, "[World] options.viewer is required" ); + + $.EventSource.call( this ); + + this.viewer = options.viewer; + this._items = []; + this._needsDraw = false; + this._delegatedFigureSizes = function(event) { + _this._figureSizes(); + }; + + this._figureSizes(); +}; + +$.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.World.prototype */{ + /** + * Add the specified item. + * @param {OpenSeadragon.TiledImage} item - The item to add. + * @param {Number} [options.index] - Index for the item. If not specified, goes at the top. + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.World.event:metrics-change + */ + addItem: function( item, options ) { + $.console.assert(item, "[World.addItem] item is required"); + $.console.assert(item instanceof $.TiledImage, "[World.addItem] only TiledImages supported at this time"); + + options = options || {}; + if (options.index !== undefined) { + var index = Math.max(0, Math.min(this._items.length, options.index)); + this._items.splice(index, 0, item); + } else { + this._items.push( item ); + } + + this._figureSizes(); + this._needsDraw = true; + + item.addHandler('bounds-change', this._delegatedFigureSizes); + + /** + * Raised when an item is added to the World. + * @event add-item + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item that has been added. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'add-item', { + item: item + } ); + }, + + /** + * Get the item at the specified index. + * @param {Number} index - The item's index. + * @returns {OpenSeadragon.TiledImage} The item at the specified index. + */ + getItemAt: function( index ) { + $.console.assert(index !== undefined, "[World.getItemAt] index is required"); + return this._items[ index ]; + }, + + /** + * Get the index of the given item or -1 if not present. + * @param {OpenSeadragon.TiledImage} item - The item. + * @returns {Number} The index of the item or -1 if not present. + */ + getIndexOfItem: function( item ) { + $.console.assert(item, "[World.getIndexOfItem] item is required"); + return $.indexOf( this._items, item ); + }, + + /** + * @returns {Number} The number of items used. + */ + getItemCount: function() { + return this._items.length; + }, + + /** + * Change the index of a item so that it appears over or under others. + * @param {OpenSeadragon.TiledImage} item - The item to move. + * @param {Number} index - The new index. + * @fires OpenSeadragon.World.event:item-index-change + */ + setItemIndex: function( item, index ) { + $.console.assert(item, "[World.setItemIndex] item is required"); + $.console.assert(index !== undefined, "[World.setItemIndex] index is required"); + + var oldIndex = this.getIndexOfItem( item ); + + if ( index >= this._items.length ) { + throw new Error( "Index bigger than number of layers." ); + } + + if ( index === oldIndex || oldIndex === -1 ) { + return; + } + + this._items.splice( oldIndex, 1 ); + this._items.splice( index, 0, item ); + this._needsDraw = true; + + /** + * Raised when the order of the indexes has been changed. + * @event item-index-change + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item whose index has + * been changed + * @property {Number} previousIndex - The previous index of the item + * @property {Number} newIndex - The new index of the item + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'item-index-change', { + item: item, + previousIndex: oldIndex, + newIndex: index + } ); + }, + + /** + * Remove an item. + * @param {OpenSeadragon.TiledImage} item - The item to remove. + * @fires OpenSeadragon.World.event:remove-item + * @fires OpenSeadragon.World.event:metrics-change + */ + removeItem: function( item ) { + $.console.assert(item, "[World.removeItem] item is required"); + + var index = $.indexOf(this._items, item ); + if ( index === -1 ) { + return; + } + + item.removeHandler('bounds-change', this._delegatedFigureSizes); + this._items.splice( index, 1 ); + this._figureSizes(); + this._needsDraw = true; + this._raiseRemoveItem(item); + }, + + /** + * Remove all items. + * @fires OpenSeadragon.World.event:remove-item + * @fires OpenSeadragon.World.event:metrics-change + */ + removeAll: function() { + var item; + for (var i = 0; i < this._items.length; i++) { + item = this._items[i]; + item.removeHandler('bounds-change', this._delegatedFigureSizes); + } + + var removedItems = this._items; + this._items = []; + this._figureSizes(); + this._needsDraw = true; + + for (i = 0; i < removedItems.length; i++) { + item = removedItems[i]; + this._raiseRemoveItem(item); + } + }, + + /** + * Clears all tiles and triggers updates for all items. + */ + resetItems: function() { + for ( var i = 0; i < this._items.length; i++ ) { + this._items[i].reset(); + } + }, + + /** + * Updates (i.e. animates bounds of) all items. + */ + update: function() { + var animated = false; + for ( var i = 0; i < this._items.length; i++ ) { + animated = this._items[i].update() || animated; + } + + return animated; + }, + + /** + * Draws all items. + */ + draw: function() { + for ( var i = 0; i < this._items.length; i++ ) { + this._items[i].draw(); + } + + this._needsDraw = false; + }, + + /** + * @returns {Boolean} true if any items need updating. + */ + needsDraw: function() { + for ( var i = 0; i < this._items.length; i++ ) { + if ( this._items[i].needsDraw() ) { + return true; + } + } + return this._needsDraw; + }, + + /** + * @returns {OpenSeadragon.Rect} The smallest rectangle that encloses all items, in viewport coordinates. + */ + getHomeBounds: function() { + return this._homeBounds.clone(); + }, + + /** + * To facilitate zoom constraints, we keep track of the pixel density of the + * densest item in the World (i.e. the item whose content size to viewport size + * ratio is the highest) and save it as this "content factor". + * @returns {Number} the number of content units per viewport unit. + */ + getContentFactor: function() { + return this._contentFactor; + }, + + /** + * Arranges all of the TiledImages with the specified settings. + * @param {Object} options - Specifies how to arrange. + * @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.tileSize] - See collectionTileSize in {@link OpenSeadragon.Options}. + * @param {Number} [options.tileMargin] - See collectionTileMargin in {@link OpenSeadragon.Options}. + * @fires OpenSeadragon.World.event:metrics-change + */ + arrange: function(options) { + options = options || {}; + var immediately = options.immediately || false; + var layout = options.layout || $.DEFAULT_SETTINGS.collectionLayout; + var rows = options.rows || $.DEFAULT_SETTINGS.collectionRows; + 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 x = 0; + var y = 0; + var item, box, width, height, position; + for (var i = 0; i < this._items.length; i++) { + if (i && (i % wrap) === 0) { + if (layout === 'horizontal') { + y += increment; + x = 0; + } else { + x += increment; + y = 0; + } + } + + item = this._items[i]; + box = item.getBounds(); + if (box.width > box.height) { + width = tileSize; + } else { + width = tileSize * (box.width / box.height); + } + + height = width * (box.height / box.width); + position = new $.Point(x + ((tileSize - width) / 2), + y + ((tileSize - height) / 2)); + + item.setPosition(position, immediately); + item.setWidth(width, immediately); + + if (layout === 'horizontal') { + x += increment; + } else { + y += increment; + } + } + }, + + // private + _figureSizes: function() { + var oldHomeBounds = this._homeBounds ? this._homeBounds.clone() : null; + var oldContentSize = this._contentSize ? this._contentSize.clone() : null; + var oldContentFactor = this._contentFactor || 0; + + if ( !this._items.length ) { + this._homeBounds = new $.Rect(0, 0, 1, 1); + this._contentSize = new $.Point(1, 1); + this._contentFactor = 1; + } else { + var bounds = this._items[0].getBounds(); + this._contentFactor = this._items[0].getContentSize().x / bounds.width; + var left = bounds.x; + var top = bounds.y; + var right = bounds.x + bounds.width; + var bottom = bounds.y + bounds.height; + var box; + for ( var i = 1; i < this._items.length; i++ ) { + box = this._items[i].getBounds(); + this._contentFactor = Math.max(this._contentFactor, this._items[i].getContentSize().x / box.width); + left = Math.min( left, box.x ); + top = Math.min( top, box.y ); + right = Math.max( right, box.x + box.width ); + bottom = Math.max( bottom, box.y + box.height ); + } + + this._homeBounds = new $.Rect( left, top, right - left, bottom - top ); + this._contentSize = new $.Point(this._homeBounds.width * this._contentFactor, + this._homeBounds.height * this._contentFactor); + } + + if (this._contentFactor !== oldContentFactor || !this._homeBounds.equals(oldHomeBounds) || + !this._contentSize.equals(oldContentSize)) { + /** + * Raised when the home bounds or content factor change. + * @event metrics-change + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('metrics-change', {}); + } + }, + + // private + _raiseRemoveItem: function(item) { + /** + * Raised when an item is removed. + * @event remove-item + * @memberOf OpenSeadragon.World + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the World which raised the event. + * @property {OpenSeadragon.TiledImage} item - The item's underlying item. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'remove-item', { item: item } ); + } +}); + +}( OpenSeadragon )); diff --git a/test/coverage.html b/test/coverage.html new file mode 100644 index 00000000..2c3bc093 --- /dev/null +++ b/test/coverage.html @@ -0,0 +1,79 @@ + + + + + OpenSeadragon QUnit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/demo/collections/index.html b/test/demo/collections/index.html new file mode 100644 index 00000000..46b4e7f2 --- /dev/null +++ b/test/demo/collections/index.html @@ -0,0 +1,31 @@ + + + + OpenSeadragon Collections Demo + + + + + + + + + diff --git a/test/demo/collections/main.js b/test/demo/collections/main.js new file mode 100644 index 00000000..83563085 --- /dev/null +++ b/test/demo/collections/main.js @@ -0,0 +1,381 @@ +/* globals $, App */ + +(function() { + + window.App = { + init: function() { + var self = this; + + var testInitialOpen = true; + var testOverlays = false; + var testMargins = false; + var testNavigator = false; + var margins; + + var config = { + // debugMode: true, + zoomPerScroll: 1.02, + showNavigator: testNavigator, + useCanvas: true, + // defaultZoomLevel: 2, + // homeFillsViewer: true, + // sequenceMode: true, + // showReferenceStrip: true, + // referenceStripScroll: 'vertical', + navPrevNextWrap: false, + preserveViewport: false, + // collectionMode: true, + // collectionRows: 1, + // collectionLayout: 'vertical', + // collectionTileSize: 10, + // collectionTileMargin: 10, + // wrapHorizontal: true, + // wrapVertical: true, + id: "contentDiv", + prefixUrl: "../../../build/openseadragon/images/" + }; + + var highsmith = { + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2008", + Url: "http://openseadragon.github.io/example-images/highsmith/highsmith_files/", + Format: "jpg", + Overlap: "2", + TileSize: "256", + Size: { + Height: "9221", + Width: "7026" + } + } + }; + + if (testInitialOpen) { + config.tileSources = [ + { + tileSource: "../../data/testpattern.dzi", + x: 4, + y: 2, + width: 2 + }, + { + tileSource: "../../data/tall.dzi", + x: 1.5, + y: 0, + width: 1 + }, + { + tileSource: '../../data/wide.dzi', + opacity: 1, + x: 0, + y: 1.5, + height: 1 + } + ]; + + // config.tileSources = { + // tileSource: highsmith, + // width: 1 + // }; + } + + if (testOverlays) { + config.overlays = [ + { + id: "overlay1", + x: 2, + y: 0, + width: 0.25, + height: 0.25 + }, + { + px: 13, + py: 120, + width: 124, + height: 132, + id: "overlay" + }, + { + px: 400, + py: 500, + width: 400, + height: 400, + id: "fixed-overlay", + placement: "TOP_LEFT" + } + ]; + } + + if (testMargins) { + margins = { + top: 250, + left: 250, + right: 250, + bottom: 250 + }; + + config.viewportMargins = margins; + } + + this.viewer = OpenSeadragon(config); + + if (testInitialOpen) { + function openHandler() { + self.viewer.removeHandler('open', openHandler); + } + + this.viewer.addHandler( "open", openHandler); + } + + if (testMargins) { + this.viewer.addHandler('animation', function() { + var box = new OpenSeadragon.Rect(margins.left, margins.top, + $('#contentDiv').width() - (margins.left + margins.right), + $('#contentDiv').height() - (margins.top + margins.bottom)); + + self.viewer.drawer.debugRect(box); + }); + } + + if (!testInitialOpen) { + this.basicTest(); + } + }, + + // ---------- + shrink: function(index) { + index = index || 0; + var image = this.viewer.world.getItemAt(index); + image.setWidth(image.getBounds().width * 0.3); + }, + + // ---------- + move: function(index) { + index = index || 0; + var image = this.viewer.world.getItemAt(index); + var point = image.getBounds().getTopLeft(); + point.x += image.getBounds().width * 0.3; + image.setPosition(point); + }, + + // ---------- + add: function() { + var self = this; + + this.viewer.addTiledImage({ + tileSource: "../../data/testpattern.dzi", + width: 1, + success: function() { + self.viewer.viewport.goHome(); + } + }); + }, + + // ---------- + toggle: function() { + var $el = $(this.viewer.element); + $el.toggleClass('small'); + }, + + // ---------- + basicTest: function() { + var self = this; + + this.viewer.addHandler('open', function() { + }); + + this.viewer.open({ + tileSource: "../../data/testpattern.dzi", + width: 1 + }); + }, + + // ---------- + crossTest: function() { + var self = this; + + this.viewer.addHandler( "open", function() { + var options = { + tileSource: '../../data/wide.dzi', + opacity: 1, + x: 0, + y: 1.5, + height: 1 + }; + + var addItemHandler = function( event ) { + if ( event.options === options ) { + self.viewer.world.removeHandler( "add-item", addItemHandler ); + self.viewer.viewport.goHome(); + } + }; + self.viewer.world.addHandler( "add-item", addItemHandler ); + self.viewer.addTiledImage( options ); + }); + + this.viewer.open({ + tileSource: "../../data/tall.dzi", + x: 1.5, + y: 0, + width: 1 + }); + }, + + // ---------- + crossTest2: function() { + this.viewer.open([ + { + tileSource: "../../data/tall.dzi", + x: 1.5, + y: 0, + width: 1 + }, + { + tileSource: '../../data/wide.dzi', + x: 0, + y: 1.5, + height: 1 + } + ]); + }, + + // ---------- + crossTest3: function() { + var self = this; + var expected = 2; + var loaded = 0; + + this.viewer.world.addHandler('add-item', function() { + loaded++; + if (loaded === expected) { + // self.viewer.viewport.goHome(); + } + }); + + this.viewer.addTiledImage({ + tileSource: "../../data/tall.dzi", + x: 1.5, + y: 0, + width: 1 + }); + + this.viewer.addTiledImage({ + tileSource: '../../data/wide.dzi', + opacity: 1, + x: 0, + y: 1.5, + height: 1 + }); + }, + + // ---------- + collectionTest: function() { + var tileSources = []; + var random; + for (var i = 0; i < 10; i++) { + random = Math.random(); + if (random < 0.33) { + tileSources.push('../../data/testpattern.dzi'); + } else if (random < 0.66) { + tileSources.push('../../data/tall.dzi'); + } else { + tileSources.push('../../data/wide.dzi'); + } + } + + this.viewer.open(tileSources); + }, + + // ---------- + gridTest: function() { + var self = this; + var startX = -3; + var expected = 0; + var loaded = 0; + + this.viewer.addHandler( "open", function() { + self.viewer.world.addHandler('add-item', function() { + loaded++; + if (loaded === expected) { + self.viewer.viewport.goHome(true); + } + }); + + var x, y; + for (y = 0; y < 6; y++) { + for (x = 0; x < 6; x++) { + if (!x && !y) { + continue; + } + + var options = { + tileSource: '../../data/testpattern.dzi', + x: startX + x, + y: y, + width: 1 + }; + + expected++; + self.viewer.addTiledImage( options ); + } + } + }); + + this.viewer.open({ + tileSource: "../../data/testpattern.dzi", + x: startX, + y: 0, + width: 1 + }); + }, + + // ---------- + bigTest: function() { + this.viewer.open({ + tileSource: "../../data/testpattern.dzi", + x: -2, + y: -2, + width: 6 + }); + }, + + // ---------- + cjTest: function() { + var imageKey = "e-pluribus-unum"; + var imageXML = ''; + var $xml = $($.parseXML(imageXML)); + var $image = $xml.find('Image'); + var $size = $xml.find('Size'); + + var dzi = { + Image: { + xmlns: $image.attr('xmlns'), + Url: "http://chrisjordan.com/dzi/" + imageKey + '_files/', + Format: $image.attr('Format'), + Overlap: $image.attr('Overlap'), + TileSize: $image.attr('TileSize'), + Size: { + Height: $size.attr('Height'), + Width: $size.attr('Width') + } + } + }; + + this.viewer.open({ + tileSource: dzi, + width: 100 + }); + }, + + // ---------- + stanfordTest: function() { + var info = {"@context":"http://library.stanford.edu/iiif/image-api/1.1/context.json","@id":"http://ids.lib.harvard.edu/ids/iiif/48530377","width":6251,"height":109517,"scale_factors":[1,2,4,8,16,32],"tile_width":256,"tile_height":256,"formats":["jpg"],"qualities":["native"],"profile":"http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level1"}; + + this.viewer.open(info); + } + }; + + $(document).ready(function() { + App.init(); + }); + +})(); diff --git a/test/demo/fitboundswithconstraints.html b/test/demo/fitboundswithconstraints.html index c0317c98..51889d88 100644 --- a/test/demo/fitboundswithconstraints.html +++ b/test/demo/fitboundswithconstraints.html @@ -13,7 +13,7 @@ #highlights li { cursor: pointer; } - + @@ -23,33 +23,33 @@ - + viewport.fitBoundsWithConstraints(bounds); viewport.fitBounds(bounds); viewport.fitBounds(bounds).applyConstraints(); - + + + + + + + + + Pause + Add More + Image Count: + + +
rendered