diff --git a/src/tiledimage.js b/src/tiledimage.js index 446ed1f5..bd54d232 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -799,7 +799,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @returns {OpenSeadragon.Point} */ _getRotationPoint: function(current) { - return this.getBoundsNoRotate(current).getTopLeft(); + return this.getBoundsNoRotate(current).getCenter(); }, /** diff --git a/src/tiledimage.js.orig b/src/tiledimage.js.orig new file mode 100644 index 00000000..2eaa471e --- /dev/null +++ b/src/tiledimage.js.orig @@ -0,0 +1,1707 @@ +/* + * OpenSeadragon - TiledImage + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +/** + * You shouldn't have to create a TiledImage directly; use {@link OpenSeadragon.Viewer#open} + * or {@link OpenSeadragon.Viewer#addTiledImage} instead. + * @class TiledImage + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * A new instance is created for each TileSource opened. + * @param {Object} options - Configuration for this TiledImage. + * @param {OpenSeadragon.TileSource} options.source - The TileSource that defines this TiledImage. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this TiledImage. + * @param {OpenSeadragon.TileCache} options.tileCache - The TileCache for this TiledImage to use. + * @param {OpenSeadragon.Drawer} options.drawer - The Drawer for this TiledImage to draw onto. + * @param {OpenSeadragon.ImageLoader} options.imageLoader - The ImageLoader for this TiledImage to use. + * @param {Number} [options.x=0] - Left position, in viewport coordinates. + * @param {Number} [options.y=0] - Top position, in viewport coordinates. + * @param {Number} [options.width=1] - Width, in viewport coordinates. + * @param {Number} [options.height] - Height, in viewport coordinates. + * @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates + * to fit the image into. If specified, x, y, width and height get ignored. + * @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds if options.fitBounds is set. + * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to + * (portions of the image outside of this area will not be visible). Only works on + * browsers that support the HTML5 canvas. + * @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.immediateRender] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. + * @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}. + */ +$.TiledImage = function( options ) { + var _this = this; + + $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); + $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); + $.console.assert( options.viewer, "[TiledImage] options.viewer is required" ); + $.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" ); + $.console.assert( options.source, "[TiledImage] options.source is required" ); + $.console.assert(!options.clip || options.clip instanceof $.Rect, + "[TiledImage] options.clip must be an OpenSeadragon.Rect if present"); + + $.EventSource.call( this ); + + this._tileCache = options.tileCache; + delete options.tileCache; + + this._drawer = options.drawer; + delete options.drawer; + + this._imageLoader = options.imageLoader; + delete options.imageLoader; + + if (options.clip instanceof $.Rect) { + this._clip = options.clip.clone(); + } + + delete options.clip; + + var x = options.x || 0; + delete options.x; + var y = options.y || 0; + delete options.y; + + // Ratio of zoomable image height to width. + this.normHeight = options.source.dimensions.y / options.source.dimensions.x; + this.contentAspectX = options.source.dimensions.x / options.source.dimensions.y; + + var scale = 1; + if ( options.width ) { + scale = options.width; + delete options.width; + + if ( options.height ) { + $.console.error( "specifying both width and height to a tiledImage is not supported" ); + delete options.height; + } + } else if ( options.height ) { + scale = options.height / this.normHeight; + delete options.height; + } + + var fitBounds = options.fitBounds; + delete options.fitBounds; + var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; + delete options.fitBoundsPlacement; + + this._degrees = $.positiveModulo(options.degrees || 0, 360); + delete options.degrees; + + $.extend( true, this, { + + //internal state properties + viewer: null, + tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. + coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. + lastDrawn: [], // An unordered list of Tiles drawn last frame. + lastResetTime: 0, // Last time for which the tiledImage was reset. + _midDraw: false, // Is the tiledImage currently updating the viewport? + _needsDraw: true, // Does the tiledImage need to update the viewport again? + _hasOpaqueTile: false, // Do we have even one fully opaque tile? + //configurable settings + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation + }, options ); + + this._fullyLoaded = false; + + this._xSpring = new $.Spring({ + initial: x, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._ySpring = new $.Spring({ + initial: y, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._scaleSpring = new $.Spring({ + initial: scale, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._updateForScale(); + + if (fitBounds) { + this.fitBounds(fitBounds, fitBoundsPlacement, true); + } + + // 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.viewer.raiseEvent('tile-drawing', $.extend({ + tiledImage: _this + }, args)); + }; +}; + +$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + needsDraw: function() { + return this._needsDraw; + }, + + /** + * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded. + */ + getFullyLoaded: function() { + return this._fullyLoaded; + }, + + // private + _setFullyLoaded: function(flag) { + if (flag === this._fullyLoaded) { + return; + } + + this._fullyLoaded = flag; + + /** + * Fired when the TiledImage's "fully loaded" flag (whether all tiles necessary for this TiledImage + * to draw at the current view have been loaded) changes. + * + * @event fully-loaded-change + * @memberof OpenSeadragon.TiledImage + * @type {object} + * @property {Boolean} fullyLoaded - The new "fully loaded" value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('fully-loaded-change', { + fullyLoaded: this._fullyLoaded + }); + }, + + /** + * Clears all tiles and triggers an update on the next call to + * {@link OpenSeadragon.TiledImage#update}. + */ + reset: function() { + this._tileCache.clearTilesFor(this); + this.lastResetTime = $.now(); + this._needsDraw = true; + }, + + /** + * Updates the TiledImage's bounds, animating if needed. + * @returns {Boolean} Whether the TiledImage animated. + */ + update: function() { + var oldX = this._xSpring.current.value; + var oldY = this._ySpring.current.value; + var oldScale = this._scaleSpring.current.value; + + this._xSpring.update(); + this._ySpring.update(); + this._scaleSpring.update(); + + if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY || + this._scaleSpring.current.value !== oldScale) { + this._updateForScale(); + this._needsDraw = true; + return true; + } + + return false; + }, + + /** + * Draws the TiledImage to its Drawer. + */ + draw: function() { + if (this.opacity !== 0) { + this._midDraw = true; + this._updateViewport(); + this._midDraw = false; + } + }, + + /** + * Destroy the TiledImage (unload current loaded tiles). + */ + destroy: function() { + this.reset(); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBounds: function(current) { + return this.getBoundsNoRotate(current) + .rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates without taking + * rotation into account. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBoundsNoRotate: function(current) { + return current ? + new $.Rect( + this._xSpring.current.value, + this._ySpring.current.value, + this._worldWidthCurrent, + this._worldHeightCurrent) : + new $.Rect( + this._xSpring.target.value, + this._ySpring.target.value, + this._worldWidthTarget, + this._worldHeightTarget); + }, + + // deprecated + getWorldBounds: function() { + $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); + return this.getBounds(); + }, + + /** + * Get the bounds of the displayed part of the tiled image. + * @param {Boolean} [current=false] Pass true for the current location, + * false for the target location. + * @returns {$.Rect} The clipped bounds in viewport coordinates. + */ + getClippedBounds: function(current) { + var bounds = this.getBoundsNoRotate(current); + if (this._clip) { + var worldWidth = current ? + this._worldWidthCurrent : this._worldWidthTarget; + var ratio = worldWidth / this.source.dimensions.x; + var clip = this._clip.times(ratio); + bounds = new $.Rect( + bounds.x + clip.x, + bounds.y + clip.y, + clip.width, + clip.height); + } + return bounds.rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. + */ + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + }, + + // private + _viewportToImageDelta: function( viewerX, viewerY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point(viewerX * (this.source.dimensions.x / scale), + viewerY * ((this.source.dimensions.y * this.contentAspectX) / scale)); + }, + + /** + * Translates from OpenSeadragon viewer coordinate system to image coordinate system. + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system. + * @param {Number} [viewerY] - The Y coordinate in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the image. + */ + viewportToImageCoordinates: function(viewerX, viewerY, current) { + var point; + if (viewerX instanceof $.Point) { + //they passed a point instead of individual components + current = viewerY; + point = viewerX; + } else { + point = new $.Point(viewerX, viewerY); + } + + point = point.rotate(-this._degrees, this._getRotationPoint(current)); + return current ? + this._viewportToImageDelta( + point.x - this._xSpring.current.value, + point.y - this._ySpring.current.value) : + this._viewportToImageDelta( + point.x - this._xSpring.target.value, + point.y - this._ySpring.target.value); + }, + + // private + _imageToViewportDelta: function( imageX, imageY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point((imageX / this.source.dimensions.x) * scale, + (imageY / this.source.dimensions.y / this.contentAspectX) * scale); + }, + + /** + * Translates from image coordinate system to OpenSeadragon viewer coordinate system + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system. + * @param {Number} [imageY] - The Y coordinate in image coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function(imageX, imageY, current) { + if (imageX instanceof $.Point) { + //they passed a point instead of individual components + current = imageY; + imageY = imageX.y; + imageX = imageX.x; + } + + var point = this._imageToViewportDelta(imageX, imageY); + if (current) { + point.x += this._xSpring.current.value; + point.y += this._ySpring.current.value; + } else { + point.x += this._xSpring.target.value; + point.y += this._ySpring.target.value; + } + + return point.rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * Translates from a rectangle which describes a portion of the image in + * pixel coordinates to OpenSeadragon viewport rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system. + * @param {Number} [imageY] - The top coordinate in image coordinate system. + * @param {Number} [pixelWidth] - The width in pixel of the rectangle. + * @param {Number} [pixelHeight] - The height in pixel of the rectangle. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. + */ + imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) { + var rect = imageX; + if (rect instanceof $.Rect) { + //they passed a rect instead of individual components + current = imageY; + } else { + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); + } + + var coordA = this.imageToViewportCoordinates(rect.getTopLeft(), current); + var coordB = this._imageToViewportDelta(rect.width, rect.height, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees + this._degrees + ); + }, + + /** + * Translates from a rectangle which describes a portion of + * the viewport in point coordinates to image rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system. + * @param {Number} [viewerY] - The top coordinate in viewport coordinate system. + * @param {Number} [pointWidth] - The width in viewport coordinate system. + * @param {Number} [pointHeight] - The height in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Rect} A rect representing the coordinates in the image. + */ + viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight, current ) { + var rect = viewerX; + if (viewerX instanceof $.Rect) { + //they passed a rect instead of individual components + current = viewerY; + } else { + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); + } + + var coordA = this.viewportToImageCoordinates(rect.getTopLeft(), current); + var coordB = this._viewportToImageDelta(rect.width, rect.height, current); + + return new $.Rect( + coordA.x, + coordA.y, + coordB.x, + coordB.y, + rect.degrees - this._degrees + ); + }, + + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToImageCoordinates: function( pixel ) { + var point = this.viewport.pointFromPixel( pixel, true ); + return this.viewportToImageCoordinates( point ); + }, + + /** + * Convert pixel coordinates relative to the image to + * viewer element coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( pixel ) { + var point = this.imageToViewportCoordinates( pixel ); + return this.viewport.pixelFromPoint( point, true ); + }, + + /** + * Convert pixel coordinates relative to the window to image coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToImageCoordinates: function( pixel ) { + var viewerCoordinates = pixel.minus( + OpenSeadragon.getElementPosition( this.viewer.element )); + return this.viewerElementToImageCoordinates( viewerCoordinates ); + }, + + /** + * Convert image coordinates to pixel coordinates relative to the window. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToWindowCoordinates: function( pixel ) { + var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); + return viewerCoordinates.plus( + OpenSeadragon.getElementPosition( this.viewer.element )); + }, + + // private + // Convert rectangle in tiled image coordinates to viewport coordinates. + _tiledImageToViewportRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + return new $.Rect( + rect.x * scale + this._xSpring.current.value, + rect.y * scale + this._ySpring.current.value, + rect.width * scale, + rect.height * scale, + rect.degrees) + .rotate(this.getRotation(), this._getRotationPoint(true)); + }, + + // private + // Convert rectangle in viewport coordinates to tiled image coordinates. + _viewportToTiledImageRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true)); + return new $.Rect( + (rect.x - this._xSpring.current.value) / scale, + (rect.y - this._ySpring.current.value) / scale, + rect.width / scale, + rect.height / scale, + rect.degrees); + }, + + /** + * Convert a viewport zoom to an image zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * @function + * @param {Number} viewportZoom The viewport zoom + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function( viewportZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return ratio * viewportZoom ; + }, + + /** + * Convert an image zoom to a viewport zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * Note: not accurate with multi-image. + * @function + * @param {Number} imageZoom The image zoom + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function( imageZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return imageZoom / ratio; + }, + + /** + * Sets the TiledImage's position in the world. + * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setPosition: function(position, immediately) { + var sameTarget = (this._xSpring.target.value === position.x && + this._ySpring.target.value === position.y); + + if (immediately) { + if (sameTarget && this._xSpring.current.value === position.x && + this._ySpring.current.value === position.y) { + return; + } + + this._xSpring.resetTo(position.x); + this._ySpring.resetTo(position.y); + this._needsDraw = true; + } else { + if (sameTarget) { + return; + } + + this._xSpring.springTo(position.x); + this._ySpring.springTo(position.y); + this._needsDraw = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + /** + * Sets the TiledImage's width in the world, adjusting the height to match based on aspect ratio. + * @param {Number} width - The new width, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setWidth: function(width, immediately) { + this._setScale(width, immediately); + }, + + /** + * Sets the TiledImage's height in the world, adjusting the width to match based on aspect ratio. + * @param {Number} height - The new height, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setHeight: function(height, immediately) { + this._setScale(height / this.normHeight, immediately); + }, + + /** + * Positions and scales the TiledImage to fit in the specified bounds. + * Note: this method fires OpenSeadragon.TiledImage.event:bounds-change + * twice + * @param {OpenSeadragon.Rect} bounds The bounds to fit the image into. + * @param {OpenSeadragon.Placement} [anchor=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds. + * @param {Boolean} [immediately=false] Whether to animate to the new size + * or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + fitBounds: function(bounds, anchor, immediately) { + anchor = anchor || $.Placement.CENTER; + var anchorProperties = $.Placement.properties[anchor]; + var aspectRatio = this.contentAspectX; + var xOffset = 0; + var yOffset = 0; + var displayedWidthRatio = 1; + var displayedHeightRatio = 1; + if (this._clip) { + aspectRatio = this._clip.getAspectRatio(); + displayedWidthRatio = this._clip.width / this.source.dimensions.x; + displayedHeightRatio = this._clip.height / this.source.dimensions.y; + if (bounds.getAspectRatio() > aspectRatio) { + xOffset = this._clip.x / this._clip.height * bounds.height; + yOffset = this._clip.y / this._clip.height * bounds.height; + } else { + xOffset = this._clip.x / this._clip.width * bounds.width; + yOffset = this._clip.y / this._clip.width * bounds.width; + } + } + + if (bounds.getAspectRatio() > aspectRatio) { + // We will have margins on the X axis + var height = bounds.height / displayedHeightRatio; + var marginLeft = 0; + if (anchorProperties.isHorizontallyCentered) { + marginLeft = (bounds.width - bounds.height * aspectRatio) / 2; + } else if (anchorProperties.isRight) { + marginLeft = bounds.width - bounds.height * aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset), + immediately); + this.setHeight(height, immediately); + } else { + // We will have margins on the Y axis + var width = bounds.width / displayedWidthRatio; + var marginTop = 0; + if (anchorProperties.isVerticallyCentered) { + marginTop = (bounds.height - bounds.width / aspectRatio) / 2; + } else if (anchorProperties.isBottom) { + marginTop = bounds.height - bounds.width / aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop), + immediately); + this.setWidth(width, immediately); + } + }, + + /** + * @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle, + * in image pixels, or null if none. + */ + getClip: function() { + if (this._clip) { + return this._clip.clone(); + } + + return null; + }, + + /** + * @param {OpenSeadragon.Rect|null} newClip - An area, in image pixels, to clip to + * (portions of the image outside of this area will not be visible). Only works on + * browsers that support the HTML5 canvas. + */ + setClip: function(newClip) { + $.console.assert(!newClip || newClip instanceof $.Rect, + "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); + +//TODO: should this._raiseBoundsChange(); be called? + + if (newClip instanceof $.Rect) { + this._clip = newClip.clone(); + } else { + this._clip = null; + } + + this._needsDraw = true; + }, + + /** + * @returns {Number} The TiledImage's current opacity. + */ + getOpacity: function() { + return this.opacity; + }, + + /** + * @param {Number} opacity Opacity the tiled image should be drawn at. + */ + setOpacity: function(opacity) { + this.opacity = opacity; + this._needsDraw = true; + }, + + /** + * Get the current rotation of this tiled image in degrees. + * @returns {Number} the current rotation of this tiled image in degrees. + */ + getRotation: function() { + return this._degrees; + }, + + /** + * Set the current rotation of this tiled image in degrees. + * @param {Number} degrees the rotation in degrees. + */ + setRotation: function(degrees) { + degrees = $.positiveModulo(degrees, 360); + if (this._degrees === degrees) { + return; + } + this._degrees = degrees; + this._needsDraw = true; + this._raiseBoundsChange(); + }, + + /** + * @private + * Get the point around which this tiled image is rotated + * @param {Boolean} current True for current rotation point, false for target. + * @returns {OpenSeadragon.Point} + */ + _getRotationPoint: function(current) { + return this.getBoundsNoRotate(current).getTopLeft(); + }, + + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this.compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; + this._needsDraw = true; + }, + + // private + _setScale: function(scale, immediately) { + var sameTarget = (this._scaleSpring.target.value === scale); + if (immediately) { + if (sameTarget && this._scaleSpring.current.value === scale) { + return; + } + + this._scaleSpring.resetTo(scale); + this._updateForScale(); + this._needsDraw = true; + } else { + if (sameTarget) { + return; + } + + this._scaleSpring.springTo(scale); + this._updateForScale(); + this._needsDraw = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + // private + _updateForScale: function() { + this._worldWidthTarget = this._scaleSpring.target.value; + this._worldHeightTarget = this.normHeight * this._scaleSpring.target.value; + this._worldWidthCurrent = this._scaleSpring.current.value; + this._worldHeightCurrent = this.normHeight * this._scaleSpring.current.value; + }, + + // private + _raiseBoundsChange: function() { + /** + * Raised when the TiledImage's bounds are changed. + * Note that this event is triggered only when the animation target is changed; + * not for every frame of animation. + * @event bounds-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('bounds-change'); + }, + + // private + _isBottomItem: function() { + return this.viewer.world.getItemAt(0) === this; + }, + + // private + _getLevelsInterval: function() { + var lowestLevel = Math.max( + this.source.minLevel, + Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2)) + ); + var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(0), true).x * + this._scaleSpring.current.value; + var highestLevel = Math.min( + Math.abs(this.source.maxLevel), + Math.abs(Math.floor( + Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2) + )) + ); + + // Calculations for the interval of levels to draw + // can return invalid intervals; fix that here if necessary + lowestLevel = Math.min(lowestLevel, highestLevel); + return { + lowestLevel: lowestLevel, + highestLevel: highestLevel + }; + }, + + // private + _updateViewport: function() { + this._needsDraw = false; + + // Reset tile's internal drawn state + while (this.lastDrawn.length > 0) { + var tile = this.lastDrawn.pop(); + tile.beingDrawn = false; + } + + var viewport = this.viewport; + var drawArea = this._viewportToTiledImageRectangle( + viewport.getBoundsWithMargins(true)); + + if (!this.wrapHorizontal && !this.wrapVertical) { + var tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(true)); + drawArea = drawArea.intersection(tiledImageBounds); + if (drawArea === null) { + return; + } + } + + var levelsInterval = this._getLevelsInterval(); + var lowestLevel = levelsInterval.lowestLevel; + var highestLevel = levelsInterval.highestLevel; + var bestTile = null; + var haveDrawn = false; + var currentTime = $.now(); + + // Update any level that will be drawn + for (var level = highestLevel; level >= lowestLevel; level--) { + var drawLevel = false; + + //Avoid calculations for draw if we have already drawn this + var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + true + ).x * this._scaleSpring.current.value; + + if (level === lowestLevel || + (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) { + drawLevel = true; + haveDrawn = true; + } else if (!haveDrawn) { + continue; + } + + //Perform calculations for draw if we haven't drawn this + var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio( + Math.max( + this.source.getClosestLevel(viewport.containerSize) - 1, + 0 + ) + ), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var optimalRatio = this.immediateRender ? 1 : targetZeroRatio; + var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); + var levelVisibility = optimalRatio / Math.abs( + optimalRatio - targetRenderPixelRatio + ); + + // Update the level and keep track of 'best' tile to load + bestTile = updateLevel( + this, + haveDrawn, + drawLevel, + level, + levelOpacity, + levelVisibility, + drawArea, + currentTime, + bestTile + ); + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if (providesCoverage(this.coverage, level)) { + break; + } + } + + // Perform the actual drawing + drawTiles(this, this.lastDrawn); + +<<<<<<< HEAD + // Load the new 'best' tile + if (bestTile && !bestTile.context2D) { + loadTile(this, bestTile, currentTime); + this._setFullyLoaded(false); + } else { + this._setFullyLoaded(true); + } +======= + // Load the new 'best' tile + if (best && !best.context2D) { + loadTile( tiledImage, best, currentTime ); + tiledImage._needsDraw = true; + tiledImage._setFullyLoaded(false); + } else { + tiledImage._setFullyLoaded(true); +>>>>>>> e8324627e144f6e01ab1ce70cd88194fbab46487 + } +}); + +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 + }); + } + + //OK, a new drawing so do your calculations + var topLeftTile = tiledImage.source.getTileAtPoint(level, topLeftBound); + var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound); + var numberOfTiles = tiledImage.source.getNumTiles(level); + + resetCoverage(tiledImage.coverage, level); + + if (tiledImage.wrapHorizontal) { + topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) + } else { + // Adjust for floating point error + topLeftTile.x = Math.max(topLeftTile.x, 0); + bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); + } + if (tiledImage.wrapVertical) { + topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) + } else { + // Adjust for floating point error + topLeftTile.y = Math.max(topLeftTile.y, 0); + bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); + } + + var viewportCenter = tiledImage.viewport.pixelFromPoint( + tiledImage.viewport.getCenter()); + for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { + for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { + + var tileBounds = tiledImage.source.getTileBounds(level, x, y); + + if (drawArea.intersection(tileBounds) === null) { + // This tile is outside of the viewport, no need to draw it + continue; + } + + best = updateTile( + tiledImage, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + + return best; +} + +function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + tiledImage.source, + tiledImage.tilesMatrix, + currentTime, + numberOfTiles, + tiledImage._worldWidthCurrent, + tiledImage._worldHeightCurrent + ), + drawTile = drawLevel; + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event update-tile + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'update-tile', { + tiledImage: tiledImage, + tile: tile + }); + } + + setCoverage( tiledImage.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( tiledImage.coverage, level, x, y ) ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + tiledImage.source.tileOverlap, + tiledImage.viewport, + viewportCenter, + levelVisibility, + tiledImage + ); + + if (!tile.loaded) { + if (tile.context2D) { + setTileLoaded(tiledImage, tile); + } else { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + 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 + // 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, + context2D, + 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 ); + context2D = tileSource.getContext2D ? + tileSource.getContext2D(level, xMod, yMod) : undefined; + + bounds.x += ( x - xMod ) / numTiles.x; + bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url, + context2D + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +} + +function loadTile( tiledImage, tile, time ) { + tile.loading = true; + tiledImage._imageLoader.addJob({ + src: tile.url, + crossOriginPolicy: tiledImage.crossOriginPolicy, + callback: function( image, errorMsg ){ + onTileLoad( tiledImage, tile, time, image, errorMsg ); + }, + abort: function() { + tile.loading = false; + } + }); +} + +function onTileLoad( tiledImage, tile, time, image, errorMsg ) { + if ( !image ) { + $.console.log( "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. + */ + tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg}); + tile.loading = false; + tile.exists = false; + return; + } + + if ( time < tiledImage.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + tile.loading = false; + return; + } + + var finish = function() { + var cutoff = Math.ceil( Math.log( + tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) ); + setTileLoaded(tiledImage, tile, image, cutoff); + }; + + // Check if we're mid-update; this can happen on IE8 because image load events for + // 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); + } +} + +function setTileLoaded(tiledImage, tile, image, cutoff) { + var increment = 0; + + function getCompletionCallback() { + increment++; + return completionCallback; + } + + function completionCallback() { + increment--; + if (increment === 0) { + tile.loading = false; + tile.loaded = true; + 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 {function} getCompletionCallback - A function giving a callback to call + * when the asynchronous processing of the image is done. The image will be + * marked as entirely loaded when the callback has been called once for each + * call to getCompletionCallback. + */ + tiledImage.viewer.raiseEvent("tile-loaded", { + tile: tile, + tiledImage: tiledImage, + image: image, + getCompletionCallback: getCompletionCallback + }); + // In case the completion callback is never called, we at least force it once. + getCompletionCallback()(); +} + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ + var boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= tiledImage._scaleSpring.current.value; + boundsTL.y *= tiledImage._scaleSpring.current.value; + boundsTL.x += tiledImage._xSpring.current.value; + boundsTL.y += tiledImage._ySpring.current.value; + + var boundsSize = tile.bounds.getSize(); + + boundsSize.x *= tiledImage._scaleSpring.current.value; + boundsSize.y *= tiledImage._scaleSpring.current.value; + + var positionC = viewport.pixelFromPointNoRotate(boundsTL, true), + positionT = viewport.pixelFromPointNoRotate(boundsTL, false), + sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true), + sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileDistance = viewportCenter.distanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +} + + +function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( tiledImage.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + tiledImage.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + tiledImage._hasOpaqueTile = true; + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +} + +/** + * @private + * @inner + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + */ +function providesCoverage( coverage, level, x, y ) { + var rows, + cols, + i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); +} + +/** + * @private + * @inner + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +} + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; +} + +/** + * @private + * @inner + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +} + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +} + +function drawTiles( tiledImage, lastDrawn ) { + if (lastDrawn.length === 0) { + return; + } + var tile = lastDrawn[0]; + + var 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 (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice) { + // 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() + .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( + tiledImage.viewport.degrees, useSketch); + } + if (tiledImage._degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage._degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch); + } + } + + var usedClip = false; + if ( tiledImage._clip ) { + tiledImage._drawer.saveContext(useSketch); + + var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); + box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint()); + 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.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); + } + + for (var i = lastDrawn.length - 1; i >= 0; i--) { + tile = lastDrawn[ i ]; + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); + 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._degrees !== 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( + tiledImage.viewport.degrees, false); + } + if (tiledImage._degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage._degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch); + } + } + tiledImage._drawer.blendSketch({ + opacity: tiledImage.opacity, + scale: sketchScale, + translate: sketchTranslate, + compositeOperation: tiledImage.compositeOperation, + bounds: bounds + }); + if (sketchScale) { + if (tiledImage._degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } + } + } + drawDebugInfo( tiledImage, lastDrawn ); +} + +function drawDebugInfo( tiledImage, lastDrawn ) { + if( tiledImage.debugMode ) { + for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { + var tile = lastDrawn[ i ]; + try { + tiledImage._drawer.drawDebugInfo( + tile, lastDrawn.length, i, tiledImage); + } catch(e) { + $.console.error(e); + } + } + } +} + +}( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index 842f7f2d..06b224e0 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1229,7 +1229,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * browsers that support the HTML5 canvas. * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. * @param {Number} [options.degrees=0] Initial rotation of the tiled image around - * it top left corner in degrees. + * its top left corner in degrees. * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy.