diff --git a/src/tiledimage.js b/src/tiledimage.js index 80204328..368a8c41 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1,36 +1,36 @@ /* - * 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. - */ +* 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( $ ){ @@ -72,7 +72,8 @@ * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. * @param {Number} [options.opacity=1] - Set to draw at proportional opacity. If zero, images will not draw. * @param {Boolean} [options.preload=false] - Set true to load even when the image is hidden by zero opacity. - * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. + * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible + values. * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. @@ -218,21 +219,21 @@ $.TiledImage = function( options ) { // We need a callback to give image manipulation a chance to happen this._drawingHandler = function(args) { - /** - * This event is fired just before the tile is drawn giving the application a chance to alter the image. - * - * NOTE: This event is only fired when the drawer is using a <canvas>. - * - * @event tile-drawing - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - The Tile being drawn. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. - * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ + /** + * This 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 <canvas>. + * + * @event tile-drawing + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Tile} tile - The Tile being drawn. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. + * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ _this.viewer.raiseEvent('tile-drawing', $.extend({ tiledImage: _this }, args)); @@ -423,7 +424,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * @returns {OpenSeadragon.Point} The TiledImage's content size, in window coordinates. */ - getSizeInWindowCoordinates: function() { + getSizeInWindowCoordinates: function() { var topLeft = this.imageToWindowCoordinates(new $.Point(0, 0)); var bottomRight = this.imageToWindowCoordinates(this.getContentSize()); return new $.Point(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); @@ -592,7 +593,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag */ windowToImageCoordinates: function( pixel ) { var viewerCoordinates = pixel.minus( - OpenSeadragon.getElementPosition( this.viewer.element )); + OpenSeadragon.getElementPosition( this.viewer.element )); return this.viewerElementToImageCoordinates( viewerCoordinates ); }, @@ -604,7 +605,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag imageToWindowCoordinates: function( pixel ) { var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); return viewerCoordinates.plus( - OpenSeadragon.getElementPosition( this.viewer.element )); + OpenSeadragon.getElementPosition( this.viewer.element )); }, // private @@ -633,7 +634,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag */ viewportToImageZoom: function( viewportZoom ) { var ratio = this._scaleSpring.current.value * - this.viewport._containerInnerSize.x / this.source.dimensions.x; + this.viewport._containerInnerSize.x / this.source.dimensions.x; return ratio * viewportZoom; }, @@ -650,7 +651,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag */ imageToViewportZoom: function( imageZoom ) { var ratio = this._scaleSpring.current.value * - this.viewport._containerInnerSize.x / this.source.dimensions.x; + this.viewport._containerInnerSize.x / this.source.dimensions.x; return imageZoom / ratio; }, @@ -666,7 +667,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag if (immediately) { if (sameTarget && this._xSpring.current.value === position.x && - this._ySpring.current.value === position.y) { + this._ySpring.current.value === position.y) { return; } @@ -1162,8 +1163,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag ); // Update the level and keep track of 'best' tile to load - bestTile = updateLevel( - this, + bestTile = this._updateLevel( haveDrawn, drawLevel, level, @@ -1176,17 +1176,17 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag // Stop the loop if lower-res tiles would all be covered by // already drawn tiles - if (providesCoverage(this.coverage, level)) { + if ($.TileSource._providesCoverage(this.coverage, level)) { break; } } // Perform the actual drawing - drawTiles(this, this.lastDrawn); + this._drawTiles(this.lastDrawn); // Load the new 'best' tile if (bestTile && !bestTile.context2D) { - loadTile(this, bestTile, currentTime); + this._loadTile(bestTile, currentTime); this._needsDraw = true; this._setFullyLoaded(false); } else { @@ -1233,586 +1233,855 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag topLeft: topLeftTile, bottomRight: bottomRightTile, }; - } -}); - -/** - * @private - * @inner - * Updates all tiles at a given resolution level. - * @param {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @param {Boolean} haveDrawn - * @param {Boolean} drawLevel - * @param {Number} level - * @param {Number} levelOpacity - * @param {Number} levelVisibility - * @param {OpenSeadragon.Point} viewportTL - The index of the most top-left visible tile. - * @param {OpenSeadragon.Point} viewportBR - The index of the most bottom-right visible tile. - * @param {Number} currentTime - * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. - */ -function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, - levelVisibility, drawArea, currentTime, best) { - - var topLeftBound = drawArea.getBoundingBox().getTopLeft(); - var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); - - if (tiledImage.viewer) { - /** - * - Needs documentation - - * - * @event update-level - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {Object} havedrawn - * @property {Object} level - * @property {Object} opacity - * @property {Object} visibility - * @property {OpenSeadragon.Rect} drawArea - * @property {Object} topleft deprecated, use drawArea instead - * @property {Object} bottomright deprecated, use drawArea instead - * @property {Object} currenttime - * @property {Object} best - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent('update-level', { - tiledImage: tiledImage, - havedrawn: haveDrawn, - level: level, - opacity: levelOpacity, - visibility: levelVisibility, - drawArea: drawArea, - topleft: topLeftBound, - bottomright: bottomRightBound, - currenttime: currentTime, - best: best - }); - } - - resetCoverage(tiledImage.coverage, level); - resetCoverage(tiledImage.loadingCoverage, level); - - //OK, a new drawing so do your calculations - var cornerTiles = tiledImage._getCornerTiles(level, topLeftBound, bottomRightBound); - var topLeftTile = cornerTiles.topLeft; - var bottomRightTile = cornerTiles.bottomRight; - var numberOfTiles = tiledImage.source.getNumTiles(level); - - var viewportCenter = tiledImage.viewport.pixelFromPoint( - tiledImage.viewport.getCenter()); - - if (tiledImage.getFlip()) { - // The right-most tile can be narrower than the others. When flipped, - // this tile is now on the left. Because it is narrower than the normal - // left-most tile, the subsequent tiles may not be wide enough to completely - // fill the viewport. Fix this by rendering an extra column of tiles. If we - // are not wrapping, make sure we never render more than the number of tiles - // in the image. - bottomRightTile.x += 1; - if (!tiledImage.wrapHorizontal) { - bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); - } - } - - for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { - for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { - - var flippedX; - if (tiledImage.getFlip()) { - var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x; - flippedX = x + numberOfTiles.x - xMod - xMod - 1; - } else { - flippedX = x; - } - - if (drawArea.intersection(tiledImage.getTileBounds(level, flippedX, y)) === null) { - // This tile is outside of the viewport, no need to draw it - continue; - } - - best = updateTile( - tiledImage, - drawLevel, - haveDrawn, - flippedX, y, - level, - levelOpacity, - levelVisibility, - viewportCenter, - numberOfTiles, - currentTime, - best - ); - - } - } - - return best; -} - -/** - * @private - * @inner - * Update a single tile at a particular resolution level. - * @param {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @param {Boolean} haveDrawn - * @param {Boolean} drawLevel - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {Number} levelOpacity - * @param {Number} levelVisibility - * @param {OpenSeadragon.Point} viewportCenter - * @param {Number} numberOfTiles - * @param {Number} currentTime - * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. - */ -function updateTile( tiledImage, haveDrawn, drawLevel, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ - - var tile = getTile( - x, y, - level, - tiledImage, - tiledImage.source, - tiledImage.tilesMatrix, - currentTime, - numberOfTiles, - tiledImage._worldWidthCurrent, - tiledImage._worldHeightCurrent - ), - drawTile = drawLevel; - - if( tiledImage.viewer ){ - /** - * - Needs documentation - - * - * @event update-tile - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent( 'update-tile', { - tiledImage: tiledImage, - tile: tile - }); - } - - setCoverage( tiledImage.coverage, level, x, y, false ); - - var loadingCoverage = tile.loaded || tile.loading || isCovered(tiledImage.loadingCoverage, level, x, y); - setCoverage(tiledImage.loadingCoverage, level, x, y, loadingCoverage); - - 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) { - if (tile.context2D) { - setTileLoaded(tiledImage, tile); - } else { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.cacheKey); - if (imageRecord) { - var image = imageRecord.getImage(); - setTileLoaded(tiledImage, tile, image); - } - } - } - - if ( tile.loaded ) { - var needsDraw = blendTile( - tiledImage, - tile, - x, y, - level, - levelOpacity, - currentTime - ); - - if ( needsDraw ) { - tiledImage._needsDraw = true; - } - } else if ( tile.loading ) { - // the tile is already in the download queue - tiledImage._tilesLoading++; - } else if (!loadingCoverage) { - best = compareTiles( best, tile ); - } - - return best; -} - -/** - * @private - * @inner - * Obtains a tile at the given location. - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.TileSource} tileSource - * @param {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile. - * @param {Number} time - * @param {Number} numTiles - * @param {Number} worldWidth - * @param {Number} worldHeight - * @returns {OpenSeadragon.Tile} - */ -function getTile( - x, y, - level, - tiledImage, - tileSource, - tilesMatrix, - time, - numTiles, - worldWidth, - worldHeight -) { - var xMod, - yMod, - bounds, - sourceBounds, - exists, - url, - post, - ajaxHeaders, - context2D, - tile; - - if ( !tilesMatrix[ level ] ) { - tilesMatrix[ level ] = {}; - } - if ( !tilesMatrix[ level ][ x ] ) { - tilesMatrix[ level ][ x ] = {}; - } - - if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ x ][ y ].flipped !== !tiledImage.flipped ) { - xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; - yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - bounds = tiledImage.getTileBounds( level, x, y ); - sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true ); - exists = tileSource.tileExists( level, xMod, yMod ); - url = tileSource.getTileUrl( level, xMod, yMod ); - post = tileSource.getTilePostData( level, xMod, yMod ); - - // Headers are only applicable if loadTilesWithAjax is set - if (tiledImage.loadTilesWithAjax) { - ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod ); - // Combine tile AJAX headers with tiled image AJAX headers (if applicable) - if ($.isPlainObject(tiledImage.ajaxHeaders)) { - ajaxHeaders = $.extend({}, tiledImage.ajaxHeaders, ajaxHeaders); - } - } else { - ajaxHeaders = null; - } - - context2D = tileSource.getContext2D ? - tileSource.getContext2D(level, xMod, yMod) : undefined; - - tile = new $.Tile( - level, - x, - y, - bounds, - exists, - url, - context2D, - tiledImage.loadTilesWithAjax, - ajaxHeaders, - sourceBounds, - post - ); - - if (tiledImage.getFlip()) { - if (xMod === 0) { - tile.isRightMost = true; - } - } else { - if (xMod === numTiles.x - 1) { - tile.isRightMost = true; - } - } - - if (yMod === numTiles.y - 1) { - tile.isBottomMost = true; - } - - tile.flipped = tiledImage.flipped; - - tilesMatrix[ level ][ x ][ y ] = tile; - } - - tile = tilesMatrix[ level ][ x ][ y ]; - tile.lastTouchTime = time; - - return tile; -} - -/** - * @private - * @inner - * Dispatch a job to the ImageLoader to load the Image for a Tile. - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile} tile - * @param {Number} time - */ -function loadTile( tiledImage, tile, time ) { - tile.loading = true; - tiledImage._imageLoader.addJob({ - src: tile.url, - postData: tile.postData, - loadWithAjax: tile.loadWithAjax, - ajaxHeaders: tile.ajaxHeaders, - crossOriginPolicy: tiledImage.crossOriginPolicy, - ajaxWithCredentials: tiledImage.ajaxWithCredentials, - callback: function( image, errorMsg, tileRequest ){ - onTileLoad( tiledImage, tile, time, image, errorMsg, tileRequest ); - }, - abort: function() { - tile.loading = false; - } - }); -} - -/** - * @private - * @inner - * Callback fired when a Tile's Image finished downloading. - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile} tile - * @param {Number} time - * @param {Image} image - * @param {String} errorMsg - * @param {XMLHttpRequest} tileRequest - */ -function onTileLoad( tiledImage, tile, time, image, errorMsg, tileRequest ) { - if ( !image ) { - $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); - /** - * Triggered when a tile fails to load. - * - * @event tile-load-failed - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Tile} tile - The tile that failed to load. - * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. - * @property {number} time - The time in milliseconds when the tile load began. - * @property {string} message - The error message. - * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available. - */ - tiledImage.viewer.raiseEvent("tile-load-failed", { - tile: tile, - tiledImage: tiledImage, - time: time, - message: errorMsg, - tileRequest: tileRequest - }); - tile.loading = false; - tile.exists = false; - return; - } - - if ( time < tiledImage.lastResetTime ) { - $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); - tile.loading = false; - return; - } - - var finish = function() { - var cutoff = tiledImage.source.getClosestLevel(); - setTileLoaded(tiledImage, tile, image, cutoff, tileRequest); - }; - - // Check if we're mid-update; this can happen on IE8 because image load events for - // cached images happen immediately there - if ( !tiledImage._midDraw ) { - finish(); - } else { - // Wait until after the update, in case caching unloads any tiles - window.setTimeout( finish, 1); - } -} - -/** - * @private - * @inner - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile} tile - * @param {Image} image - * @param {Number} cutoff - */ -function setTileLoaded(tiledImage, tile, image, cutoff, tileRequest) { - var increment = 0; - - function getCompletionCallback() { - increment++; - return completionCallback; - } - - function completionCallback() { - increment--; - if (increment === 0) { - tile.loading = false; - tile.loaded = true; - if (!tile.context2D) { - tiledImage._tileCache.cacheTile({ - image: image, - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); - } - tiledImage._needsDraw = true; - } - } + }, /** - * Triggered when a tile has just been loaded in memory. That means that the - * image has been downloaded and can be modified before being drawn to the canvas. - * - * @event tile-loaded - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {Image} image - The image of the tile. - * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. - * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. - * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). - * @property {function} getCompletionCallback - A function giving a callback to call - * when the asynchronous processing of the image is done. The image will be - * marked as entirely loaded when the callback has been called once for each - * call to getCompletionCallback. + * @private + * Updates all tiles at a given resolution level. + * @param {Boolean} haveDrawn + * @param {Boolean} drawLevel + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} levelVisibility + * @param {OpenSeadragon.Rect} drawArea + * @param {Number} currentTime + * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. */ - tiledImage.viewer.raiseEvent("tile-loaded", { - tile: tile, - tiledImage: tiledImage, - tileRequest: tileRequest, - image: image, - getCompletionCallback: getCompletionCallback - }); - // In case the completion callback is never called, we at least force it once. - getCompletionCallback()(); -} + _updateLevel: function(haveDrawn, drawLevel, level, levelOpacity, + levelVisibility, drawArea, currentTime, best) { -/** - * @private - * @inner - * @param {OpenSeadragon.Tile} tile - * @param {Boolean} overlap - * @param {OpenSeadragon.Viewport} viewport - * @param {OpenSeadragon.Point} viewportCenter - * @param {Number} levelVisibility - * @param {OpenSeadragon.TiledImage} tiledImage - */ -function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ - var boundsTL = tile.bounds.getTopLeft(); + var topLeftBound = drawArea.getBoundingBox().getTopLeft(); + var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); - boundsTL.x *= tiledImage._scaleSpring.current.value; - boundsTL.y *= tiledImage._scaleSpring.current.value; - boundsTL.x += tiledImage._xSpring.current.value; - boundsTL.y += tiledImage._ySpring.current.value; + if (this.viewer) { + /** + * - Needs documentation - + * + * @event update-level + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {Object} havedrawn + * @property {Object} level + * @property {Object} opacity + * @property {Object} visibility + * @property {OpenSeadragon.Rect} drawArea + * @property {Object} topleft deprecated, use drawArea instead + * @property {Object} bottomright deprecated, use drawArea instead + * @property {Object} currenttime + * @property {Object} best + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('update-level', { + tiledImage: this, + havedrawn: haveDrawn, + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + drawArea: drawArea, + topleft: topLeftBound, + bottomright: bottomRightBound, + currenttime: currentTime, + best: best + }); + } - var boundsSize = tile.bounds.getSize(); + $.TileSource._resetCoverage(this.coverage, level); + $.TileSource._resetCoverage(this.loadingCoverage, level); - boundsSize.x *= tiledImage._scaleSpring.current.value; - boundsSize.y *= tiledImage._scaleSpring.current.value; + //OK, a new drawing so do your calculations + var cornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound); + var topLeftTile = cornerTiles.topLeft; + var bottomRightTile = cornerTiles.bottomRight; + var numberOfTiles = this.source.getNumTiles(level); - var positionC = viewport.pixelFromPointNoRotate(boundsTL, true), - positionT = viewport.pixelFromPointNoRotate(boundsTL, false), - sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true), - sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false), - tileCenter = positionT.plus( sizeT.divide( 2 ) ), - tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter ); + var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter()); - if ( !overlap ) { - sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + if (this.getFlip()) { + // The right-most tile can be narrower than the others. When flipped, + // this tile is now on the left. Because it is narrower than the normal + // left-most tile, the subsequent tiles may not be wide enough to completely + // fill the viewport. Fix this by rendering an extra column of tiles. If we + // are not wrapping, make sure we never render more than the number of tiles + // in the image. + bottomRightTile.x += 1; + if (!this.wrapHorizontal) { + bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); + } + } + + for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { + for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { + + var flippedX; + if (this.getFlip()) { + var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x; + flippedX = x + numberOfTiles.x - xMod - xMod - 1; + } else { + flippedX = x; + } + + if (drawArea.intersection(this.getTileBounds(level, flippedX, y)) === null) { + // This tile is outside of the viewport, no need to draw it + continue; + } + + best = this._updateTile( + drawLevel, + haveDrawn, + flippedX, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + } + } + + return best; + }, + + /** + * @private + * @inner + * Update a single tile at a particular resolution level. + * @param {Boolean} haveDrawn + * @param {Boolean} drawLevel + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} levelVisibility + * @param {OpenSeadragon.Point} viewportCenter + * @param {Number} numberOfTiles + * @param {Number} currentTime + * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. + */ + _updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity, + levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = this._getTile( + x, y, + level, + currentTime, + numberOfTiles, + this._worldWidthCurrent, + this._worldHeightCurrent + ), + drawTile = drawLevel; + + if( this.viewer ){ + /** + * - Needs documentation - + * + * @event update-tile + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'update-tile', { + tiledImage: this, + tile: tile + }); + } + + $.TileSource._setCoverage( this.coverage, level, x, y, false ); + + var loadingCoverage = tile.loaded || tile.loading || $.TileSource._isCovered(this.loadingCoverage, level, x, y); + $.TileSource._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( $.TileSource._isCovered( this.coverage, level, x, y ) ) { + $.TileSource._setCoverage( this.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + this._positionTile( + tile, + this.source.tileOverlap, + this.viewport, + viewportCenter, + levelVisibility + ); + + if (!tile.loaded) { + if (tile.context2D) { + this._setTileLoaded(tile); + } else { + var imageRecord = this._tileCache.getImageRecord(tile.cacheKey); + if (imageRecord) { + var image = imageRecord.getImage(); + this._setTileLoaded(tile, image); + } + } + } + + if ( tile.loaded ) { + var needsDraw = this._blendTile( + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + if ( needsDraw ) { + this._needsDraw = true; + } + } else if ( tile.loading ) { + // the tile is already in the download queue + this._tilesLoading++; + } else if (!loadingCoverage) { + best = this._compareTiles( best, tile ); + } + + return best; + }, + + /** + * @private + * @inner + * Obtains a tile at the given location. + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} time + * @param {Number} numTiles + * @param {Number} worldWidth + * @param {Number} worldHeight + * @returns {OpenSeadragon.Tile} + */ + _getTile: function( + x, y, + level, + time, + numTiles, + worldWidth, + worldHeight + ) { + var xMod, + yMod, + bounds, + sourceBounds, + exists, + url, + post, + ajaxHeaders, + context2D, + tile, + tilesMatrix = this.tilesMatrix, + tileSource = this.source; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ x ][ y ].flipped !== !this.flipped ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = this.getTileBounds( level, x, y ); + sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true ); + exists = tileSource.tileExists( level, xMod, yMod ); + url = tileSource.getTileUrl( level, xMod, yMod ); + post = tileSource.getTilePostData( level, xMod, yMod ); + + // Headers are only applicable if loadTilesWithAjax is set + if (this.loadTilesWithAjax) { + ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod ); + // Combine tile AJAX headers with tiled image AJAX headers (if applicable) + if ($.isPlainObject(this.ajaxHeaders)) { + ajaxHeaders = $.extend({}, this.ajaxHeaders, ajaxHeaders); + } + } else { + ajaxHeaders = null; + } + + context2D = tileSource.getContext2D ? + tileSource.getContext2D(level, xMod, yMod) : undefined; + + tile = new $.Tile( + level, + x, + y, + bounds, + exists, + url, + context2D, + this.loadTilesWithAjax, + ajaxHeaders, + sourceBounds, + post + ); + + if (this.getFlip()) { + if (xMod === 0) { + tile.isRightMost = true; + } + } else { + if (xMod === numTiles.x - 1) { + tile.isRightMost = true; + } + } + + if (yMod === numTiles.y - 1) { + tile.isBottomMost = true; + } + + tile.flipped = this.flipped; + + tilesMatrix[ level ][ x ][ y ] = tile; + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; + }, + + /** + * @private + * @inner + * Dispatch a job to the ImageLoader to load the Image for a Tile. + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + */ + _loadTile: function(tile, time ) { + var _this = this; + tile.loading = true; + this._imageLoader.addJob({ + src: tile.url, + postData: tile.postData, + loadWithAjax: tile.loadWithAjax, + ajaxHeaders: tile.ajaxHeaders, + crossOriginPolicy: this.crossOriginPolicy, + ajaxWithCredentials: this.ajaxWithCredentials, + callback: function( image, errorMsg, tileRequest ){ + _this._onTileLoad( tile, time, image, errorMsg, tileRequest ); + }, + abort: function() { + tile.loading = false; + } + }); + }, + + /** + * @private + * @inner + * Callback fired when a Tile's Image finished downloading. + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + * @param {Image} image + * @param {String} errorMsg + * @param {XMLHttpRequest} tileRequest + */ + _onTileLoad: function( tile, time, image, errorMsg, tileRequest ) { + if ( !image ) { + $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); + /** + * Triggered when a tile fails to load. + * + * @event tile-load-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Tile} tile - The tile that failed to load. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. + * @property {number} time - The time in milliseconds when the tile load began. + * @property {string} message - The error message. + * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available. + */ + this.viewer.raiseEvent("tile-load-failed", { + tile: tile, + tiledImage: this, + time: time, + message: errorMsg, + tileRequest: tileRequest + }); + tile.loading = false; + tile.exists = false; + return; + } + + if ( time < this.lastResetTime ) { + $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + tile.loading = false; + return; + } + + var _this = this, + finish = function() { + var ccc = _this.source; + var cutoff = ccc.getClosestLevel(); + _this._setTileLoaded(tile, image, cutoff, tileRequest); + }; + + // Check if we're mid-update; this can happen on IE8 because image load events for + // cached images happen immediately there + if ( !this._midDraw ) { + finish(); + } else { + // Wait until after the update, in case caching unloads any tiles + window.setTimeout( finish, 1); + } + }, + + /** + * @private + * @inner + * @param {OpenSeadragon.Tile} tile + * @param {Image || undefined} image + * @param {Number || undefined} cutoff + * @param {XMLHttpRequest || undefined} tileRequest + */ + _setTileLoaded: function(tile, image, cutoff, tileRequest) { + var increment = 0, + _this = this; + + function getCompletionCallback() { + increment++; + return completionCallback; + } + + function completionCallback() { + increment--; + if (increment === 0) { + tile.loading = false; + tile.loaded = true; + if (!tile.context2D) { + _this._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: _this + }); + } + _this._needsDraw = true; + } + } + + /** + * Triggered when a tile has just been loaded in memory. That means that the + * image has been downloaded and can be modified before being drawn to the canvas. + * + * @event tile-loaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {Image} image - The image of the tile. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. + * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. + * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). + * @property {function} getCompletionCallback - A function giving a callback to call + * when the asynchronous processing of the image is done. The image will be + * marked as entirely loaded when the callback has been called once for each + * call to getCompletionCallback. + */ + this.viewer.raiseEvent("tile-loaded", { + tile: tile, + tiledImage: this, + tileRequest: tileRequest, + image: image, + getCompletionCallback: getCompletionCallback + }); + // In case the completion callback is never called, we at least force it once. + getCompletionCallback()(); + }, + + /** + * @private + * @inner + * @param {OpenSeadragon.Tile} tile + * @param {Boolean} overlap + * @param {OpenSeadragon.Viewport} viewport + * @param {OpenSeadragon.Point} viewportCenter + * @param {Number} levelVisibility + */ + _positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){ + var boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= this._scaleSpring.current.value; + boundsTL.y *= this._scaleSpring.current.value; + boundsTL.x += this._xSpring.current.value; + boundsTL.y += this._ySpring.current.value; + + var boundsSize = tile.bounds.getSize(); + + boundsSize.x *= this._scaleSpring.current.value; + boundsSize.y *= this._scaleSpring.current.value; + + var positionC = viewport.pixelFromPointNoRotate(boundsTL, true), + positionT = viewport.pixelFromPointNoRotate(boundsTL, false), + sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true), + sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + if (tile.isRightMost && this.wrapHorizontal) { + sizeC.x += 0.75; // Otherwise Firefox and Safari show seams + } + + if (tile.isBottomMost && this.wrapVertical) { + sizeC.y += 0.75; // Otherwise Firefox and Safari show seams + } + + tile.position = positionC; + tile.size = sizeC; + tile.squaredDistance = tileSquaredDistance; + tile.visibility = levelVisibility; + }, + + /** + * @private + * @inner + * Updates the opacity of a tile according to the time it has been on screen + * to perform a fade-in. + * Updates coverage once a tile is fully opaque. + * Returns whether the fade-in has completed. + * + * @param {OpenSeadragon.Tile} tile + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} currentTime + * @returns {Boolean} + */ + _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * this.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( this.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + this.lastDrawn.push( tile ); + + if ( opacity === 1 ) { + $.TileSource._setCoverage( this.coverage, level, x, y, true ); + this._hasOpaqueTile = true; + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; + }, + + + /** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + * + * @param {OpenSeadragon.Tile} previousBest + * @param {OpenSeadragon.Tile} tile + * @returns {OpenSeadragon.Tile} The new best tile. + */ + _compareTiles: function( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility === previousBest.visibility ) { + if ( tile.squaredDistance < previousBest.squaredDistance ) { + return tile; + } + } + return previousBest; + }, + + /** + * @private + * @inner + * Draws a TiledImage. + * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. + */ + _drawTiles: function( lastDrawn ) { + if (this.opacity === 0 || (lastDrawn.length === 0 && !this.placeholderFillStyle)) { + return; + } + + var tile = lastDrawn[0]; + var useSketch; + + if (tile) { + useSketch = this.opacity < 1 || + (this.compositeOperation && this.compositeOperation !== 'source-over') || + (!this._isBottomItem() && tile._hasTransparencyChannel()); + } + + var sketchScale; + var sketchTranslate; + + var zoom = this.viewport.getZoom(true); + var imageZoom = this.viewportToImageZoom(zoom); + + if (lastDrawn.length > 1 && + imageZoom > this.smoothTileEdgesMinZoom && + !this.iOSDevice && + this.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation. + $.supportsCanvas && this.viewer.useCanvas) { + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + // Note: Disabled on iOS devices per default as it causes a native crash + useSketch = true; + sketchScale = tile.getScaleForEdgeSmoothing(); + sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, + this._drawer.getCanvasSize(false), + this._drawer.getCanvasSize(true)); + } + + var bounds; + if (useSketch) { + if (!sketchScale) { + // Except when edge smoothing, we only clean the part of the + // sketch canvas we are going to use for performance reasons. + bounds = this.viewport.viewportToViewerElementRectangle( + this.getClippedBounds(true)) + .getIntegerBoundingBox(); + + if(this._drawer.viewer.viewport.getFlip()) { + if (this.viewport.degrees !== 0 || this.getRotation(true) % 360 !== 0) { + bounds.x = this._drawer.viewer.container.clientWidth - (bounds.x + bounds.width); + } + } + + bounds = bounds.times($.pixelDensityRatio); + } + this._drawer._clear(true, bounds); + } + + // When scaling, we must rotate only when blending the sketch canvas to + // avoid interpolation + if (!sketchScale) { + if (this.viewport.degrees !== 0) { + this._drawer._offsetForRotation({ + degrees: this.viewport.degrees, + useSketch: useSketch + }); + } + if (this.getRotation(true) % 360 !== 0) { + this._drawer._offsetForRotation({ + degrees: this.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + this._getRotationPoint(true), true), + useSketch: useSketch + }); + } + + if (this.viewport.degrees === 0 && this.getRotation(true) % 360 === 0){ + if(this._drawer.viewer.viewport.getFlip()) { + this._drawer._flip(); + } + } + } + + var usedClip = false; + if ( this._clip ) { + this._drawer.saveContext(useSketch); + + var box = this.imageToViewportRectangle(this._clip, true); + box = box.rotate(-this.getRotation(true), this._getRotationPoint(true)); + var clipRect = this._drawer.viewportToDrawerRectangle(box); + if (sketchScale) { + clipRect = clipRect.times(sketchScale); + } + if (sketchTranslate) { + clipRect = clipRect.translate(sketchTranslate); + } + this._drawer.setClip(clipRect, useSketch); + + usedClip = true; + } + + if (this._croppingPolygons) { + this._drawer.saveContext(useSketch); + try { + var polygons = this._croppingPolygons.map(function (polygon) { + return polygon.map(function (coord) { + var point = this + .imageToViewportCoordinates(coord.x, coord.y, true) + .rotate(-this.getRotation(true), this._getRotationPoint(true)); + var clipPoint = this._drawer.viewportCoordToDrawerCoord(point); + if (sketchScale) { + clipPoint = clipPoint.times(sketchScale); + } + return clipPoint; + }); + }); + this._drawer.clipWithPolygons(polygons, useSketch); + } catch (e) { + $.console.error(e); + } + usedClip = true; + } + + if ( this.placeholderFillStyle && this._hasOpaqueTile === false ) { + var placeholderRect = this._drawer.viewportToDrawerRectangle(this.getBounds(true)); + if (sketchScale) { + placeholderRect = placeholderRect.times(sketchScale); + } + if (sketchTranslate) { + placeholderRect = placeholderRect.translate(sketchTranslate); + } + + var fillStyle = null; + if ( typeof this.placeholderFillStyle === "function" ) { + fillStyle = this.placeholderFillStyle(this, this._drawer.context); + } + else { + fillStyle = this.placeholderFillStyle; + } + + this._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); + } + + var subPixelRoundingRule = determineSubPixelRoundingRule(this.subPixelRoundingForTransparency); + + var shouldRoundPositionAndSize = false; + + if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) { + shouldRoundPositionAndSize = true; + } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) { + var isAnimating = this.viewer && this.viewer.isAnimating(); + shouldRoundPositionAndSize = !isAnimating; + } + + for (var i = lastDrawn.length - 1; i >= 0; i--) { + tile = lastDrawn[ i ]; + this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale, sketchTranslate, shouldRoundPositionAndSize ); + tile.beingDrawn = true; + + if( this.viewer ){ + /** + * - Needs documentation - + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: this, + tile: tile + }); + } + } + + if ( usedClip ) { + this._drawer.restoreContext( useSketch ); + } + + if (!sketchScale) { + if (this.getRotation(true) % 360 !== 0) { + this._drawer._restoreRotationChanges(useSketch); + } + if (this.viewport.degrees !== 0) { + this._drawer._restoreRotationChanges(useSketch); + } + } + + if (useSketch) { + if (sketchScale) { + if (this.viewport.degrees !== 0) { + this._drawer._offsetForRotation({ + degrees: this.viewport.degrees, + useSketch: false + }); + } + if (this.getRotation(true) % 360 !== 0) { + this._drawer._offsetForRotation({ + degrees: this.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + this._getRotationPoint(true), true), + useSketch: false + }); + } + } + this._drawer.blendSketch({ + opacity: this.opacity, + scale: sketchScale, + translate: sketchTranslate, + compositeOperation: this.compositeOperation, + bounds: bounds + }); + if (sketchScale) { + if (this.getRotation(true) % 360 !== 0) { + this._drawer._restoreRotationChanges(false); + } + if (this.viewport.degrees !== 0) { + this._drawer._restoreRotationChanges(false); + } + } + } + + if (!sketchScale) { + if (this.viewport.degrees === 0 && this.getRotation(true) % 360 === 0){ + if(this._drawer.viewer.viewport.getFlip()) { + this._drawer._flip(); + } + } + } + + this._drawDebugInfo( lastDrawn ); + }, + + /** + * @private + * @inner + * Draws special debug information for a TiledImage if in debug mode. + * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. + */ + _drawDebugInfo: function( lastDrawn ) { + if( this.debugMode ) { + for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { + var tile = lastDrawn[ i ]; + try { + this._drawer.drawDebugInfo(tile, lastDrawn.length, i, this); + } catch(e) { + $.console.error(e); + } + } + } } - - if (tile.isRightMost && tiledImage.wrapHorizontal) { - sizeC.x += 0.75; // Otherwise Firefox and Safari show seams - } - - if (tile.isBottomMost && tiledImage.wrapVertical) { - sizeC.y += 0.75; // Otherwise Firefox and Safari show seams - } - - tile.position = positionC; - tile.size = sizeC; - tile.squaredDistance = tileSquaredDistance; - tile.visibility = levelVisibility; -} - -/** - * @private - * @inner - * Updates the opacity of a tile according to the time it has been on screen - * to perform a fade-in. - * Updates coverage once a tile is fully opaque. - * Returns whether the fade-in has completed. - * - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile} tile - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {Number} levelOpacity - * @param {Number} currentTime - * @returns {Boolean} - */ -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 ); - tiledImage._hasOpaqueTile = true; - } else if ( deltaTime < blendTimeMillis ) { - return true; - } - - return false; -} +}); /** * @private @@ -1831,7 +2100,7 @@ function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ * @param {Number} y - The Y position of the tile. * @returns {Boolean} */ -function providesCoverage( coverage, level, x, y ) { +$.TileSource._providesCoverage = function( coverage, level, x, y ) { var rows, cols, i, j; @@ -1861,7 +2130,7 @@ function providesCoverage( coverage, level, x, y ) { coverage[ level ][ x ][ y ] === undefined || coverage[ level ][ x ][ y ] === true ); -} +}; /** * @private @@ -1876,18 +2145,18 @@ function providesCoverage( coverage, level, x, y ) { * @param {Number} y - The Y position of the tile. * @returns {Boolean} */ -function isCovered( coverage, level, x, y ) { +$.TileSource._isCovered = function( coverage, level, x, y ) { if ( x === undefined || y === undefined ) { - return providesCoverage( coverage, level + 1 ); + return this._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 ) + this._providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + this._providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + this._providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + this._providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) ); } -} +}; /** * @private @@ -1900,7 +2169,7 @@ function isCovered( coverage, level, x, y ) { * @param {Number} y - The Y position of the tile. * @param {Boolean} covers - Whether the tile provides coverage. */ -function setCoverage( coverage, level, x, y, covers ) { +$.TileSource._setCoverage = function( coverage, level, x, y, covers ) { if ( !coverage[ level ] ) { $.console.warn( "Setting coverage for a tile before its level's coverage has been reset: %s", @@ -1914,7 +2183,7 @@ function setCoverage( coverage, level, x, y, covers ) { } coverage[ level ][ x ][ y ] = covers; -} +}; /** * @private @@ -1926,35 +2195,9 @@ function setCoverage( coverage, level, x, y, covers ) { * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. * @param {Number} level - The resolution level of tiles to completely reset. */ -function resetCoverage( coverage, level ) { +$.TileSource._resetCoverage = function( coverage, level ) { coverage[ level ] = {}; -} - -/** - * @private - * @inner - * Determines whether the 'last best' tile for the area is better than the - * tile in question. - * - * @param {OpenSeadragon.Tile} previousBest - * @param {OpenSeadragon.Tile} tile - * @returns {OpenSeadragon.Tile} The new best tile. - */ -function compareTiles( previousBest, tile ) { - if ( !previousBest ) { - return tile; - } - - if ( tile.visibility > previousBest.visibility ) { - return tile; - } else if ( tile.visibility === previousBest.visibility ) { - if ( tile.squaredDistance < previousBest.squaredDistance ) { - return tile; - } - } - - return previousBest; -} +}; /** * @private @@ -1973,7 +2216,7 @@ var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER; * @returns {Boolean} Returns true if the input value is none of the expected * {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value. */ - function isSubPixelRoundingRuleUnknown(value) { +function isSubPixelRoundingRuleUnknown(value) { return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS && value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST && value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER; @@ -1988,7 +2231,7 @@ var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER; * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize. * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value. */ - function normalizeSubPixelRoundingRule(value) { +function normalizeSubPixelRoundingRule(value) { if (isSubPixelRoundingRuleUnknown(value)) { return DEFAULT_SUBPIXEL_ROUNDING_RULE; } @@ -2023,266 +2266,4 @@ function determineSubPixelRoundingRule(subPixelRoundingRules) { return normalizeSubPixelRoundingRule(subPixelRoundingRule); } -/** - * @private - * @inner - * Draws a TiledImage. - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. - */ -function drawTiles( tiledImage, lastDrawn ) { - if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) { - return; - } - - var tile = lastDrawn[0]; - var useSketch; - - if (tile) { - useSketch = tiledImage.opacity < 1 || - (tiledImage.compositeOperation && - tiledImage.compositeOperation !== 'source-over') || - (!tiledImage._isBottomItem() && tile._hasTransparencyChannel()); - } - - var sketchScale; - var sketchTranslate; - - var zoom = tiledImage.viewport.getZoom(true); - var imageZoom = tiledImage.viewportToImageZoom(zoom); - - if (lastDrawn.length > 1 && - imageZoom > tiledImage.smoothTileEdgesMinZoom && - !tiledImage.iOSDevice && - tiledImage.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation. - $.supportsCanvas && tiledImage.viewer.useCanvas) { - // When zoomed in a lot (>100%) the tile edges are visible. - // So we have to composite them at ~100% and scale them up together. - // Note: Disabled on iOS devices per default as it causes a native crash - useSketch = true; - sketchScale = tile.getScaleForEdgeSmoothing(); - sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, - tiledImage._drawer.getCanvasSize(false), - tiledImage._drawer.getCanvasSize(true)); - } - - var bounds; - if (useSketch) { - if (!sketchScale) { - // Except when edge smoothing, we only clean the part of the - // sketch canvas we are going to use for performance reasons. - bounds = tiledImage.viewport.viewportToViewerElementRectangle( - tiledImage.getClippedBounds(true)) - .getIntegerBoundingBox(); - - if(tiledImage._drawer.viewer.viewport.getFlip()) { - if (tiledImage.viewport.degrees !== 0 || tiledImage.getRotation(true) % 360 !== 0){ - bounds.x = tiledImage._drawer.viewer.container.clientWidth - (bounds.x + bounds.width); - } - } - - bounds = bounds.times($.pixelDensityRatio); - } - tiledImage._drawer._clear(true, bounds); - } - - // When scaling, we must rotate only when blending the sketch canvas to - // avoid interpolation - if (!sketchScale) { - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation({ - degrees: tiledImage.viewport.degrees, - useSketch: useSketch - }); - } - if (tiledImage.getRotation(true) % 360 !== 0) { - tiledImage._drawer._offsetForRotation({ - degrees: tiledImage.getRotation(true), - point: tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true), - useSketch: useSketch - }); - } - - if (tiledImage.viewport.degrees === 0 && tiledImage.getRotation(true) % 360 === 0){ - if(tiledImage._drawer.viewer.viewport.getFlip()) { - tiledImage._drawer._flip(); - } - } - } - - var usedClip = false; - if ( tiledImage._clip ) { - tiledImage._drawer.saveContext(useSketch); - - var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); - box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); - var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box); - if (sketchScale) { - clipRect = clipRect.times(sketchScale); - } - if (sketchTranslate) { - clipRect = clipRect.translate(sketchTranslate); - } - tiledImage._drawer.setClip(clipRect, useSketch); - - usedClip = true; - } - - if (tiledImage._croppingPolygons) { - tiledImage._drawer.saveContext(useSketch); - try { - var polygons = tiledImage._croppingPolygons.map(function (polygon) { - return polygon.map(function (coord) { - var point = tiledImage - .imageToViewportCoordinates(coord.x, coord.y, true) - .rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); - var clipPoint = tiledImage._drawer.viewportCoordToDrawerCoord(point); - if (sketchScale) { - clipPoint = clipPoint.times(sketchScale); - } - return clipPoint; - }); - }); - tiledImage._drawer.clipWithPolygons(polygons, useSketch); - } catch (e) { - $.console.error(e); - } - usedClip = true; - } - - if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { - var placeholderRect = tiledImage._drawer.viewportToDrawerRectangle(tiledImage.getBounds(true)); - if (sketchScale) { - placeholderRect = placeholderRect.times(sketchScale); - } - if (sketchTranslate) { - placeholderRect = placeholderRect.translate(sketchTranslate); - } - - var fillStyle = null; - if ( typeof tiledImage.placeholderFillStyle === "function" ) { - fillStyle = tiledImage.placeholderFillStyle(tiledImage, tiledImage._drawer.context); - } - else { - fillStyle = tiledImage.placeholderFillStyle; - } - - tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); - } - - var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency); - - var shouldRoundPositionAndSize = false; - - if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) { - shouldRoundPositionAndSize = true; - } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) { - var isAnimating = tiledImage.viewer && tiledImage.viewer.isAnimating(); - shouldRoundPositionAndSize = !isAnimating; - } - - for (var i = lastDrawn.length - 1; i >= 0; i--) { - tile = lastDrawn[ i ]; - tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate, shouldRoundPositionAndSize ); - tile.beingDrawn = true; - - if( tiledImage.viewer ){ - /** - * - Needs documentation - - * - * @event tile-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent( 'tile-drawn', { - tiledImage: tiledImage, - tile: tile - }); - } - } - - if ( usedClip ) { - tiledImage._drawer.restoreContext( useSketch ); - } - - if (!sketchScale) { - if (tiledImage.getRotation(true) % 360 !== 0) { - tiledImage._drawer._restoreRotationChanges(useSketch); - } - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(useSketch); - } - } - - if (useSketch) { - if (sketchScale) { - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation({ - degrees: tiledImage.viewport.degrees, - useSketch: false - }); - } - if (tiledImage.getRotation(true) % 360 !== 0) { - tiledImage._drawer._offsetForRotation({ - degrees: tiledImage.getRotation(true), - point: tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true), - useSketch: false - }); - } - } - tiledImage._drawer.blendSketch({ - opacity: tiledImage.opacity, - scale: sketchScale, - translate: sketchTranslate, - compositeOperation: tiledImage.compositeOperation, - bounds: bounds - }); - if (sketchScale) { - if (tiledImage.getRotation(true) % 360 !== 0) { - tiledImage._drawer._restoreRotationChanges(false); - } - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(false); - } - } - } - - if (!sketchScale) { - if (tiledImage.viewport.degrees === 0 && tiledImage.getRotation(true) % 360 === 0){ - if(tiledImage._drawer.viewer.viewport.getFlip()) { - tiledImage._drawer._flip(); - } - } - } - - drawDebugInfo( tiledImage, lastDrawn ); -} - -/** - * @private - * @inner - * Draws special debug information for a TiledImage if in debug mode. - * @param {OpenSeadragon.TiledImage} tiledImage - * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame. - */ -function drawDebugInfo( tiledImage, lastDrawn ) { - if( tiledImage.debugMode ) { - for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { - var tile = lastDrawn[ i ]; - try { - tiledImage._drawer.drawDebugInfo( - tile, lastDrawn.length, i, tiledImage); - } catch(e) { - $.console.error(e); - } - } - } -} - }( OpenSeadragon )); diff --git a/test/modules/basic.js b/test/modules/basic.js index 2eefd509..6cc2ad2b 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -58,7 +58,7 @@ assert.equal($(".openseadragon-message").length, 1, "Open failures should display a message"); - assert.ok(testLog.log.contains('["AJAX request returned %d: %s",404,"/test/data/not-a-real-file"]'), + assert.ok(testLog.error.contains('["AJAX request returned %d: %s",404,"/test/data/not-a-real-file"]'), "AJAX failures should be logged to the console"); done(); diff --git a/test/modules/strings.js b/test/modules/strings.js index ec1bf87a..78a324c5 100644 --- a/test/modules/strings.js +++ b/test/modules/strings.js @@ -22,11 +22,11 @@ QUnit.test("getInvalidString", function(assert) { assert.equal(OpenSeadragon.getString("Greeting"), "", "Handled unset string key"); - assert.ok(testLog.log.contains('["Untranslated source string:","Greeting"]'), + assert.ok(testLog.error.contains('["Untranslated source string:","Greeting"]'), 'Invalid string keys are logged'); assert.equal(OpenSeadragon.getString("Errors"), "", "Handled requesting parent key"); - assert.ok(testLog.log.contains('["Untranslated source string:","Errors"]'), + assert.ok(testLog.error.contains('["Untranslated source string:","Errors"]'), 'Invalid string parent keys are logged'); });