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/src/drawer.js b/src/drawer.js index a4b7f2ec..1f24fbdb 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -50,7 +50,6 @@ var DEVICE_SCREEN = $.getWindowSize(), /** * @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,6 +57,9 @@ 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 @@ -72,32 +74,10 @@ $.Drawer = function( options ) { }; } - 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 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 @@ -174,7 +154,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 */{ @@ -343,895 +340,121 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed) this.canvas.width = 1; this.canvas.height = 1; + }, + + 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.context.strokeRect( + tile.position.x, + tile.position.y, + tile.size.x, + tile.size.y + ); + 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.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; - drawer.context.strokeRect( - tile.position.x, - tile.position.y, - tile.size.x, - tile.size.y - ); - 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 - ); - drawer.context.restore(); - } -} - - }( OpenSeadragon )); diff --git a/src/openseadragon.js b/src/openseadragon.js index 74c6c66e..4819f55f 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -2224,7 +2224,8 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ debug: nullfunction, info: nullfunction, warn: nullfunction, - error: nullfunction + error: nullfunction, + assert: nullfunction }; diff --git a/src/tilecache.js b/src/tilecache.js new file mode 100644 index 00000000..2e8faadb --- /dev/null +++ b/src/tilecache.js @@ -0,0 +1,113 @@ +/* + * 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( $ ){ + +/** + * @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( tile, cutoff ) { + cutoff = 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; + + for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) { + prevTile = this._tilesLoaded[ i ]; + + 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 ] = tile; + }, + + /** + * Clears all tiles. + * @method + */ + reset: function() { + for ( var i = 0; i < this._tilesLoaded.length; ++i ) { + this._tilesLoaded[i].unload(); + } + + this._tilesLoaded = []; + } +}; + +}( OpenSeadragon )); diff --git a/src/tiledimage.js b/src/tiledimage.js index a4b7f2ec..750f7d2c 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1,5 +1,5 @@ /* - * OpenSeadragon - Drawer + * OpenSeadragon - TiledImage * * Copyright (C) 2009 CodePlex Foundation * Copyright (C) 2010-2013 OpenSeadragon contributors @@ -34,43 +34,22 @@ (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 + * @class TiledImage * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. - * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#drawer}). + * A new instance is created for each TileSource opened. * * @memberof OpenSeadragon - * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. - * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. - * @param {Element} element - Parent element. */ -$.Drawer = function( options ) { +$.TiledImage = function( options ) { + $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); + $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); - //backward compatibility for positional args while prefering more - //idiomatic javascript options object as the only argument - var args = arguments, - i; + this._tileCache = options.tileCache; + delete options.tileCache; - if( !$.isPlainObject( options ) ){ - options = { - source: args[ 0 ], // Reference to Viewer tile source. - viewport: args[ 1 ], // Reference to Viewer viewport. - element: args[ 2 ] // Parent element. - }; - } + this._drawer = options.drawer; + delete options.drawer; this._worldX = options.x || 0; delete options.x; @@ -85,7 +64,7 @@ $.Drawer = function( options ) { delete options.width; if ( options.height ) { - $.console.error( "specifying both width and height to a drawer is not supported" ); + $.console.error( "specifying both width and height to a tiledImage is not supported" ); delete options.height; } } else if ( options.height ) { @@ -104,12 +83,11 @@ $.Drawer = function( options ) { 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? + 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? //internal state / configurable settings @@ -130,219 +108,73 @@ $.Drawer = function( options ) { crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy }, options ); - - this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true ); - /** - * The parent element of this Drawer instance, passed in when the Drawer was created. - * The parent of {@link OpenSeadragon.Drawer#canvas}. - * @member {Element} container - * @memberof OpenSeadragon.Drawer# - */ - this.container = $.getElement( this.element ); - /** - * A <canvas> element if the browser supports them, otherwise a <div> element. - * Child element of {@link OpenSeadragon.Drawer#container}. - * @member {Element} canvas - * @memberof OpenSeadragon.Drawer# - */ - this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" ); - /** - * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a <canvas> element, otherwise null. - * @member {Object} context - * @memberof OpenSeadragon.Drawer# - */ - this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null; - - /** - * @member {Element} element - * @memberof OpenSeadragon.Drawer# - * @deprecated Alias for {@link OpenSeadragon.Drawer#container}. - */ - this.element = this.container; - - // We force our container to ltr because our drawing math doesn't work in rtl. - // This issue only affects our canvas renderer, but we do it always for consistency. - // Note that this means overlays you want to be rtl need to be explicitly set to rtl. - this.container.dir = 'ltr'; - - this.canvas.style.width = "100%"; - this.canvas.style.height = "100%"; - this.canvas.style.position = "absolute"; - $.setElementOpacity( this.canvas, this.opacity, true ); - - // explicit left-align - this.container.style.textAlign = "left"; - this.container.appendChild( this.canvas ); - - //this.profiler = new $.Profiler(); }; -$.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ - +$.TiledImage.prototype = /** @lends OpenSeadragon.TiledImage.prototype */{ /** - * Adds an html element as an overlay to the current viewport. Useful for - * highlighting words or areas of interest on an image or other zoomable - * interface. - * @method - * @param {Element|String|Object} element - A reference to an element or an id for - * the element which will overlayed. Or an Object specifying the configuration for the overlay - * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the - * viewport which the location coordinates will be treated as relative - * to. - * @param {function} onDraw - If supplied the callback is called when the overlay - * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning. - * It is passed position, size and element. - * @fires OpenSeadragon.Viewer.event:add-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#addOverlay} instead. - */ - addOverlay: function( element, location, placement, onDraw ) { - $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead."); - this.viewer.addOverlay( element, location, placement, onDraw ); - return this; - }, - - /** - * Updates the overlay represented by the reference to the element or - * element id moving it to the new location, relative to the new placement. - * @method - * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the - * viewport which the location coordinates will be treated as relative - * to. - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:update-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#updateOverlay} instead. - */ - updateOverlay: function( element, location, placement ) { - $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead."); - this.viewer.updateOverlay( element, location, placement ); - return this; - }, - - /** - * Removes and overlay identified by the reference element or element id - * and schedules and update. - * @method - * @param {Element|String} element - A reference to the element or an - * element id which represent the ovelay content to be removed. - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:remove-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#removeOverlay} instead. - */ - removeOverlay: function( element ) { - $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead."); - this.viewer.removeOverlay( element ); - return this; - }, - - /** - * Removes all currently configured Overlays from this Drawer and schedules - * and update. - * @method - * @return {OpenSeadragon.Drawer} Chainable. - * @fires OpenSeadragon.Viewer.event:clear-overlay - * @deprecated - use {@link OpenSeadragon.Viewer#clearOverlays} instead. - */ - clearOverlays: function() { - $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead."); - this.viewer.clearOverlays(); - return this; - }, - - /** - * Set the opacity of the drawer. + * Set the opacity of the TiledImage. * @method * @param {Number} opacity - * @return {OpenSeadragon.Drawer} Chainable. + * @return {OpenSeadragon.TiledImage} Chainable. */ setOpacity: function( opacity ) { this.opacity = opacity; - $.setElementOpacity( this.canvas, this.opacity, true ); + // TODO: trigger update return this; }, /** - * Get the opacity of the drawer. + * Get the opacity of the TiledImage. * @method * @returns {Number} */ getOpacity: function() { return this.opacity; }, + /** - * Returns whether the Drawer is scheduled for an update at the + * Returns whether the TiledImage is scheduled for an update at the * soonest possible opportunity. * @method - * @returns {Boolean} - Whether the Drawer is scheduled for an update at the + * @returns {Boolean} - Whether the TiledImage is scheduled for an update at the * soonest possible opportunity. */ needsUpdate: function() { return this.updateAgain; }, - /** - * Returns the total number of tiles that have been loaded by this Drawer. - * @method - * @returns {Number} - The total number of tiles that have been loaded by - * this Drawer. - */ - numTilesLoaded: function() { - return this.tilesLoaded.length; - }, - /** * Clears all tiles and triggers an update on the next call to - * Drawer.prototype.update(). + * TiledImage.prototype.update(). * @method - * @return {OpenSeadragon.Drawer} Chainable. + * @return {OpenSeadragon.TiledImage} Chainable. */ reset: function() { - clearTiles( this ); + this._tileCache.clearTilesFor(this); this.lastResetTime = $.now(); this.updateAgain = true; return this; }, /** - * Forces the Drawer to update. + * Forces the TiledImage to update. * @method - * @return {OpenSeadragon.Drawer} Chainable. + * @return {OpenSeadragon.TiledImage} Chainable. */ update: function() { - //this.profiler.beginUpdate(); this.midUpdate = true; updateViewport( this ); this.midUpdate = false; - //this.profiler.endUpdate(); return this; }, /** - * Returns whether rotation is supported or not. - * @method - * @return {Boolean} True if rotation is supported. - */ - canRotate: function() { - return this.useCanvas; - }, - - /** - * Destroy the drawer (unload current loaded tiles) + * Destroy the TiledImage (unload current loaded tiles) * @method * @return null */ destroy: function() { - //unload current loaded tiles (=empty TILE_CACHE) - for ( var i = 0; i < this.tilesLoaded.length; ++i ) { - this.tilesLoaded[i].unload(); - } - - //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed) - this.canvas.width = 1; - this.canvas.height = 1; + this.reset(); } }; @@ -353,11 +185,11 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * 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 ) { +function updateViewport( tiledImage ) { - drawer.updateAgain = false; + tiledImage.updateAgain = false; - if( drawer.viewer ){ + if( tiledImage.viewer ){ /** * - Needs documentation - * @@ -367,7 +199,7 @@ function updateViewport( drawer ) { * @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', {} ); + tiledImage.viewer.raiseEvent( 'update-viewport', {} ); } var tile, @@ -375,29 +207,29 @@ function updateViewport( drawer ) { best = null, haveDrawn = false, currentTime = $.now(), - viewportSize = drawer.viewport.getContainerSize(), - viewportBounds = drawer.viewport.getBounds( true ), + viewportSize = tiledImage.viewport.getContainerSize(), + viewportBounds = tiledImage.viewport.getBounds( true ), viewportTL = viewportBounds.getTopLeft(), viewportBR = viewportBounds.getBottomRight(), - zeroRatioC = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( 0 ), + zeroRatioC = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( 0 ), true - ).x * drawer._scale, + ).x * tiledImage._scale, lowestLevel = Math.max( - drawer.source.minLevel, + tiledImage.source.minLevel, Math.floor( - Math.log( drawer.minZoomImageRatio ) / + Math.log( tiledImage.minZoomImageRatio ) / Math.log( 2 ) ) ), highestLevel = Math.min( - Math.abs(drawer.source.maxLevel), + Math.abs(tiledImage.source.maxLevel), Math.abs(Math.floor( - Math.log( zeroRatioC / drawer.minPixelRatio ) / + Math.log( zeroRatioC / tiledImage.minPixelRatio ) / Math.log( 2 ) )) ), - degrees = drawer.viewport.degrees, + degrees = tiledImage.viewport.degrees, renderPixelRatioC, renderPixelRatioT, zeroRatioT, @@ -405,28 +237,17 @@ function updateViewport( drawer ) { levelOpacity, levelVisibility; - viewportTL.x -= drawer._worldX; - viewportTL.y -= drawer._worldY; - viewportBR.x -= drawer._worldX; - viewportBR.y -= drawer._worldY; + viewportTL.x -= tiledImage._worldX; + viewportTL.y -= tiledImage._worldY; + viewportBR.x -= tiledImage._worldX; + viewportBR.y -= tiledImage._worldY; // Reset tile's internal drawn state - while ( drawer.lastDrawn.length > 0 ) { - tile = drawer.lastDrawn.pop(); + while ( tiledImage.lastDrawn.length > 0 ) { + tile = tiledImage.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 ); @@ -435,23 +256,23 @@ function updateViewport( drawer ) { } //Don't draw if completely outside of the viewport - if ( !drawer.wrapHorizontal && - ( viewportBR.x < 0 || viewportTL.x > drawer._worldWidth ) ) { + if ( !tiledImage.wrapHorizontal && + ( viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidth ) ) { return; } else if - ( !drawer.wrapVertical && - ( viewportBR.y < 0 || viewportTL.y > drawer._worldHeight ) ) { + ( !tiledImage.wrapVertical && + ( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeight ) ) { return; } // Calculate viewport rect / bounds - if ( !drawer.wrapHorizontal ) { + if ( !tiledImage.wrapHorizontal ) { viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, drawer._worldWidth ); + viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidth ); } - if ( !drawer.wrapVertical ) { + if ( !tiledImage.wrapVertical ) { viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, drawer._worldHeight ); + viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeight ); } // Calculations for the interval of levels to draw @@ -465,12 +286,12 @@ function updateViewport( drawer ) { drawLevel = false; //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( level ), + renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( level ), true - ).x * drawer._scale; + ).x * tiledImage._scale; - if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) || + if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) || ( level == lowestLevel ) ) { drawLevel = true; haveDrawn = true; @@ -479,22 +300,22 @@ function updateViewport( drawer ) { } //Perform calculations for draw if we haven't drawn this - renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( level ), + renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( level ), false - ).x * drawer._scale; + ).x * tiledImage._scale; - zeroRatioT = drawer.viewport.deltaPixelsFromPoints( - drawer.source.getPixelRatio( + zeroRatioT = tiledImage.viewport.deltaPixelsFromPoints( + tiledImage.source.getPixelRatio( Math.max( - drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1, + tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, 0 ) ), false - ).x * drawer._scale; + ).x * tiledImage._scale; - optimalRatio = drawer.immediateRender ? + optimalRatio = tiledImage.immediateRender ? 1 : zeroRatioT; @@ -506,7 +327,7 @@ function updateViewport( drawer ) { // Update the level and keep track of 'best' tile to load best = updateLevel( - drawer, + tiledImage, haveDrawn, drawLevel, level, @@ -520,34 +341,34 @@ function updateViewport( drawer ) { // Stop the loop if lower-res tiles would all be covered by // already drawn tiles - if ( providesCoverage( drawer.coverage, level ) ) { + if ( providesCoverage( tiledImage.coverage, level ) ) { break; } } // Perform the actual drawing - drawTiles( drawer, drawer.lastDrawn ); + drawTiles( tiledImage, tiledImage.lastDrawn ); // Load the new 'best' tile if ( best ) { - loadTile( drawer, best, currentTime ); + loadTile( tiledImage, best, currentTime ); // because we haven't finished drawing, so - drawer.updateAgain = true; + tiledImage.updateAgain = true; } } -function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ +function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ var x, y, tileTL, tileBR, numberOfTiles, - viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ); + viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() ); - if( drawer.viewer ){ + if( tiledImage.viewer ){ /** * - Needs documentation - * @@ -565,7 +386,7 @@ function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVi * @property {Object} best * @property {?Object} userData - Arbitrary subscriber-defined object. */ - drawer.viewer.raiseEvent( 'update-level', { + tiledImage.viewer.raiseEvent( 'update-level', { havedrawn: haveDrawn, level: level, opacity: levelOpacity, @@ -578,16 +399,16 @@ function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVi } //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 ); + tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scale )); + tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scale )); + numberOfTiles = tiledImage.source.getNumTiles( level ); - resetCoverage( drawer.coverage, level ); + resetCoverage( tiledImage.coverage, level ); - if ( !drawer.wrapHorizontal ) { + if ( !tiledImage.wrapHorizontal ) { tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); } - if ( !drawer.wrapVertical ) { + if ( !tiledImage.wrapVertical ) { tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); } @@ -595,7 +416,7 @@ function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVi for ( y = tileTL.y; y <= tileBR.y; y++ ) { best = updateTile( - drawer, + tiledImage, drawLevel, haveDrawn, x, y, @@ -614,21 +435,21 @@ function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVi return best; } -function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ +function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ var tile = getTile( x, y, level, - drawer.source, - drawer.tilesMatrix, + tiledImage.source, + tiledImage.tilesMatrix, currentTime, numberOfTiles, - drawer._worldWidth, - drawer._worldHeight + tiledImage._worldWidth, + tiledImage._worldHeight ), drawTile = drawLevel; - if( drawer.viewer ){ + if( tiledImage.viewer ){ /** * - Needs documentation - * @@ -639,20 +460,20 @@ function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, le * @property {OpenSeadragon.Tile} tile * @property {?Object} userData - Arbitrary subscriber-defined object. */ - drawer.viewer.raiseEvent( 'update-tile', { + tiledImage.viewer.raiseEvent( 'update-tile', { tile: tile }); } - setCoverage( drawer.coverage, level, x, y, false ); + setCoverage( tiledImage.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 ); + if ( isCovered( tiledImage.coverage, level, x, y ) ) { + setCoverage( tiledImage.coverage, level, x, y, true ); } else { drawTile = true; } @@ -664,16 +485,16 @@ function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, le positionTile( tile, - drawer.source.tileOverlap, - drawer.viewport, + tiledImage.source.tileOverlap, + tiledImage.viewport, viewportCenter, levelVisibility, - drawer + tiledImage ); if ( tile.loaded ) { var needsUpdate = blendTile( - drawer, + tiledImage, tile, x, y, level, @@ -682,7 +503,7 @@ function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, le ); if ( needsUpdate ) { - drawer.updateAgain = true; + tiledImage.updateAgain = true; } } else if ( tile.loading ) { // the tile is already in the download queue @@ -735,46 +556,30 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid 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 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( drawer, tile, time, image ) { - var insertionIndex, - cutoff, - worstTile, - worstTime, - worstLevel, - worstTileIndex, - prevTile, - prevTime, - prevLevel, - i; - +function onTileLoad( tiledImage, tile, time, image ) { tile.loading = false; - if ( drawer.midUpdate ) { + if ( tiledImage.midUpdate ) { $.console.warn( "Tile load callback in middle of drawing routine." ); return; - } else if ( !image && !drawer.viewport.collectionMode ) { + } else if ( !image ) { $.console.log( "Tile %s failed to load: %s", tile, tile.url ); - if( !drawer.debugMode ){ + if( !tiledImage.debugMode ){ tile.exists = false; return; } - } else if ( time < drawer.lastResetTime ) { + } else if ( time < tiledImage.lastResetTime ) { $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); return; } @@ -782,60 +587,29 @@ function onTileLoad( drawer, tile, time, image ) { tile.loaded = true; tile.image = image; - insertionIndex = drawer.tilesLoaded.length; + var cutoff = Math.ceil( Math.log( tiledImage.source.getTileSize(tile.level) ) / Math.log( 2 ) ); + tiledImage._tileCache.cacheTile({ + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); - 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; + tiledImage.updateAgain = true; } -function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, drawer ){ +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ var boundsTL = tile.bounds.getTopLeft(); - boundsTL.x *= drawer._scale; - boundsTL.y *= drawer._scale; - boundsTL.x += drawer._worldX; - boundsTL.y += drawer._worldY; + boundsTL.x *= tiledImage._scale; + boundsTL.y *= tiledImage._scale; + boundsTL.x += tiledImage._worldX; + boundsTL.y += tiledImage._worldY; var boundsSize = tile.bounds.getSize(); - boundsSize.x *= drawer._scale; - boundsSize.y *= drawer._scale; + boundsSize.x *= tiledImage._scale; + boundsSize.y *= tiledImage._scale; var positionC = viewport.pixelFromPoint( boundsTL, true ), positionT = viewport.pixelFromPoint( boundsTL, false ), @@ -855,8 +629,8 @@ function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, } -function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * drawer.blendTime, +function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, deltaTime, opacity; @@ -867,16 +641,16 @@ function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ deltaTime = currentTime - tile.blendStart; opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; - if ( drawer.alwaysBlend ) { + if ( tiledImage.alwaysBlend ) { opacity *= levelOpacity; } tile.opacity = opacity; - drawer.lastDrawn.push( tile ); + tiledImage.lastDrawn.push( tile ); if ( opacity == 1 ) { - setCoverage( drawer.coverage, level, x, y, true ); + setCoverage( tiledImage.coverage, level, x, y, true ); } else if ( deltaTime < blendTimeMillis ) { return true; } @@ -884,12 +658,6 @@ function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ return false; } - -function clearTiles( drawer ) { - drawer.tilesMatrix = {}; - drawer.tilesLoaded = []; -} - /** * @private * @inner @@ -1007,7 +775,7 @@ function compareTiles( previousBest, tile ) { return previousBest; } -function drawTiles( drawer, lastDrawn ){ +function drawTiles( tiledImage, lastDrawn ){ var i, tile, tileKey, @@ -1019,11 +787,11 @@ function drawTiles( drawer, lastDrawn ){ // We need a callback to give image manipulation a chance to happen var drawingHandler = function(args) { - if (drawer.viewer) { + 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 drawer is using a . + * NOTE: This event is only fired when the tiledImage is using a . * * @event tile-drawing * @memberof OpenSeadragon.Viewer @@ -1032,103 +800,24 @@ function drawTiles( drawer, lastDrawn ){ * @property {OpenSeadragon.Tile} tile * @property {?Object} userData - 'context', 'tile' and 'rendered'. */ - drawer.viewer.raiseEvent('tile-drawing', args); + tiledImage.viewer.raiseEvent('tile-drawing', args); } }; for ( i = lastDrawn.length - 1; i >= 0; i-- ) { tile = lastDrawn[ i ]; + tiledImage._drawer.drawTile( tile ); + tile.beingDrawn = true; - //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 ){ + if( tiledImage.debugMode ){ try{ - drawDebugInfo( drawer, tile, lastDrawn.length, i ); + tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); }catch(e){ $.console.error(e); } } - if( drawer.viewer ){ + if( tiledImage.viewer ){ /** * - Needs documentation - * @@ -1139,99 +828,11 @@ function drawTiles( drawer, lastDrawn ){ * @property {OpenSeadragon.Tile} tile * @property {?Object} userData - Arbitrary subscriber-defined object. */ - drawer.viewer.raiseEvent( 'tile-drawn', { + tiledImage.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; - drawer.context.strokeRect( - tile.position.x, - tile.position.y, - tile.size.x, - tile.size.y - ); - 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 - ); - drawer.context.restore(); - } -} - - }( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index 667ef4fb..9e4c4a1e 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -151,7 +151,7 @@ $.Viewer = function( options ) { * @memberof OpenSeadragon.Viewer# */ drawer: null, - drawers: [], + world: null, // Container inside the canvas where drawers (layers) are drawn. drawersContainer: null, /** @@ -554,7 +554,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; @@ -1102,11 +1102,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return; } - var drawer = new $.Drawer({ + var tiledImage = new $.TiledImage({ viewer: _this, source: tileSource, viewport: _this.viewport, - element: _this.drawersContainer, x: options.x, y: options.y, width: options.width, @@ -1126,7 +1125,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, debugMode: _this.debugMode, debugGridColor: _this.debugGridColor }); - _this.drawers.push( drawer ); + _this.world.add( tiledImage ); if ( options.level !== undefined ) { _this.setLayerLevel( drawer, options.level ); } @@ -1971,17 +1970,16 @@ function openTileSource( viewer, source, options ) { _this.source.overlays = _this.source.overlays || []; + _this.tileCache = new $.TileCache({ + maxImageCacheCount: _this.maxImageCacheCount + }); + _this.drawer = new $.Drawer({ viewer: _this, source: _this.source, viewport: _this.viewport, element: _this.drawersContainer, - 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, @@ -1995,6 +1993,32 @@ function openTileSource( viewer, source, options ) { debugGridColor: _this.debugGridColor, crossOriginPolicy: _this.crossOriginPolicy }); + + var tiledImage = new $.TiledImage({ + viewer: _this, + source: _this.source, + viewport: _this.viewport, + drawer: _this.drawer, + tileCache: _this.tileCache, + x: options.x, + y: options.y, + width: options.width, + height: options.height, + opacity: _this.opacity, + imageLoaderLimit: _this.imageLoaderLimit, + minZoomImageRatio: _this.minZoomImageRatio, + wrapHorizontal: _this.wrapHorizontal, + wrapVertical: _this.wrapVertical, + immediateRender: _this.immediateRender, + blendTime: _this.blendTime, + alwaysBlend: _this.alwaysBlend, + minPixelRatio: _this.collectionMode ? 0 : _this.minPixelRatio, + timeout: _this.timeout, + debugMode: _this.debugMode, + debugGridColor: _this.debugGridColor, + crossOriginPolicy: _this.crossOriginPolicy + }); + _this.drawers = [_this.drawer]; // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons @@ -2711,7 +2735,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 ); @@ -2726,8 +2750,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 ); @@ -2782,19 +2806,9 @@ 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 drawersNeedUpdate( viewer ) { - for (var i = 0; i < viewer.drawers.length; i++ ) { - if (viewer.drawers[i].needsUpdate()) { - return true; - } - } - return false; +function updateWorld( viewer ) { + viewer.drawer.clear(); + viewer.world.update(); } /////////////////////////////////////////////////////////////////////////////// diff --git a/src/world.js b/src/world.js new file mode 100644 index 00000000..31e8520c --- /dev/null +++ b/src/world.js @@ -0,0 +1,164 @@ +/* + * OpenSeadragon - World + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +/** + * @class World + * @classdesc + */ +$.World = function( options ) { + $.console.assert( options.viewer, "[World] options.viewer is required" ); + + this.viewer = options.viewer; + this._items = []; +}; + +$.World.prototype = /** @lends OpenSeadragon.World.prototype */{ + addItem: function( item ) { + this._items.push( item ); + }, + + /** + * Get the item at the specified level. + * @param {Number} level The item to retrieve level. + * @returns {OpenSeadragon.TiledImage} The item at the specified level. + */ + getItemAt: function( level ) { + if ( level >= this._items.length ) { + throw new Error( "Level bigger than number of items." ); + } + return this._items[ level ]; + }, + + /** + * Get the level of the given item or -1 if not present. + * @param {OpenSeadragon.TiledImage} item The item. + * @returns {Number} The level of the item or -1 if not present. + */ + getLevelOfItem: function( item ) { + 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 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 + * @fires OpenSeadragon.Viewer.event:layer-level-changed + */ + setItemLevel: function( item, level ) { + var oldLevel = this.getLevelOfItem( item ); + + if ( level >= this._items.length ) { + throw new Error( "Level bigger than number of layers." ); + } + if ( level === oldLevel || oldLevel === -1 ) { + return; + } + this._items.splice( oldLevel, 1 ); + this._items.splice( level, 0, item ); + + /** + * 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. + */ + // TODO: deprecate + this.viewer.raiseEvent( 'layer-level-changed', { + drawer: item, + previousLevel: oldLevel, + newLevel: 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 + * @fires OpenSeadragon.Viewer.event:remove-layer + */ + removeItem: function( item ) { + var index = this._items.indexOf( item ); + if ( index === -1 ) { + return; + } + + this._items.splice( index, 1 ); + /** + * 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. + */ + // TODO: deprecate + this.raiseEvent( 'remove-layer', { drawer: item } ); + }, + + update: function() { + for (var i = 0; i < this._items.length; i++ ) { + this._items[i].update(); + } + }, + + needsUpdate: function() { + for (var i = 0; i < this._items.length; i++ ) { + if (this._items[i].needsUpdate()) { + return true; + } + } + return false; + } +}; + +}( OpenSeadragon ));