diff --git a/Gruntfile.js b/Gruntfile.js index 18ac1bd2..8dff6cd2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -48,7 +48,10 @@ module.exports = function(grunt) { "src/tile.js", "src/overlay.js", "src/drawer.js", - "src/viewport.js" + "src/viewport.js", + "src/tiledimage.js", + "src/tilecache.js", + "src/world.js" ]; // ---------- diff --git a/changelog.txt b/changelog.txt index e9c895f1..babc5979 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,8 @@ OPENSEADRAGON CHANGELOG ======================= +2.0.0: (in progress) + 1.2.0: (in progress) * New combined IIIF TileSource for 1.0 through 2.0 (#441) diff --git a/src/drawer.js b/src/drawer.js index d7210114..fbb8db0f 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -34,23 +34,9 @@ (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. @@ -58,11 +44,13 @@ var DEVICE_SCREEN = $.getWindowSize(), * @param {Element} element - Parent element. */ $.Drawer = function( options ) { + var self = 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,64 +60,17 @@ $.Drawer = function( options ) { }; } - this._worldX = options.x || 0; - delete options.x; - this._worldY = options.y || 0; - delete options.y; + $.console.assert( options.viewport, "[Drawer] options.viewport is required" ); + $.console.assert( options.element, "[Drawer] options.element is required" ); - // Ratio of zoomable image height to width. - this.normHeight = options.source.dimensions.y / options.source.dimensions.x; - - if ( options.width ) { - this._scale = options.width; - delete options.width; - - if ( options.height ) { - $.console.error( "specifying both width and height to a drawer is not supported" ); - delete options.height; - } - } else if ( options.height ) { - this._scale = options.height / this.normHeight; - delete options.height; - } else { - this._scale = 1; + if ( options.source ) { + $.console.error( "[Drawer] options.source is no longer accepted; use TiledImage instead" ); } - this._worldWidth = this._scale; - this._worldHeight = this.normHeight * this._scale; - - $.extend( true, this, { - - //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 viewport again? - - - //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 ); /** @@ -138,7 +79,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}. @@ -174,7 +115,24 @@ $.Drawer = function( options ) { this.container.style.textAlign = "left"; this.container.appendChild( this.canvas ); - //this.profiler = new $.Profiler(); + // We need a callback to give image manipulation a chance to happen + this._drawingHandler = function(args) { + if (self.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'. + */ + self.viewer.raiseEvent('tile-drawing', args); + } + }; }; $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ @@ -272,6 +230,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ getOpacity: function() { return this.opacity; }, + /** * Returns whether the Drawer is scheduled for an update at the * soonest possible opportunity. @@ -280,7 +239,8 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * soonest possible opportunity. */ needsUpdate: function() { - return this.updateAgain; + $.console.error( "[Drawer.needsUpdate] this function is deprecated." ); + return false; }, /** @@ -290,7 +250,8 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * this Drawer. */ numTilesLoaded: function() { - return this.tilesLoaded.length; + $.console.error( "[Drawer.numTilesLoaded] this function is deprecated." ); + return 0; }, /** @@ -300,9 +261,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @return {OpenSeadragon.Drawer} Chainable. */ reset: function() { - clearTiles( this ); - this.lastResetTime = $.now(); - this.updateAgain = true; + $.console.error( "[Drawer.reset] this function is deprecated." ); return this; }, @@ -312,11 +271,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @return {OpenSeadragon.Drawer} Chainable. */ update: function() { - //this.profiler.beginUpdate(); - this.midUpdate = true; - updateViewport( this ); - this.midUpdate = false; - //this.profiler.endUpdate(); + $.console.error( "[Drawer.update] this function is deprecated." ); return this; }, @@ -335,916 +290,137 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @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; + }, + + clear: function() { + this.canvas.innerHTML = ""; + if ( this.useCanvas ) { + var viewportSize = this.viewport.getContainerSize(); + 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 ); + } + }, + + drawTile: function( tile ) { + 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, this._drawingHandler ); + this._restoreRotationChanges( tile ); + } else { + tile.drawCanvas( this.context, this._drawingHandler ); + } + } else { + tile.drawHTML( this.canvas ); + } + }, + + drawDebugInfo: function( tile, count, i ){ + if ( this.useCanvas ) { + this.context.save(); + this.context.lineWidth = 2; + this.context.font = 'small-caps bold 13px ariel'; + this.context.strokeStyle = this.debugGridColor; + this.context.fillStyle = this.debugGridColor; + + this._offsetForRotation( tile, this.canvas, this.context, this.viewport.degrees ); + + this.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. + 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, + tile.position.y - 30 + ); + this.context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x, + tile.position.y - 20 + ); + } + this.context.fillText( + "Level: " + tile.level, + tile.position.x + 10, + tile.position.y + 20 + ); + this.context.fillText( + "Column: " + tile.x, + tile.position.x + 10, + tile.position.y + 30 + ); + this.context.fillText( + "Row: " + tile.y, + tile.position.x + 10, + tile.position.y + 40 + ); + this.context.fillText( + "Order: " + i + " of " + count, + tile.position.x + 10, + tile.position.y + 50 + ); + this.context.fillText( + "Size: " + tile.size.toString(), + tile.position.x + 10, + tile.position.y + 60 + ); + this.context.fillText( + "Position: " + tile.position.toString(), + tile.position.x + 10, + tile.position.y + 70 + ); + this._restoreRotationChanges( tile, this.canvas, this.context ); + this.context.restore(); + } + }, + + _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; + }, + + _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 - * @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 * drawer._scale, - 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; - - viewportTL.x -= drawer._worldX; - viewportTL.y -= drawer._worldY; - viewportBR.x -= drawer._worldX; - viewportBR.y -= drawer._worldY; - - // 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(); - } - - //Don't draw if completely outside of the viewport - if ( !drawer.wrapHorizontal && - ( viewportBR.x < 0 || viewportTL.x > drawer._worldWidth ) ) { - return; - } else if - ( !drawer.wrapVertical && - ( viewportBR.y < 0 || viewportTL.y > drawer._worldHeight ) ) { - return; - } - - // Calculate viewport rect / bounds - if ( !drawer.wrapHorizontal ) { - viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, drawer._worldWidth ); - } - if ( !drawer.wrapVertical ) { - viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, drawer._worldHeight ); - } - - // 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 * drawer._scale; - - 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 * drawer._scale; - - zeroRatioT = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( - Math.max( - drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1, - 0 - ) - ), - false - ).x * drawer._scale; - - 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.divide( drawer._scale )); - tileBR = drawer.source.getTileAtPoint( level, viewportBR.divide( drawer._scale )); - 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._worldWidth, - drawer._worldHeight - ), - 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, - drawer - ); - - 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, 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 += worldWidth * ( x - xMod ) / numTiles.x; - bounds.y += worldHeight * ( 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 ) { - var insertionIndex, - cutoff, - worstTile, - worstTime, - worstLevel, - worstTileIndex, - prevTile, - prevTime, - prevLevel, - i; - - tile.loading = false; - - if ( drawer.midUpdate ) { - $.console.warn( "Tile load callback in middle of drawing routine." ); - return; - } else 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; - - insertionIndex = drawer.tilesLoaded.length; - - if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) { - cutoff = Math.ceil( Math.log( drawer.source.getTileSize(tile.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 ] = tile; - drawer.updateAgain = true; -} - - -function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, drawer ){ - var boundsTL = tile.bounds.getTopLeft(); - - boundsTL.x *= drawer._scale; - boundsTL.y *= drawer._scale; - boundsTL.x += drawer._worldX; - boundsTL.y += drawer._worldY; - - var boundsSize = tile.bounds.getSize(); - - boundsSize.x *= drawer._scale; - boundsSize.y *= drawer._scale; - - 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( 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/openseadragon.js b/src/openseadragon.js index f684f3dc..e4053374 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -2243,7 +2243,8 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ debug: nullfunction, info: nullfunction, warn: nullfunction, - error: nullfunction + error: nullfunction, + assert: nullfunction }; diff --git a/src/point.js b/src/point.js index 38aad1f1..0004426b 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. diff --git a/src/rectangle.js b/src/rectangle.js index 99172e7d..3a3ba547 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. diff --git a/src/tilecache.js b/src/tilecache.js new file mode 100644 index 00000000..7228eff6 --- /dev/null +++ b/src/tilecache.js @@ -0,0 +1,133 @@ +/* + * 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( $ ){ + +var TileRecord = function( params ) { + $.console.assert( params, "[TileCache.cacheTile] params is required" ); + $.console.assert( params.tile, "[TileCache.cacheTile] params.tile is required" ); + $.console.assert( params.tiledImage, "[TileCache.cacheTile] params.tiledImage is required" ); + this.tile = params.tile; + this.tiledImage = params.tiledImage; +}; + +/** + * @class TileCache + * @classdesc + */ +$.TileCache = function( options ) { + options = options || {}; + + this._tilesLoaded = []; + this._maxImageCacheCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount; +}; + +$.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ + /** + * Returns the total number of tiles that have been loaded by this TileCache. + * @method + * @returns {Number} - The total number of tiles that have been loaded by + * this TileCache. + */ + numTilesLoaded: function() { + return this._tilesLoaded.length; + }, + + cacheTile: function( params ) { + $.console.assert( params, "[TileCache.cacheTile] params is required" ); + $.console.assert( params.tile, "[TileCache.cacheTile] params.tile is required" ); + $.console.assert( params.tiledImage, "[TileCache.cacheTile] params.tiledImage is required" ); + + var cutoff = params.cutoff || 0; + var insertionIndex = this._tilesLoaded.length; + + if ( this._tilesLoaded.length >= 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 ) { + worstTile.unload(); + insertionIndex = worstTileIndex; + } + } + + this._tilesLoaded[ insertionIndex ] = new TileRecord({ + tile: params.tile, + tiledImage: params.tiledImage + }); + }, + + /** + * Clears all tiles associated with the specified tiledImage. + * @method + */ + clearTilesFor: function( tiledImage ) { + var tileRecord; + for ( var i = 0; i < this._tilesLoaded.length; ++i ) { + tileRecord = this._tilesLoaded[ i ]; + if ( tileRecord.tiledImage === tiledImage ) { + tileRecord.tile.unload(); + this._tilesLoaded.splice( i, 1 ); + i--; + } + } + } +}; + +}( OpenSeadragon )); diff --git a/src/tiledimage.js b/src/tiledimage.js new file mode 100644 index 00000000..4a19278c --- /dev/null +++ b/src/tiledimage.js @@ -0,0 +1,809 @@ +/* + * 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( $ ){ + +/** + * @class TiledImage + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * A new instance is created for each TileSource opened. + * + * @memberof OpenSeadragon + */ +$.TiledImage = function( options ) { + $.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" ); + + this._tileCache = options.tileCache; + delete options.tileCache; + + this._drawer = options.drawer; + delete options.drawer; + + this._imageLoader = options.imageLoader; + delete options.imageLoader; + + this._worldX = options.x || 0; + delete options.x; + this._worldY = options.y || 0; + delete options.y; + + // Ratio of zoomable image height to width. + this.normHeight = options.source.dimensions.y / options.source.dimensions.x; + + if ( options.width ) { + this._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 ) { + this._scale = options.height / this.normHeight; + delete options.height; + } else { + this._scale = 1; + } + + this._worldWidth = this._scale; + this._worldHeight = this.normHeight * this._scale; + + $.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. + midUpdate: false, // Is the tiledImage currently updating the viewport? + updateAgain: true, // Does the tiledImage need to update the viewport again? + + //configurable settings + 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 ); +}; + +$.TiledImage.prototype = /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * Returns whether the TiledImage is scheduled for an update at the + * soonest possible opportunity. + * @method + * @returns {Boolean} - Whether the TiledImage is scheduled for an update at the + * soonest possible opportunity. + */ + needsUpdate: function() { + return this.updateAgain; + }, + + /** + * Clears all tiles and triggers an update on the next call to + * TiledImage.prototype.update(). + * @method + * @return {OpenSeadragon.TiledImage} Chainable. + */ + reset: function() { + this._tileCache.clearTilesFor(this); + this.lastResetTime = $.now(); + this.updateAgain = true; + return this; + }, + + /** + * Forces the TiledImage to update. + * @method + * @return {OpenSeadragon.TiledImage} Chainable. + */ + update: function() { + this.midUpdate = true; + updateViewport( this ); + this.midUpdate = false; + return this; + }, + + /** + * Destroy the TiledImage (unload current loaded tiles) + * @method + * @return null + */ + destroy: function() { + this.reset(); + }, + + getWorldBounds: function() { + return new $.Rect( this._worldX, this._worldY, this._worldWidth, this._worldHeight ); + }, + + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + } +}; + +/** + * @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.updateAgain = false; + + var tile, + level, + best = null, + haveDrawn = false, + currentTime = $.now(), + viewportSize = tiledImage.viewport.getContainerSize(), + viewportBounds = tiledImage.viewport.getBounds( true ), + viewportTL = viewportBounds.getTopLeft(), + viewportBR = viewportBounds.getBottomRight(), + zeroRatioC = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( 0 ), + true + ).x * tiledImage._scale, + 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; + + viewportTL.x -= tiledImage._worldX; + viewportTL.y -= tiledImage._worldY; + viewportBR.x -= tiledImage._worldX; + viewportBR.y -= tiledImage._worldY; + + // 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) { + var rotatedBounds = viewportBounds.rotate( degrees ); + viewportTL = rotatedBounds.getTopLeft(); + viewportBR = rotatedBounds.getBottomRight(); + } + + //Don't draw if completely outside of the viewport + if ( !tiledImage.wrapHorizontal && + ( viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidth ) ) { + return; + } else if + ( !tiledImage.wrapVertical && + ( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeight ) ) { + return; + } + + // Calculate viewport rect / bounds + if ( !tiledImage.wrapHorizontal ) { + viewportTL.x = Math.max( viewportTL.x, 0 ); + viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidth ); + } + if ( !tiledImage.wrapVertical ) { + viewportTL.y = Math.max( viewportTL.y, 0 ); + viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeight ); + } + + // 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._scale; + + 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._scale; + + zeroRatioT = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( + Math.max( + tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, + 0 + ) + ), + false + ).x * tiledImage._scale; + + 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.updateAgain = 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 {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', { + 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._scale )); + tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scale )); + 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._worldWidth, + tiledImage._worldHeight + ), + 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.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'update-tile', { + 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 needsUpdate = blendTile( + tiledImage, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + if ( needsUpdate ) { + tiledImage.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, 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 += worldWidth * ( x - xMod ) / numTiles.x; + bounds.y += worldHeight * ( 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 ) { + tile.loading = false; + + if ( tiledImage.midUpdate ) { + $.console.warn( "Tile load callback in middle of drawing routine." ); + return; + } else if ( !image ) { + $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + if( !tiledImage.debugMode ){ + tile.exists = false; + return; + } + } else if ( time < tiledImage.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + return; + } + + 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 + }); + + tiledImage.updateAgain = true; +} + + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ + var boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= tiledImage._scale; + boundsTL.y *= tiledImage._scale; + boundsTL.x += tiledImage._worldX; + boundsTL.y += tiledImage._worldY; + + var boundsSize = tile.bounds.getSize(); + + boundsSize.x *= tiledImage._scale; + boundsSize.y *= tiledImage._scale; + + 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, + collectionTileSource; + + // We need a callback to give image manipulation a chance to happen + var drawingHandler = function(args) { + if (tiledImage.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 tiledImage 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'. + */ + tiledImage.viewer.raiseEvent('tile-drawing', args); + } + }; + + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { + tile = lastDrawn[ i ]; + tiledImage._drawer.drawTile( tile ); + 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.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'tile-drawn', { + tile: tile + }); + } + } +} + +}( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index 6e5287b3..93c8394b 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -151,9 +151,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 @@ -264,7 +262,6 @@ $.Viewer = function( options ) { this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); this.keyboardCommandArea = $.makeNeutralElement( "textarea" ); - this.drawersContainer = $.makeNeutralElement( "div" ); this.overlaysContainer = $.makeNeutralElement( "div" ); this.canvas.className = "openseadragon-canvas"; @@ -304,7 +301,6 @@ $.Viewer = function( options ) { this.container.insertBefore( this.canvas, this.container.firstChild ); this.container.insertBefore( this.keyboardCommandArea, 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 @@ -540,7 +536,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } this.clearOverlays(); - this.drawersContainer.innerHTML = ""; this.overlaysContainer.innerHTML = ""; if ( this.drawer ) { @@ -549,7 +544,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.source = null; this.drawer = null; - this.drawers = []; + this.world = null; this.viewport = this.preserveViewport ? this.viewport : null; @@ -627,7 +622,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.element = null; }, - /** * @function * @return {Boolean} @@ -1038,77 +1032,71 @@ $.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 layer's aspect ratio. + * The other dimension will be calculated according to the item's aspect ratio. * @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. + * @param {String|Object|Function} options.tileSource - The TileSource of the item. + * @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 world coordinates. * @param {Number} [options.y=0] The Y position for the image in world coordinates. * @param {Number} [options.width=1] The width for the image in world coordinates. * @param {Number} [options.height] The height for the image in world coordinates. - * @returns {OpenSeadragon.Viewer} Chainable. - * @fires OpenSeadragon.Viewer.event:add-layer - * @fires OpenSeadragon.Viewer.event:add-layer-failed + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.Viewer.event:add-item-failed */ - addLayer: function( options ) { + addTiledImage: function( options ) { + $.console.assert(options, "[Viewer.addTiledImage] options is required"); + $.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required"); + var _this = this, tileSource = options.tileSource; 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." ); + throw new Error( "An image must be loaded before adding additional images." ); } - function raiseAddLayerFailed( event ) { + function raiseAddItemFailed( 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 ); } getTileSourceImplementation( this, 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.", source: tileSource, options: options }); return; } - var drawer = new $.Drawer({ + var tiledImage = new $.TiledImage({ viewer: _this, source: tileSource, viewport: _this.viewport, - element: _this.drawersContainer, + drawer: _this.drawer, + tileCache: _this.tileCache, + imageLoader: _this.imageLoader, x: options.x, y: options.y, width: options.width, height: options.height, - opacity: options.opacity !== undefined ? - options.opacity : _this.opacity, - maxImageCacheCount: _this.maxImageCacheCount, imageLoaderLimit: _this.imageLoaderLimit, minZoomImageRatio: _this.minZoomImageRatio, wrapHorizontal: _this.wrapHorizontal, @@ -1117,168 +1105,94 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, 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 + + _this.world.addItem( tiledImage, { + index: options.index }); }, function( event ) { event.options = options; - raiseAddLayerFailed(event); + raiseAddItemFailed(event); } ); + }, + /** + * @function + * @private + */ + addLayer: function( options ) { + var self = this; + + $.console.error( "[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead." ); + + var addItemHandler = function(event) { + self.world.removeHandler("add-item", addItemHandler); + self.raiseEvent("add-layer", { + options: options, + drawer: event.item + }); + }; + + var failureHandler = function(event) { + self.removeHandler("add-item-failed", failureHandler); + self.raiseEvent("add-layer-failed", event); + }; + + this.world.addHandler("add-item", addItemHandler); + this.addHandler("add-item-failed", failureHandler); + this.addTiledImage(options); 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. + * @function + * @private */ 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. + * @function + * @private */ 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. + * @function + * @private */ 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 + * @function + * @private */ 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 + * @private */ 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() { @@ -1926,7 +1840,6 @@ function openTileSource( viewer, source, options ) { collectionMode: true, collectionTileSource: _this.source, containerSize: THIS[ _this.hash ].prevContainerSize, - contentSize: _this.source.dimensions, springStiffness: _this.springStiffness, animationTime: _this.animationTime, showNavigator: false, @@ -1944,7 +1857,6 @@ function openTileSource( viewer, source, options ) { } _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, @@ -1961,23 +1873,52 @@ function openTileSource( viewer, source, options ) { }); } - if( _this.preserveViewport ){ - _this.viewport.resetContentSize( _this.source.dimensions ); - } + // TODO: what to do about this? + // if( _this.preserveViewport ){ + // _this.viewport.resetContentSize( _this.source.dimensions ); + // } _this.source.overlays = _this.source.overlays || []; + _this.imageLoader = new $.ImageLoader(); + + _this.tileCache = new $.TileCache({ + maxImageCacheCount: _this.maxImageCacheCount + }); + + _this.world = new $.World({ + viewer: _this + }); + + _this.world.addHandler('add-item', function(event) { + _this.viewport.setHomeBounds(_this.world.getHomeBounds(), _this.world.getContentFactor()); + THIS[ _this.hash ].forceRedraw = true; + }); + + _this.world.addHandler('remove-item', function(event) { + _this.viewport.setHomeBounds(_this.world.getHomeBounds(), _this.world.getContentFactor()); + THIS[ _this.hash ].forceRedraw = true; + }); + _this.drawer = new $.Drawer({ + viewer: _this, + viewport: _this.viewport, + element: _this.canvas, + opacity: _this.opacity, + debugGridColor: _this.debugGridColor + }); + + var tiledImage = new $.TiledImage({ viewer: _this, source: _this.source, viewport: _this.viewport, - element: _this.drawersContainer, + drawer: _this.drawer, + tileCache: _this.tileCache, + imageLoader: _this.imageLoader, x: options.x, y: options.y, width: options.width, height: options.height, - opacity: _this.opacity, - maxImageCacheCount: _this.maxImageCacheCount, imageLoaderLimit: _this.imageLoaderLimit, minZoomImageRatio: _this.minZoomImageRatio, wrapHorizontal: _this.wrapHorizontal, @@ -1986,12 +1927,13 @@ function openTileSource( viewer, source, options ) { 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]; + + _this.world.addItem( tiledImage ); + _this.viewport.goHome( true ); // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons if (!_this.drawer.canRotate()) { @@ -2699,7 +2641,7 @@ function updateOnce( viewer ) { } if ( animated ) { - updateDrawers( viewer ); + updateWorld( viewer ); drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); @@ -2714,8 +2656,8 @@ function updateOnce( viewer ) { * @property {?Object} userData - Arbitrary subscriber-defined object. */ viewer.raiseEvent( "animation" ); - } else if ( THIS[ viewer.hash ].forceRedraw || drawersNeedUpdate( viewer ) ) { - updateDrawers( viewer ); + } else if ( THIS[ viewer.hash ].forceRedraw || viewer.world.needsUpdate() ) { + updateWorld( viewer ); drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); @@ -2770,19 +2712,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 updateWorld( viewer ) { + viewer.drawer.clear(); + viewer.world.update(); -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', {} ); } /////////////////////////////////////////////////////////////////////////////// diff --git a/src/viewport.js b/src/viewport.js index 1fbe6898..9ad8dab3 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -104,44 +104,82 @@ $.Viewport = function( options ) { animationTime: this.animationTime }); - this.resetContentSize( this.contentSize ); + 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 world coordinates + * @param {Number} contentFactor - how many content units per world 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 ); + // TODO: seems like fitWidthBounds and fitHeightBounds should be thin slices + // across the appropriate axis, centered in the image, rather than what we have + // here. + this.fitWidthBounds = new $.Rect(this.homeBounds.x, this.homeBounds.y, + this.homeBounds.width, this.homeBounds.width); + + this.fitHeightBounds = new $.Rect(this.homeBounds.x, this.homeBounds.y, + this.homeBounds.height, this.homeBounds.height); 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; }, /** @@ -154,9 +192,11 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ if( this.defaultZoomLevel ){ return this.defaultZoomLevel; } else { - return ( aspectFactor >= 1 ) ? + var output = ( aspectFactor >= 1 ) ? 1 : aspectFactor; + + return output / this.homeBounds.width; } }, @@ -164,16 +204,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @function */ getHomeBounds: function() { - var center = this.homeBounds.getCenter( ), - width = 1.0 / this.getHomeZoom( ), - height = width / this.getAspectRatio(); - - return new $.Rect( - center.x - ( width / 2.0 ), - center.y - ( height / 2.0 ), - width, - height - ); + return this.homeBounds.clone(); }, /** @@ -216,9 +247,11 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @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.containerSize.x; + zoom /= this.homeBounds.width; + } return Math.max( zoom, this.getHomeZoom() ); }, @@ -347,9 +380,9 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ verticalThreshold = this.visibilityRatio * newBounds.height; left = newBounds.x + newBounds.width; - right = 1 - newBounds.x; + right = this.homeBounds.width - newBounds.x; top = newBounds.y + newBounds.height; - bottom = this.contentAspectY - newBounds.y; + bottom = this.homeBounds.height - newBounds.y; if ( this.wrapHorizontal ) { //do nothing @@ -380,11 +413,11 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ if ( dx || dy || immediately ) { newBounds.x += dx; newBounds.y += dy; - if( newBounds.width > 1 ){ - newBounds.x = 0.5 - newBounds.width/2; + if( newBounds.width > this.homeBounds.width ){ + newBounds.x = this.homeBounds.width / 2 - newBounds.width/2; } - if( newBounds.height > this.contentAspectY ){ - newBounds.y = this.contentAspectY/2 - newBounds.height/2; + if( newBounds.height > this.homeBounds.height){ + newBounds.y = this.homeBounds.height / 2 - newBounds.height/2; } } @@ -413,10 +446,6 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @fires OpenSeadragon.Viewer.event:constrain */ applyConstraints: function( immediately ) { - if (true) { - return; // TEMP - } - var actualZoom = this.getZoom(), constrainedZoom = Math.max( Math.min( actualZoom, this.getMaxZoom() ), @@ -739,7 +768,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. diff --git a/src/world.js b/src/world.js new file mode 100644 index 00000000..a9299edb --- /dev/null +++ b/src/world.js @@ -0,0 +1,276 @@ +/* + * 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( $ ){ + +/** + * Keeps track of all of the tiled images in the scene. + * + * @class World + * @classdesc + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @param {OpenSeadragon.Options} options - World options. + **/ +$.World = function( options ) { + $.console.assert( options.viewer, "[World] options.viewer is required" ); + + $.EventSource.call( this ); + + this.viewer = options.viewer; + this._items = []; + 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 (optional). + * @fires OpenSeadragon.World.event:add-item + */ + 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(); + + /** + * 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.Drawer} 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 ); + }, + + /** + * Get the number of items used. + * @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-changed + */ + 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 ); + + /** + * Raised when the order of the indexes has been changed. + * @event item-index-changed + * @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-changed', { + item: item, + previousIndex: oldIndex, + newIndex: index + } ); + }, + + /** + * Remove an item. + * @function + * @param {OpenSeadragon.TiledImage} item - The item to remove. + * @fires OpenSeadragon.World.event:remove-item + */ + removeItem: function( item ) { + $.console.assert(item, "[World.removeItem] item is required"); + + var index = this._items.indexOf( item ); + if ( index === -1 ) { + return; + } + + this._items.splice( index, 1 ); + this._figureSizes(); + + /** + * Raised when a 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 } ); + }, + + /** + * Clears all tiles and triggers updates for all items. + * @function + */ + resetItems: function() { + for ( var i = 0; i < this._items.length; i++ ) { + this._items[i].reset(); + } + }, + + /** + * Updates (i.e. draws) all items. + * @function + */ + update: function() { + for ( var i = 0; i < this._items.length; i++ ) { + this._items[i].update(); + } + }, + + /** + * @function + * @returns {Boolean} true if any items need updating. + */ + needsUpdate: function() { + for ( var i = 0; i < this._items.length; i++ ) { + if ( this._items[i].needsUpdate() ) { + return true; + } + } + return false; + }, + + /** + * @function + * @returns {OpenSeadragon.Rect} the smallest rectangle that encloses all items, in world 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 world size + * ratio is the highest) and save it as this "content factor". + * @function + * @returns {Number} the number of content units per world unit. + */ + getContentFactor: function() { + return this._contentFactor; + }, + + /** + * @function + * @private + */ + _figureSizes: function() { + if ( !this._items.length ) { + this._homeBounds = new $.Rect(0, 0, 1, 1); + this._contentSize = new $.Point(1, 1); + return; + } + + var bounds = this._items[0].getWorldBounds(); + 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].getWorldBounds(); + this._contentFactor = Math.max(this._contentFactor, this._items[i].getContentSize().x / bounds.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); + } +}); + +}( OpenSeadragon )); diff --git a/test/basic.js b/test/basic.js index 811efd3b..4a7892d9 100644 --- a/test/basic.js +++ b/test/basic.js @@ -133,7 +133,7 @@ }; viewer.addHandler('animation-finish', homeHandler); - viewport.goHome(true); + viewer.viewport.goHome(true); } viewer.addHandler("open", opener); diff --git a/test/controls.js b/test/controls.js index a2d7d998..44c917cd 100644 --- a/test/controls.js +++ b/test/controls.js @@ -5,7 +5,12 @@ module('Controls', { setup: function () { - var example = $('
').appendTo("#qunit-fixture"); + var example = $('
') + .css({ + width: 1000, + height: 1000 + }) + .appendTo("#qunit-fixture"); testLog.reset(); diff --git a/test/demo/collections/main.js b/test/demo/collections/main.js index 3713ba26..03547079 100644 --- a/test/demo/collections/main.js +++ b/test/demo/collections/main.js @@ -1,6 +1,8 @@ +/* globals $, App */ + (function() { - var App = { + window.App = { init: function() { var self = this; @@ -38,6 +40,7 @@ var addLayerHandler = function( event ) { if ( event.options === options ) { self.viewer.removeHandler( "add-layer", addLayerHandler ); + self.viewer.viewport.goHome(); } }; this.viewer.addHandler( "add-layer", addLayerHandler ); 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 @@
- + - + - - + + + diff --git a/test/test.js b/test/test.js index c051d14b..68010ef4 100644 --- a/test/test.js +++ b/test/test.js @@ -134,6 +134,12 @@ } } + testConsole.assert = function(condition, message) { + if (condition) { + testConsole.error(message); + } + }; + OpenSeadragon.console = testConsole; } )();