From be30b429f8961eb08ee858a698866efbbf487bf7 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 22 May 2024 18:54:29 -0400 Subject: [PATCH] remove unused code paths --- src/tiledimage.js | 4337 ++++++++++++++++++++++----------------------- 1 file changed, 2152 insertions(+), 2185 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 545ec5a1..6a2d7253 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -34,2278 +34,2245 @@ (function( $ ){ -/** - * You shouldn't have to create a TiledImage instance directly; get it asynchronously by - * using {@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] - 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 {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}. - * @param {Boolean} [options.ajaxWithCredentials] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.loadTilesWithAjax] - * Whether to load tile data using AJAX requests. - * Defaults to the setting in {@link OpenSeadragon.Options}. - * @param {Object} [options.ajaxHeaders={}] - * A set of headers to include when making tile AJAX requests. - */ -$.TiledImage = function( options ) { - this._initialized = false; /** - * The {@link OpenSeadragon.TileSource} that defines this TiledImage. - * @member {OpenSeadragon.TileSource} source - * @memberof OpenSeadragon.TiledImage# - */ - $.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; - - var degrees = options.degrees || 0; - delete options.degrees; - - var ajaxHeaders = options.ajaxHeaders; - delete options.ajaxHeaders; - - $.extend( true, this, { - - //internal state properties - viewer: null, - tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. - coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas have been drawn. - loadingCoverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas are loaded or are being loaded/blended. - lastDrawn: [], // An unordered list of Tiles drawn last frame. - lastResetTime: 0, // Last time for which the tiledImage was reset. - _needsDraw: true, // Does the tiledImage need to be drawn again? - _needsUpdate: true, // Does the tiledImage need to update the viewport again? - _hasOpaqueTile: false, // Do we have even one fully opaque tile? - _tilesLoading: 0, // The number of pending tile requests. - _tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile] - _lastDrawn: [], // array of tiles that were last fetched by the drawer - _isBlending: false, // Are any tiles still being blended? - _wasBlending: false, // Were any tiles blending before the last draw? - _isTainted: false, // Has a Tile been found with tainted data? - //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, - ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, - placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity, - preload: $.DEFAULT_SETTINGS.preload, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, - subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency, - maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame - }, options ); - - this._preload = this.preload; - delete this.preload; - - 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._degreesSpring = new $.Spring({ - initial: degrees, - springStiffness: this.springStiffness, - animationTime: this.animationTime - }); - - this._updateForScale(); - - if (fitBounds) { - this.fitBounds(fitBounds, fitBoundsPlacement, true); - } - - this._ownAjaxHeaders = {}; - this.setAjaxHeaders(ajaxHeaders, false); - this._initialized = true; -}; - -$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ - /** - * @returns {Boolean} Whether the TiledImage needs to be drawn. - */ - needsDraw: function() { - return this._needsDraw; - }, - - /** - * Mark the tiled image as needing to be (re)drawn - */ - redraw: function() { - this._needsDraw = true; - }, - - /** - * @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. Based on the new - * bounds, updates the levels and tiles to be drawn into the viewport. - * @param viewportChanged Whether the viewport changed meaning tiles need to be updated. - * @returns {Boolean} Whether the TiledImage needs to be drawn. - */ - update: function(viewportChanged) { - let xUpdated = this._xSpring.update(); - let yUpdated = this._ySpring.update(); - let scaleUpdated = this._scaleSpring.update(); - let degreesUpdated = this._degreesSpring.update(); - - let updated = (xUpdated || yUpdated || scaleUpdated || degreesUpdated || this._needsUpdate); - - if (updated || viewportChanged || !this._fullyLoaded){ - let fullyLoadedFlag = this._updateLevelsForViewport(); - this._setFullyLoaded(fullyLoadedFlag); - } - - this._needsUpdate = false; - - if (updated) { - this._updateForScale(); - this._raiseBoundsChange(); - this._needsDraw = true; - return true; - } - - return false; - }, - - /** - * Mark this TiledImage as having been drawn, so that it will only be drawn - * again if something changes about the image. If the image is still blending, - * this will have no effect. - * @returns {Boolean} whether the item still needs to be drawn due to blending - */ - setDrawn: function(){ - this._needsDraw = this._isBlending || this._wasBlending; - return this._needsDraw; - }, - - /** - * Set the internal _isTainted flag for this TiledImage. Lazy loaded - not - * checked each time a Tile is loaded, but can be set if a consumer of the - * tiles (e.g. a Drawer) discovers a Tile to have tainted data so that further - * checks are not needed and alternative rendering strategies can be used. - * @private - */ - setTainted(isTainted){ - this._isTainted = isTainted; - }, - - /** - * @private - * @returns {Boolean} whether the TiledImage has been marked as tainted - */ - isTainted(){ - return this._isTainted; - }, - - /** - * Destroy the TiledImage (unload current loaded tiles). - */ - destroy: function() { - this.reset(); - - if (this.source.destroy) { - this.source.destroy(this.viewer); - } - }, - - /** - * 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.getRotation(current), 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.getRotation(current), this._getRotationPoint(current)); - }, - - /** - * @function - * @param {Number} level - * @param {Number} x - * @param {Number} y - * @returns {OpenSeadragon.Rect} Where this tile fits (in normalized coordinates). - */ - getTileBounds: function( level, x, y ) { - var numTiles = this.source.getNumTiles(level); - var xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; - var yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - var bounds = this.source.getTileBounds(level, xMod, yMod); - if (this.getFlip()) { - bounds.x = Math.max(0, 1 - bounds.x - bounds.width); - } - bounds.x += (x - xMod) / numTiles.x; - bounds.y += (this._worldHeightCurrent / this._worldWidthCurrent) * ((y - yMod) / numTiles.y); - return bounds; - }, - - /** - * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. - */ - getContentSize: function() { - return new $.Point(this.source.dimensions.x, this.source.dimensions.y); - }, - - /** - * @returns {OpenSeadragon.Point} The TiledImage's content size, in window coordinates. - */ - 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); - }, - - // 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. - * @returns {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.getRotation(current), 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. - * @returns {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, current); - 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.getRotation(current), 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. - * @returns {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.getRotation(current) - ); - }, - - /** - * 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. - * @returns {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.getRotation(current) - ); - }, - - /** - * 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 viewport coordinates to this tiled image point - // coordinates (x in [0, 1] and y in [0, aspectRatio]) - _viewportToTiledImageRectangle: function(rect) { - var scale = this._scaleSpring.current.value; - rect = rect.rotate(-this.getRotation(true), 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; - this._needsUpdate = true; - } else { - if (sameTarget) { - return; - } - - this._xSpring.springTo(position.x); - this._ySpring.springTo(position.y); - this._needsDraw = true; - this._needsUpdate = 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); - }, - - /** - * Sets an array of polygons to crop the TiledImage during draw tiles. - * The render function will use the default non-zero winding rule. - * @param {OpenSeadragon.Point[][]} polygons - represented in an array of point object in image coordinates. - * Example format: [ - * [{x: 197, y:172}, {x: 226, y:172}, {x: 226, y:198}, {x: 197, y:198}], // First polygon - * [{x: 328, y:200}, {x: 330, y:199}, {x: 332, y:201}, {x: 329, y:202}] // Second polygon - * [{x: 321, y:201}, {x: 356, y:205}, {x: 341, y:250}] // Third polygon - * ] - */ - setCroppingPolygons: function( polygons ) { - var isXYObject = function(obj) { - return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number'); - }; - - var objectToSimpleXYObject = function(objs) { - return objs.map(function(obj) { - try { - if (isXYObject(obj)) { - return { x: obj.x, y: obj.y }; - } else { - throw new Error(); - } - } catch(e) { - throw new Error('A Provided cropping polygon point is not supported'); - } - }); - }; - - try { - if (!$.isArray(polygons)) { - throw new Error('Provided cropping polygon is not an array'); - } - this._croppingPolygons = polygons.map(function(polygon){ - return objectToSimpleXYObject(polygon); - }); - this._needsDraw = true; - } catch (e) { - $.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported'); - $.console.error(e); - this.resetCroppingPolygons(); - } - }, - - /** - * Resets the cropping polygons, thus next render will remove all cropping - * polygon effects. - */ - resetCroppingPolygons: function() { - this._croppingPolygons = null; - this._needsDraw = true; - }, - - /** - * 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 + * You shouldn't have to create a TiledImage instance directly; get it asynchronously by + * using {@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. - * @fires OpenSeadragon.TiledImage.event:clip-change + * @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] - 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 {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}. + * @param {Boolean} [options.ajaxWithCredentials] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.loadTilesWithAjax] + * Whether to load tile data using AJAX requests. + * Defaults to the setting in {@link OpenSeadragon.Options}. + * @param {Object} [options.ajaxHeaders={}] + * A set of headers to include when making tile AJAX requests. */ - setClip: function(newClip) { - $.console.assert(!newClip || newClip instanceof $.Rect, - "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); - - if (newClip instanceof $.Rect) { - this._clip = newClip.clone(); - } else { - this._clip = null; - } - - this._needsDraw = true; + $.TiledImage = function( options ) { + this._initialized = false; /** - * Raised when the TiledImage's clip is changed. - * @event clip-change - * @memberOf OpenSeadragon.TiledImage - * @type {object} - * @property {OpenSeadragon.TiledImage} eventSource - A reference to the - * TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. + * The {@link OpenSeadragon.TileSource} that defines this TiledImage. + * @member {OpenSeadragon.TileSource} source + * @memberof OpenSeadragon.TiledImage# */ - this.raiseEvent('clip-change'); - }, + $.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"); - /** - * @returns {Boolean} Whether the TiledImage should be flipped before rendering. - */ - getFlip: function() { - return this.flipped; - }, + $.EventSource.call( this ); - /** - * @param {Boolean} flip Whether the TiledImage should be flipped before rendering. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - setFlip: function(flip) { - this.flipped = flip; - }, + this._tileCache = options.tileCache; + delete options.tileCache; - get flipped(){ - return this._flipped; - }, - set flipped(flipped){ - let changed = this._flipped !== !!flipped; - this._flipped = !!flipped; - if(changed){ - this.update(true); - this._needsDraw = true; - this._raiseBoundsChange(); - } - }, + this._drawer = options.drawer; + delete options.drawer; - get wrapHorizontal(){ - return this._wrapHorizontal; - }, - set wrapHorizontal(wrap){ - let changed = this._wrapHorizontal !== !!wrap; - this._wrapHorizontal = !!wrap; - if(this._initialized && changed){ - this.update(true); - this._needsDraw = true; - // this._raiseBoundsChange(); - } - }, + this._imageLoader = options.imageLoader; + delete options.imageLoader; - get wrapVertical(){ - return this._wrapVertical; - }, - set wrapVertical(wrap){ - let changed = this._wrapVertical !== !!wrap; - this._wrapVertical = !!wrap; - if(this._initialized && changed){ - this.update(true); - this._needsDraw = true; - // this._raiseBoundsChange(); - } - }, - - get debugMode(){ - return this._debugMode; - }, - set debugMode(debug){ - this._debugMode = !!debug; - 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. - * @fires OpenSeadragon.TiledImage.event:opacity-change - */ - setOpacity: function(opacity) { - this.opacity = opacity; - }, - - get opacity() { - return this._opacity; - }, - - set opacity(opacity) { - if (opacity === this.opacity) { - return; + if (options.clip instanceof $.Rect) { + this._clip = options.clip.clone(); } - this._opacity = opacity; - this._needsDraw = true; - /** - * Raised when the TiledImage's opacity is changed. - * @event opacity-change - * @memberOf OpenSeadragon.TiledImage - * @type {object} - * @property {Number} opacity - The new opacity value. - * @property {OpenSeadragon.TiledImage} eventSource - A reference to the - * TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent('opacity-change', { - opacity: this.opacity + 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; + + var degrees = options.degrees || 0; + delete options.degrees; + + var ajaxHeaders = options.ajaxHeaders; + delete options.ajaxHeaders; + + $.extend( true, this, { + + //internal state properties + viewer: null, + tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. + coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas have been drawn. + loadingCoverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas are loaded or are being loaded/blended. + lastDrawn: [], // An unordered list of Tiles drawn last frame. + lastResetTime: 0, // Last time for which the tiledImage was reset. + _needsDraw: true, // Does the tiledImage need to be drawn again? + _needsUpdate: true, // Does the tiledImage need to update the viewport again? + _hasOpaqueTile: false, // Do we have even one fully opaque tile? + _tilesLoading: 0, // The number of pending tile requests. + _tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile] + _lastDrawn: [], // array of tiles that were last fetched by the drawer + _isBlending: false, // Are any tiles still being blended? + _wasBlending: false, // Were any tiles blending before the last draw? + _isTainted: false, // Has a Tile been found with tainted data? + //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, + ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + preload: $.DEFAULT_SETTINGS.preload, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, + subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency, + maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame + }, options ); + + this._preload = this.preload; + delete this.preload; + + this._fullyLoaded = false; + + this._xSpring = new $.Spring({ + initial: x, + springStiffness: this.springStiffness, + animationTime: this.animationTime }); - }, - /** - * @returns {Boolean} whether the tiledImage can load its tiles even when it has zero opacity. - */ - getPreload: function() { - return this._preload; - }, + this._ySpring = new $.Spring({ + initial: y, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); - /** - * Set true to load even when hidden. Set false to block loading when hidden. - */ - setPreload: function(preload) { - this._preload = !!preload; - this._needsDraw = true; - }, + this._scaleSpring = new $.Spring({ + initial: scale, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); - /** - * Get the rotation of this tiled image in degrees. - * @param {Boolean} [current=false] True for current rotation, false for target. - * @returns {Number} the rotation of this tiled image in degrees. - */ - getRotation: function(current) { - return current ? - this._degreesSpring.current.value : - this._degreesSpring.target.value; - }, + this._degreesSpring = new $.Spring({ + initial: degrees, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); - /** - * Set the current rotation of this tiled image in degrees. - * @param {Number} degrees the rotation in degrees. - * @param {Boolean} [immediately=false] Whether to animate to the new angle - * or rotate immediately. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - setRotation: function(degrees, immediately) { - if (this._degreesSpring.target.value === degrees && - this._degreesSpring.isAtTargetValue()) { - return; + this._updateForScale(); + + if (fitBounds) { + this.fitBounds(fitBounds, fitBoundsPlacement, true); } - if (immediately) { - this._degreesSpring.resetTo(degrees); - } else { - this._degreesSpring.springTo(degrees); - } - this._needsDraw = true; - this._needsUpdate = true; - this._raiseBoundsChange(); - }, - /** - * Get the region of this tiled image that falls within the viewport. - * @returns {OpenSeadragon.Rect} the region of this tiled image that falls within the viewport. - * Returns false for images with opacity==0 unless preload==true - */ - getDrawArea: function(){ + this._ownAjaxHeaders = {}; + this.setAjaxHeaders(ajaxHeaders, false); + this._initialized = true; + }; + + $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + needsDraw: function() { + return this._needsDraw; + }, + + /** + * Mark the tiled image as needing to be (re)drawn + */ + redraw: function() { + this._needsDraw = true; + }, + + /** + * @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. Based on the new + * bounds, updates the levels and tiles to be drawn into the viewport. + * @param viewportChanged Whether the viewport changed meaning tiles need to be updated. + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + update: function(viewportChanged) { + let xUpdated = this._xSpring.update(); + let yUpdated = this._ySpring.update(); + let scaleUpdated = this._scaleSpring.update(); + let degreesUpdated = this._degreesSpring.update(); + + let updated = (xUpdated || yUpdated || scaleUpdated || degreesUpdated || this._needsUpdate); + + if (updated || viewportChanged || !this._fullyLoaded){ + let fullyLoadedFlag = this._updateLevelsForViewport(); + this._setFullyLoaded(fullyLoadedFlag); + } + + this._needsUpdate = false; + + if (updated) { + this._updateForScale(); + this._raiseBoundsChange(); + this._needsDraw = true; + return true; + } - if( this._opacity === 0 && !this._preload){ return false; - } + }, - var drawArea = this._viewportToTiledImageRectangle( - this.viewport.getBoundsWithMargins(true)); - - if (!this.wrapHorizontal && !this.wrapVertical) { - var tiledImageBounds = this._viewportToTiledImageRectangle( - this.getClippedBounds(true)); - drawArea = drawArea.intersection(tiledImageBounds); - } - - return drawArea; - }, - - /** - * - * @returns {Array} Array of Tiles that make up the current view - */ - getTilesToDraw: function(){ - // start with all the tiles added to this._tilesToDraw during the most recent - // call to this.update. Then update them so the blending and coverage properties - // are updated based on the current time - let tileArray = this._tilesToDraw.flat(); - - // update all tiles, which can change the coverage provided - this._updateTilesInViewport(tileArray); - - // _tilesToDraw might have been updated by the update; refresh it - tileArray = this._tilesToDraw.flat(); - - // mark the tiles as being drawn, so that they won't be discarded from - // the tileCache - tileArray.forEach(tileInfo => { - tileInfo.tile.beingDrawn = true; - }); - this._lastDrawn = tileArray; - return tileArray; - }, - - /** - * Get the point around which this tiled image is rotated - * @private - * @param {Boolean} current True for current rotation point, false for target. - * @returns {OpenSeadragon.Point} - */ - _getRotationPoint: function(current) { - return this.getBoundsNoRotate(current).getCenter(); - }, - - get compositeOperation(){ - return this._compositeOperation; - }, - - set compositeOperation(compositeOperation){ - - if (compositeOperation === this._compositeOperation) { - return; - } - this._compositeOperation = compositeOperation; - this._needsDraw = true; /** - * Raised when the TiledImage's opacity is changed. - * @event composite-operation-change - * @memberOf OpenSeadragon.TiledImage - * @type {object} - * @property {String} compositeOperation - The new compositeOperation value. - * @property {OpenSeadragon.TiledImage} eventSource - A reference to the - * TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. + * Mark this TiledImage as having been drawn, so that it will only be drawn + * again if something changes about the image. If the image is still blending, + * this will have no effect. + * @returns {Boolean} whether the item still needs to be drawn due to blending */ - this.raiseEvent('composite-operation-change', { - compositeOperation: this._compositeOperation - }); + setDrawn: function(){ + this._needsDraw = this._isBlending || this._wasBlending; + return this._needsDraw; + }, - }, + /** + * Set the internal _isTainted flag for this TiledImage. Lazy loaded - not + * checked each time a Tile is loaded, but can be set if a consumer of the + * tiles (e.g. a Drawer) discovers a Tile to have tainted data so that further + * checks are not needed and alternative rendering strategies can be used. + * @private + */ + setTainted(isTainted){ + this._isTainted = isTainted; + }, - /** - * @returns {String} The TiledImage's current compositeOperation. - */ - getCompositeOperation: function() { - return this._compositeOperation; - }, + /** + * @private + * @returns {Boolean} whether the TiledImage has been marked as tainted + */ + isTainted(){ + return this._isTainted; + }, - /** - * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. - * @fires OpenSeadragon.TiledImage.event:composite-operation-change - */ - setCompositeOperation: function(compositeOperation) { - this.compositeOperation = compositeOperation; //invokes setter - }, + /** + * Destroy the TiledImage (unload current loaded tiles). + */ + destroy: function() { + this.reset(); - /** - * Update headers to include when making AJAX requests. - * - * Unless `propagate` is set to false (which is likely only useful in rare circumstances), - * the updated headers are propagated to all tiles and queued image loader jobs. - * - * Note that the rules for merging headers still apply, i.e. headers returned by - * {@link OpenSeadragon.TileSource#getTileAjaxHeaders} take precedence over - * the headers here in the tiled image (`TiledImage.ajaxHeaders`). - * - * @function - * @param {Object} ajaxHeaders Updated AJAX headers, which will be merged over any headers specified in {@link OpenSeadragon.Options}. - * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. - */ - setAjaxHeaders: function(ajaxHeaders, propagate) { - if (ajaxHeaders === null) { - ajaxHeaders = {}; - } - if (!$.isPlainObject(ajaxHeaders)) { - console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); - return; - } + if (this.source.destroy) { + this.source.destroy(this.viewer); + } + }, - this._ownAjaxHeaders = ajaxHeaders; - this._updateAjaxHeaders(propagate); - }, + /** + * 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.getRotation(current), this._getRotationPoint(current)); + }, - /** - * Update headers to include when making AJAX requests. - * - * This function has the same effect as calling {@link OpenSeadragon.TiledImage#setAjaxHeaders}, - * except that the headers for this tiled image do not change. This is especially useful - * for propagating updated headers from {@link OpenSeadragon.TileSource#getTileAjaxHeaders} - * to existing tiles. - * - * @private - * @function - * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. - */ - _updateAjaxHeaders: function(propagate) { - if (propagate === undefined) { - propagate = true; - } + /** + * 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); + }, - // merge with viewer's headers - if ($.isPlainObject(this.viewer.ajaxHeaders)) { - this.ajaxHeaders = $.extend({}, this.viewer.ajaxHeaders, this._ownAjaxHeaders); - } else { - this.ajaxHeaders = this._ownAjaxHeaders; - } + // deprecated + getWorldBounds: function() { + $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); + return this.getBounds(); + }, - // propagate header updates to all tiles and queued image loader jobs - if (propagate) { - var numTiles, xMod, yMod, tile; + /** + * 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.getRotation(current), this._getRotationPoint(current)); + }, - for (var level in this.tilesMatrix) { - numTiles = this.source.getNumTiles(level); + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {OpenSeadragon.Rect} Where this tile fits (in normalized coordinates). + */ + getTileBounds: function( level, x, y ) { + var numTiles = this.source.getNumTiles(level); + var xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + var yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + var bounds = this.source.getTileBounds(level, xMod, yMod); + if (this.getFlip()) { + bounds.x = Math.max(0, 1 - bounds.x - bounds.width); + } + bounds.x += (x - xMod) / numTiles.x; + bounds.y += (this._worldHeightCurrent / this._worldWidthCurrent) * ((y - yMod) / numTiles.y); + return bounds; + }, - for (var x in this.tilesMatrix[level]) { - xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + /** + * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. + */ + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + }, - for (var y in this.tilesMatrix[level][x]) { - yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - tile = this.tilesMatrix[level][x][y]; + /** + * @returns {OpenSeadragon.Point} The TiledImage's content size, in window coordinates. + */ + 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); + }, - tile.loadWithAjax = this.loadTilesWithAjax; - if (tile.loadWithAjax) { - var tileAjaxHeaders = this.source.getTileAjaxHeaders( level, xMod, yMod ); - tile.ajaxHeaders = $.extend({}, this.ajaxHeaders, tileAjaxHeaders); + // 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. + * @returns {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.getRotation(current), 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. + * @returns {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, current); + 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.getRotation(current), 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. + * @returns {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.getRotation(current) + ); + }, + + /** + * 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. + * @returns {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.getRotation(current) + ); + }, + + /** + * 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 viewport coordinates to this tiled image point + // coordinates (x in [0, 1] and y in [0, aspectRatio]) + _viewportToTiledImageRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + rect = rect.rotate(-this.getRotation(true), 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; + this._needsUpdate = true; + } else { + if (sameTarget) { + return; + } + + this._xSpring.springTo(position.x); + this._ySpring.springTo(position.y); + this._needsDraw = true; + this._needsUpdate = 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); + }, + + /** + * Sets an array of polygons to crop the TiledImage during draw tiles. + * The render function will use the default non-zero winding rule. + * @param {OpenSeadragon.Point[][]} polygons - represented in an array of point object in image coordinates. + * Example format: [ + * [{x: 197, y:172}, {x: 226, y:172}, {x: 226, y:198}, {x: 197, y:198}], // First polygon + * [{x: 328, y:200}, {x: 330, y:199}, {x: 332, y:201}, {x: 329, y:202}] // Second polygon + * [{x: 321, y:201}, {x: 356, y:205}, {x: 341, y:250}] // Third polygon + * ] + */ + setCroppingPolygons: function( polygons ) { + var isXYObject = function(obj) { + return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number'); + }; + + var objectToSimpleXYObject = function(objs) { + return objs.map(function(obj) { + try { + if (isXYObject(obj)) { + return { x: obj.x, y: obj.y }; } else { - tile.ajaxHeaders = null; + throw new Error(); } + } catch(e) { + throw new Error('A Provided cropping polygon point is not supported'); + } + }); + }; + + try { + if (!$.isArray(polygons)) { + throw new Error('Provided cropping polygon is not an array'); + } + this._croppingPolygons = polygons.map(function(polygon){ + return objectToSimpleXYObject(polygon); + }); + this._needsDraw = true; + } catch (e) { + $.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported'); + $.console.error(e); + this.resetCroppingPolygons(); + } + }, + + /** + * Resets the cropping polygons, thus next render will remove all cropping + * polygon effects. + */ + resetCroppingPolygons: function() { + this._croppingPolygons = null; + this._needsDraw = true; + }, + + /** + * 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. + * @fires OpenSeadragon.TiledImage.event:clip-change + */ + setClip: function(newClip) { + $.console.assert(!newClip || newClip instanceof $.Rect, + "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); + + if (newClip instanceof $.Rect) { + this._clip = newClip.clone(); + } else { + this._clip = null; + } + + this._needsDraw = true; + /** + * Raised when the TiledImage's clip is changed. + * @event clip-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('clip-change'); + }, + + /** + * @returns {Boolean} Whether the TiledImage should be flipped before rendering. + */ + getFlip: function() { + return this.flipped; + }, + + /** + * @param {Boolean} flip Whether the TiledImage should be flipped before rendering. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setFlip: function(flip) { + this.flipped = flip; + }, + + get flipped(){ + return this._flipped; + }, + set flipped(flipped){ + let changed = this._flipped !== !!flipped; + this._flipped = !!flipped; + if(changed){ + this.update(true); + this._needsDraw = true; + this._raiseBoundsChange(); + } + }, + + get wrapHorizontal(){ + return this._wrapHorizontal; + }, + set wrapHorizontal(wrap){ + let changed = this._wrapHorizontal !== !!wrap; + this._wrapHorizontal = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } + }, + + get wrapVertical(){ + return this._wrapVertical; + }, + set wrapVertical(wrap){ + let changed = this._wrapVertical !== !!wrap; + this._wrapVertical = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } + }, + + get debugMode(){ + return this._debugMode; + }, + set debugMode(debug){ + this._debugMode = !!debug; + 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. + * @fires OpenSeadragon.TiledImage.event:opacity-change + */ + setOpacity: function(opacity) { + this.opacity = opacity; + }, + + get opacity() { + return this._opacity; + }, + + set opacity(opacity) { + if (opacity === this.opacity) { + return; + } + + this._opacity = opacity; + this._needsDraw = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event opacity-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {Number} opacity - The new opacity value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('opacity-change', { + opacity: this.opacity + }); + }, + + /** + * @returns {Boolean} whether the tiledImage can load its tiles even when it has zero opacity. + */ + getPreload: function() { + return this._preload; + }, + + /** + * Set true to load even when hidden. Set false to block loading when hidden. + */ + setPreload: function(preload) { + this._preload = !!preload; + this._needsDraw = true; + }, + + /** + * Get the rotation of this tiled image in degrees. + * @param {Boolean} [current=false] True for current rotation, false for target. + * @returns {Number} the rotation of this tiled image in degrees. + */ + getRotation: function(current) { + return current ? + this._degreesSpring.current.value : + this._degreesSpring.target.value; + }, + + /** + * Set the current rotation of this tiled image in degrees. + * @param {Number} degrees the rotation in degrees. + * @param {Boolean} [immediately=false] Whether to animate to the new angle + * or rotate immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setRotation: function(degrees, immediately) { + if (this._degreesSpring.target.value === degrees && + this._degreesSpring.isAtTargetValue()) { + return; + } + if (immediately) { + this._degreesSpring.resetTo(degrees); + } else { + this._degreesSpring.springTo(degrees); + } + this._needsDraw = true; + this._needsUpdate = true; + this._raiseBoundsChange(); + }, + + /** + * Get the region of this tiled image that falls within the viewport. + * @returns {OpenSeadragon.Rect} the region of this tiled image that falls within the viewport. + * Returns false for images with opacity==0 unless preload==true + */ + getDrawArea: function(){ + + if( this._opacity === 0 && !this._preload){ + return false; + } + + var drawArea = this._viewportToTiledImageRectangle( + this.viewport.getBoundsWithMargins(true)); + + if (!this.wrapHorizontal && !this.wrapVertical) { + var tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(true)); + drawArea = drawArea.intersection(tiledImageBounds); + } + + return drawArea; + }, + + /** + * + * @returns {Array} Array of Tiles that make up the current view + */ + getTilesToDraw: function(){ + // start with all the tiles added to this._tilesToDraw during the most recent + // call to this.update. Then update them so the blending and coverage properties + // are updated based on the current time + let tileArray = this._tilesToDraw.flat(); + + // update all tiles, which can change the coverage provided + this._updateTilesInViewport(tileArray); + + // _tilesToDraw might have been updated by the update; refresh it + tileArray = this._tilesToDraw.flat(); + + // mark the tiles as being drawn, so that they won't be discarded from + // the tileCache + tileArray.forEach(tileInfo => { + tileInfo.tile.beingDrawn = true; + }); + this._lastDrawn = tileArray; + return tileArray; + }, + + /** + * Get the point around which this tiled image is rotated + * @private + * @param {Boolean} current True for current rotation point, false for target. + * @returns {OpenSeadragon.Point} + */ + _getRotationPoint: function(current) { + return this.getBoundsNoRotate(current).getCenter(); + }, + + get compositeOperation(){ + return this._compositeOperation; + }, + + set compositeOperation(compositeOperation){ + + if (compositeOperation === this._compositeOperation) { + return; + } + this._compositeOperation = compositeOperation; + this._needsDraw = true; + /** + * Raised when the TiledImage's opacity is changed. + * @event composite-operation-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {String} compositeOperation - The new compositeOperation value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('composite-operation-change', { + compositeOperation: this._compositeOperation + }); + + }, + + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this._compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + * @fires OpenSeadragon.TiledImage.event:composite-operation-change + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; //invokes setter + }, + + /** + * Update headers to include when making AJAX requests. + * + * Unless `propagate` is set to false (which is likely only useful in rare circumstances), + * the updated headers are propagated to all tiles and queued image loader jobs. + * + * Note that the rules for merging headers still apply, i.e. headers returned by + * {@link OpenSeadragon.TileSource#getTileAjaxHeaders} take precedence over + * the headers here in the tiled image (`TiledImage.ajaxHeaders`). + * + * @function + * @param {Object} ajaxHeaders Updated AJAX headers, which will be merged over any headers specified in {@link OpenSeadragon.Options}. + * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. + */ + setAjaxHeaders: function(ajaxHeaders, propagate) { + if (ajaxHeaders === null) { + ajaxHeaders = {}; + } + if (!$.isPlainObject(ajaxHeaders)) { + console.error('[TiledImage.setAjaxHeaders] Ignoring invalid headers, must be a plain object'); + return; + } + + this._ownAjaxHeaders = ajaxHeaders; + this._updateAjaxHeaders(propagate); + }, + + /** + * Update headers to include when making AJAX requests. + * + * This function has the same effect as calling {@link OpenSeadragon.TiledImage#setAjaxHeaders}, + * except that the headers for this tiled image do not change. This is especially useful + * for propagating updated headers from {@link OpenSeadragon.TileSource#getTileAjaxHeaders} + * to existing tiles. + * + * @private + * @function + * @param {Boolean} [propagate=true] Whether to propagate updated headers to existing tiles and queued image loader jobs. + */ + _updateAjaxHeaders: function(propagate) { + if (propagate === undefined) { + propagate = true; + } + + // merge with viewer's headers + if ($.isPlainObject(this.viewer.ajaxHeaders)) { + this.ajaxHeaders = $.extend({}, this.viewer.ajaxHeaders, this._ownAjaxHeaders); + } else { + this.ajaxHeaders = this._ownAjaxHeaders; + } + + // propagate header updates to all tiles and queued image loader jobs + if (propagate) { + var numTiles, xMod, yMod, tile; + + for (var level in this.tilesMatrix) { + numTiles = this.source.getNumTiles(level); + + for (var x in this.tilesMatrix[level]) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + + for (var y in this.tilesMatrix[level][x]) { + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + tile = this.tilesMatrix[level][x][y]; + + tile.loadWithAjax = this.loadTilesWithAjax; + if (tile.loadWithAjax) { + var tileAjaxHeaders = this.source.getTileAjaxHeaders( level, xMod, yMod ); + tile.ajaxHeaders = $.extend({}, this.ajaxHeaders, tileAjaxHeaders); + } else { + tile.ajaxHeaders = null; + } + } + } + } + + for (var i = 0; i < this._imageLoader.jobQueue.length; i++) { + var job = this._imageLoader.jobQueue[i]; + job.loadWithAjax = job.tile.loadWithAjax; + job.ajaxHeaders = job.tile.loadWithAjax ? job.tile.ajaxHeaders : null; + } + } + }, + + // 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; + this._needsUpdate = true; + } else { + if (sameTarget) { + return; + } + + this._scaleSpring.springTo(scale); + this._updateForScale(); + this._needsDraw = true; + this._needsUpdate = 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.TiledImage} 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 + highestLevel = Math.max(highestLevel, this.source.minLevel || 0); + lowestLevel = Math.min(lowestLevel, highestLevel); + return { + lowestLevel: lowestLevel, + highestLevel: highestLevel + }; + }, + + // returns boolean flag of whether the image should be marked as fully loaded + _updateLevelsForViewport: function(){ + var levelsInterval = this._getLevelsInterval(); + var lowestLevel = levelsInterval.lowestLevel; // the lowest level we should draw at our current zoom + var highestLevel = levelsInterval.highestLevel; // the highest level we should draw at our current zoom + var bestTiles = []; + var drawArea = this.getDrawArea(); + var currentTime = $.now(); + + // reset each tile's beingDrawn flag + this._lastDrawn.forEach(tileinfo => { + tileinfo.tile.beingDrawn = false; + }); + // clear the list of tiles to draw + this._tilesToDraw = []; + this._tilesLoading = 0; + this.loadingCoverage = {}; + + if(!drawArea){ + this._needsDraw = false; + return this._fullyLoaded; + } + + // make a list of levels to use for the current zoom level + var levelList = new Array(highestLevel - lowestLevel + 1); + // go from highest to lowest resolution + for(let i = 0, level = highestLevel; level >= lowestLevel; level--, i++){ + levelList[i] = level; + } + + // if a single-tile level is loaded, add that to the end of the list + // as a fallback to use during zooming out, until a lower-res tile is + // loaded + for(let level = highestLevel + 1; level <= this.source.maxLevel; level++){ + var tile = ( + this.tilesMatrix[level] && + this.tilesMatrix[level][0] && + this.tilesMatrix[level][0][0] + ); + if(tile && tile.isBottomMost && tile.isRightMost && tile.loaded){ + levelList.push(level); + break; + } + } + + + // Update any level that will be drawn. + // We are iterating from highest resolution to lowest resolution + // Once a level fully covers the viewport the loop is halted and + // lower-resolution levels are skipped + for (let i = 0; i < levelList.length; i++) { + let level = levelList[i]; + + var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + true + ).x * this._scaleSpring.current.value; + + var targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + false + ).x * this._scaleSpring.current.value; + + var targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio( + Math.max( + this.source.getClosestLevel(), + 0 + ) + ), + false + ).x * this._scaleSpring.current.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' tiles to load + var result = this._updateLevel( + level, + levelOpacity, + levelVisibility, + drawArea, + currentTime, + bestTiles + ); + + bestTiles = result.bestTiles; + var tiles = result.updatedTiles.filter(tile => tile.loaded); + var makeTileInfoObject = (function(level, levelOpacity, currentTime){ + return function(tile){ + return { + tile: tile, + level: level, + levelOpacity: levelOpacity, + currentTime: currentTime + }; + }; + })(level, levelOpacity, currentTime); + + this._tilesToDraw[level] = tiles.map(makeTileInfoObject); + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if (this._providesCoverage(this.coverage, level)) { + break; + } + } + + + // Load the new 'best' n tiles + if (bestTiles && bestTiles.length > 0) { + bestTiles.forEach(function (tile) { + if (tile && !tile.context2D) { + this._loadTile(tile, currentTime); + } + }, this); + + this._needsDraw = true; + return false; + } else { + return this._tilesLoading === 0; + } + + // Update + + }, + + /** + * Update all tiles that contribute to the current view + * @private + * + */ + _updateTilesInViewport: function(tiles) { + let currentTime = $.now(); + let _this = this; + this._tilesLoading = 0; + this._wasBlending = this._isBlending; + this._isBlending = false; + this.loadingCoverage = {}; + let lowestLevel = tiles.length ? tiles[0].level : 0; + + let drawArea = this.getDrawArea(); + if(!drawArea){ + return; + } + + function updateTile(info){ + let tile = info.tile; + if(tile && tile.loaded){ + let tileIsBlending = _this._blendTile( + tile, + tile.x, + tile.y, + info.level, + info.levelOpacity, + currentTime, + lowestLevel + ); + _this._isBlending = _this._isBlending || tileIsBlending; + _this._needsDraw = _this._needsDraw || tileIsBlending || _this._wasBlending; + } + } + + // Update each tile in the list of tiles. As the tiles are updated, + // the coverage provided is also updated. If a level provides coverage + // as part of this process, discard tiles from lower levels + let level = 0; + for(let i = 0; i < tiles.length; i++){ + let tile = tiles[i]; + updateTile(tile); + if(this._providesCoverage(this.coverage, tile.level)){ + level = Math.max(level, tile.level); + } + } + if(level > 0){ + for( let levelKey in this._tilesToDraw ){ + if( levelKey < level ){ + delete this._tilesToDraw[levelKey]; } } } - for (var i = 0; i < this._imageLoader.jobQueue.length; i++) { - var job = this._imageLoader.jobQueue[i]; - job.loadWithAjax = job.tile.loadWithAjax; - job.ajaxHeaders = job.tile.loadWithAjax ? job.tile.ajaxHeaders : null; - } - } - }, + }, - // 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; - this._needsUpdate = true; - } else { - if (sameTarget) { - return; - } - - this._scaleSpring.springTo(scale); - this._updateForScale(); - this._needsDraw = true; - this._needsUpdate = 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.TiledImage} eventSource - A reference to the - * TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. + * 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. + * @private + * + * @param {OpenSeadragon.Tile} tile + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} currentTime + * @param {Boolean} lowestLevel + * @returns {Boolean} true if blending did not yet finish */ - this.raiseEvent('bounds-change'); - }, + _blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){ + let blendTimeMillis = 1000 * this.blendTime, + deltaTime, + opacity; - // 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 - highestLevel = Math.max(highestLevel, this.source.minLevel || 0); - lowestLevel = Math.min(lowestLevel, highestLevel); - return { - lowestLevel: lowestLevel, - highestLevel: highestLevel - }; - }, - - // returns boolean flag of whether the image should be marked as fully loaded - _updateLevelsForViewport: function(){ - var levelsInterval = this._getLevelsInterval(); - var lowestLevel = levelsInterval.lowestLevel; - var highestLevel = levelsInterval.highestLevel; - var bestTiles = []; - var haveDrawn = false; - var drawArea = this.getDrawArea(); - var currentTime = $.now(); - - // reset each tile's beingDrawn flag - this._lastDrawn.forEach(tileinfo => { - tileinfo.tile.beingDrawn = false; - }); - // clear the list of tiles to draw - this._tilesToDraw = []; - this._tilesLoading = 0; - this.loadingCoverage = {}; - - if(!drawArea){ - this._needsDraw = false; - return this._fullyLoaded; - } - - // make a list of levels to use for the current zoom level - var levelList = new Array(highestLevel - lowestLevel + 1); - // go from highest to lowest resolution - for(let i = 0, level = highestLevel; level >= lowestLevel; level--, i++){ - levelList[i] = level; - } - // if a single-tile level is loaded, add that to the end of the list - // as a fallback to use during zooming out, until a lower-res tile is - // loaded - for(let level = highestLevel + 1; level <= this.source.maxLevel; level++){ - var tile = ( - this.tilesMatrix[level] && - this.tilesMatrix[level][0] && - this.tilesMatrix[level][0][0] - ); - if(tile && tile.isBottomMost && tile.isRightMost && tile.loaded){ - levelList.push(level); - levelList.hasHigherResolutionFallback = true; - break; - } - } - - - // Update any level that will be drawn - for (let i = 0; i < levelList.length; i++) { - let level = levelList[i]; - var drawLevel = false; - - //Avoid calculations for draw if we have already drawn this - var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio(level), - true - ).x * this._scaleSpring.current.value; - - if (i === levelList.length - 1 || - (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio) ) { - drawLevel = true; - haveDrawn = true; - } else if (!haveDrawn) { - continue; + if ( !tile.blendStart ) { + tile.blendStart = currentTime; } - //Perform calculations for draw if we haven't drawn this - var targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio(level), - false - ).x * this._scaleSpring.current.value; + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; - var targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio( - Math.max( - this.source.getClosestLevel(), - 0 - ) - ), - false - ).x * this._scaleSpring.current.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' tiles to load - // the bestTiles - var result = this._updateLevel( - haveDrawn, - drawLevel, - level, - levelOpacity, - levelVisibility, - drawArea, - currentTime, - bestTiles - ); - - bestTiles = result.bestTiles; - var tiles = result.updatedTiles.filter(tile => tile.loaded); - var makeTileInfoObject = (function(level, levelOpacity, currentTime){ - return function(tile){ - return { - tile: tile, - level: level, - levelOpacity: levelOpacity, - currentTime: currentTime - }; - }; - })(level, levelOpacity, currentTime); - - this._tilesToDraw[level] = tiles.map(makeTileInfoObject); - - // Stop the loop if lower-res tiles would all be covered by - // already drawn tiles - if (this._providesCoverage(this.coverage, level)) { - break; - } - } - - - // Load the new 'best' n tiles - if (bestTiles && bestTiles.length > 0) { - bestTiles.forEach(function (tile) { - if (tile && !tile.context2D) { - this._loadTile(tile, currentTime); - } - }, this); - - this._needsDraw = true; - return false; - } else { - return this._tilesLoading === 0; - } - - // Update - - }, - - /** - * Update all tiles that contribute to the current view - * @private - * - */ - _updateTilesInViewport: function(tiles) { - let currentTime = $.now(); - let _this = this; - this._tilesLoading = 0; - this._wasBlending = this._isBlending; - this._isBlending = false; - this.loadingCoverage = {}; - let lowestLevel = tiles.length ? tiles[0].level : 0; - - let drawArea = this.getDrawArea(); - if(!drawArea){ - return; - } - - function updateTile(info){ - let tile = info.tile; - if(tile && tile.loaded){ - let tileIsBlending = _this._blendTile( - tile, - tile.x, - tile.y, - info.level, - info.levelOpacity, - currentTime, - lowestLevel - ); - _this._isBlending = _this._isBlending || tileIsBlending; - _this._needsDraw = _this._needsDraw || tileIsBlending || _this._wasBlending; - } - } - - // Update each tile in the list of tiles. As the tiles are updated, - // the coverage provided is also updated. If a level provides coverage - // as part of this process, discard tiles from lower levels - let level = 0; - for(let i = 0; i < tiles.length; i++){ - let tile = tiles[i]; - updateTile(tile); - if(this._providesCoverage(this.coverage, tile.level)){ - level = Math.max(level, tile.level); - } - } - if(level > 0){ - for( let levelKey in this._tilesToDraw ){ - if( levelKey < level ){ - delete this._tilesToDraw[levelKey]; - } - } - } - - }, - - /** - * 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. - * @private - * - * @param {OpenSeadragon.Tile} tile - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {Number} levelOpacity - * @param {Number} currentTime - * @param {Boolean} lowestLevel - * @returns {Boolean} true if blending did not yet finish - */ - _blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){ - let 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 tile is at the lowest level being drawn, render at opacity=1 - if(level === lowestLevel){ - opacity = 1; - deltaTime = blendTimeMillis; - } - - if ( this.alwaysBlend ) { - opacity *= levelOpacity; - } - tile.opacity = opacity; - - if ( opacity === 1 ) { - this._setCoverage( this.coverage, level, x, y, true ); - this._hasOpaqueTile = true; - } - // return true if the tile is still blending - return deltaTime < blendTimeMillis; - }, - - /** - * Updates all tiles at a given resolution level. - * @private - * @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 Array of the current best tiles - * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile - the current "best" tiles to draw, updatedTiles: OpenSeadragon.Tile) - the updated tiles}. - */ - _updateLevel: function(haveDrawn, drawLevel, level, levelOpacity, - levelVisibility, drawArea, currentTime, best) { - - var topLeftBound = drawArea.getBoundingBox().getTopLeft(); - var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); - - 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 - }); - } - - this._resetCoverage(this.coverage, level); - this._resetCoverage(this.loadingCoverage, level); - - //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 viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter()); - - 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); - } - } - var numTiles = Math.max(0, (bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y)); - var tiles = new Array(numTiles); - var tileIndex = 0; - 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; - } - - var result = this._updateTile( - haveDrawn, - drawLevel, - flippedX, y, - level, - levelVisibility, - viewportCenter, - numberOfTiles, - currentTime, - best - ); - best = result.bestTiles; - tiles[tileIndex] = result.tile; - tileIndex += 1; - } - } - - return { - bestTiles: best, - updatedTiles: tiles - }; - }, - - /** - * @private - * @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; - - tile.positionedBounds.x = boundsTL.x; - tile.positionedBounds.y = boundsTL.y; - tile.positionedBounds.width = boundsSize.x; - tile.positionedBounds.height = boundsSize.y; - - 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(this.viewer.drawer.minimumOverlapRequired()){ - if ( !overlap ) { - sizeC = sizeC.plus( new $.Point(1, 1)); + // if this tile is at the lowest level being drawn, render at opacity=1 + if(level === lowestLevel){ + opacity = 1; + deltaTime = blendTimeMillis; } - if (tile.isRightMost && this.wrapHorizontal) { - sizeC.x += 0.75; // Otherwise Firefox and Safari show seams + if ( this.alwaysBlend ) { + opacity *= levelOpacity; } + tile.opacity = opacity; - 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; - }, - - /** - * Update a single tile at a particular resolution level. - * @private - * @param {Boolean} haveDrawn - * @param {Boolean} drawLevel - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {Number} levelVisibility - * @param {OpenSeadragon.Point} viewportCenter - * @param {Number} numberOfTiles - * @param {Number} currentTime - * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. - * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile[] - the current best tiles, tile: OpenSeadragon.Tile the current tile} - */ - _updateTile: function( haveDrawn, drawLevel, x, y, level, - levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ - - var tile = this._getTile( - x, y, - level, - currentTime, - numberOfTiles - ), - 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 - }); - } - - this._setCoverage( this.coverage, level, x, y, false ); - - var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y); - this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage); - - if ( !tile.exists ) { - return { - bestTiles: best, - tile: tile - }; - } - if (tile.loaded && tile.opacity === 1){ - this._setCoverage( this.coverage, level, x, y, true ); - } - if ( drawTile && !haveDrawn ) { - if ( this._isCovered( this.coverage, level, x, y ) ) { + if ( opacity === 1 ) { this._setCoverage( this.coverage, level, x, y, true ); - } else { - haveDrawn = true; + this._hasOpaqueTile = true; } - } + // return true if the tile is still blending + return deltaTime < blendTimeMillis; + }, - if ( !haveDrawn ) { - return { - bestTiles: best, - tile: tile - }; - } + /** + * Updates all tiles at a given resolution level. + * @private + * @param {Number} level + * @param {Number} levelOpacity + * @param {Number} levelVisibility + * @param {OpenSeadragon.Rect} drawArea + * @param {Number} currentTime + * @param {OpenSeadragon.Tile[]} best Array of the current best tiles + * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile - the current "best" tiles to draw, updatedTiles: OpenSeadragon.Tile) - the updated tiles}. + */ + _updateLevel: function(level, levelOpacity, + levelVisibility, drawArea, currentTime, best) { - this._positionTile( - tile, - this.source.tileOverlap, - this.viewport, - viewportCenter, - levelVisibility - ); + var topLeftBound = drawArea.getBoundingBox().getTopLeft(); + var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); - if (!tile.loaded) { - if (tile.context2D) { - this._setTileLoaded(tile); - } else { - var imageRecord = this._tileCache.getImageRecord(tile.cacheKey); - if (imageRecord) { - this._setTileLoaded(tile, imageRecord.getData()); - } - } - } - - if ( tile.loading ) { - // the tile is already in the download queue - this._tilesLoading++; - } else if (!loadingCoverage) { - best = this._compareTiles( best, tile, this.maxTilesPerFrame ); - } - - return { - bestTiles: best, - tile: tile - }; - }, - - // private - _getCornerTiles: function(level, topLeftBound, bottomRightBound) { - var leftX; - var rightX; - if (this.wrapHorizontal) { - leftX = $.positiveModulo(topLeftBound.x, 1); - rightX = $.positiveModulo(bottomRightBound.x, 1); - } else { - leftX = Math.max(0, topLeftBound.x); - rightX = Math.min(1, bottomRightBound.x); - } - var topY; - var bottomY; - var aspectRatio = 1 / this.source.aspectRatio; - if (this.wrapVertical) { - topY = $.positiveModulo(topLeftBound.y, aspectRatio); - bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio); - } else { - topY = Math.max(0, topLeftBound.y); - bottomY = Math.min(aspectRatio, bottomRightBound.y); - } - - var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY)); - var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY)); - var numTiles = this.source.getNumTiles(level); - - if (this.wrapHorizontal) { - topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x); - bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x); - } - if (this.wrapVertical) { - topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio); - bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio); - } - - return { - topLeft: topLeftTile, - bottomRight: bottomRightTile, - }; - }, - - /** - * Obtains a tile at the given location. - * @private - * @param {Number} x - * @param {Number} y - * @param {Number} level - * @param {Number} time - * @param {Number} numTiles - * @returns {OpenSeadragon.Tile} - */ - _getTile: function( - x, y, - level, - time, - numTiles - ) { - var xMod, - yMod, - bounds, - sourceBounds, - exists, - urlOrGetter, - 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 ); - urlOrGetter = 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, - urlOrGetter, - context2D, - this.loadTilesWithAjax, - ajaxHeaders, - sourceBounds, - post, - tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, 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; - }, - - /** - * Dispatch a job to the ImageLoader to load the Image for a Tile. - * @private - * @param {OpenSeadragon.Tile} tile - * @param {Number} time - */ - _loadTile: function(tile, time ) { - var _this = this; - tile.loading = true; - this._imageLoader.addJob({ - src: tile.getUrl(), - tile: tile, - source: this.source, - postData: tile.postData, - loadWithAjax: tile.loadWithAjax, - ajaxHeaders: tile.ajaxHeaders, - crossOriginPolicy: this.crossOriginPolicy, - ajaxWithCredentials: this.ajaxWithCredentials, - callback: function( data, errorMsg, tileRequest ){ - _this._onTileLoad( tile, time, data, errorMsg, tileRequest ); - }, - abort: function() { - tile.loading = false; - } - }); - }, - - /** - * Callback fired when a Tile's Image finished downloading. - * @private - * @param {OpenSeadragon.Tile} tile - * @param {Number} time - * @param {*} data image data - * @param {String} errorMsg - * @param {XMLHttpRequest} tileRequest - */ - _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) { - if ( !data ) { - $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), 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; - } else { - tile.exists = true; - } - - if ( time < this.lastResetTime ) { - $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() ); - tile.loading = false; - return; - } - - var _this = this, - finish = function() { - var ccc = _this.source; - var cutoff = ccc.getClosestLevel(); - _this._setTileLoaded(tile, data, cutoff, tileRequest); - }; - - - finish(); - }, - - /** - * @private - * @param {OpenSeadragon.Tile} tile - * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object - * @param {Number|undefined} cutoff - * @param {XMLHttpRequest|undefined} tileRequest - */ - _setTileLoaded: function(tile, data, cutoff, tileRequest) { - var increment = 0, - eventFinished = false, - _this = this; - - function getCompletionCallback() { - if (eventFinished) { - $.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " + - "Its return value should be called asynchronously."); - } - increment++; - return completionCallback; - } - - function completionCallback() { - increment--; - if (increment === 0) { - tile.loading = false; - tile.loaded = true; - tile.hasTransparency = _this.source.hasTransparency( - tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData - ); - if (!tile.context2D) { - _this._tileCache.cacheTile({ - data: data, - tile: tile, - cutoff: cutoff, - tiledImage: _this - }); - } + if (this.viewer) { /** - * Triggered when a tile is loaded and pre-processing is compelete, - * and the tile is ready to draw. + * - Needs documentation - * - * @event tile-ready + * @event update-level * @memberof OpenSeadragon.Viewer * @type {object} - * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. - * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. - * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). + * @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 - deprecated, always true (kept for backwards compatibility) + * @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("tile-ready", { - tile: tile, - tiledImage: _this, - tileRequest: tileRequest + this.viewer.raiseEvent('update-level', { + tiledImage: this, + havedrawn: true, // deprecated, kept for backwards compatibility + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + drawArea: drawArea, + topleft: topLeftBound, + bottomright: bottomRightBound, + currenttime: currentTime, + best: best }); - _this._needsDraw = true; } - } + + this._resetCoverage(this.coverage, level); + this._resetCoverage(this.loadingCoverage, level); + + //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 viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter()); + + 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); + } + } + var numTiles = Math.max(0, (bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y)); + var tiles = new Array(numTiles); + var tileIndex = 0; + 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; + } + + var result = this._updateTile( + flippedX, y, + level, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + best = result.bestTiles; + tiles[tileIndex] = result.tile; + tileIndex += 1; + } + } + + return { + bestTiles: best, + updatedTiles: tiles + }; + }, /** - * 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 (data) of the tile. Deprecated. - * @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object - * @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 + * @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(); - var fallbackCompletion = getCompletionCallback(); - this.viewer.raiseEvent("tile-loaded", { - tile: tile, - tiledImage: this, - tileRequest: tileRequest, - get image() { - $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead."); - return data; - }, - data: data, - getCompletionCallback: getCompletionCallback - }); - eventFinished = true; - // In case the completion callback is never called, we at least force it once. - fallbackCompletion(); - }, + 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(); - /** - * Determines the 'best tiles' from the given 'last best' tiles and the - * tile in question. - * @private - * - * @param {OpenSeadragon.Tile[]} previousBest The best tiles so far. - * @param {OpenSeadragon.Tile} tile The new tile to consider. - * @param {Number} maxNTiles The max number of best tiles. - * @returns {OpenSeadragon.Tile[]} The new best tiles. - */ - _compareTiles: function( previousBest, tile, maxNTiles ) { - if ( !previousBest ) { - return [tile]; - } - previousBest.push(tile); - this._sortTiles(previousBest); - if (previousBest.length > maxNTiles) { - previousBest.pop(); - } - return previousBest; - }, + boundsSize.x *= this._scaleSpring.current.value; + boundsSize.y *= this._scaleSpring.current.value; - /** - * Sorts tiles in an array according to distance and visibility. - * @private - * - * @param {OpenSeadragon.Tile[]} tiles The tiles. - */ - _sortTiles: function( tiles ) { - tiles.sort(function (a, b) { - if (a === null) { - return 1; + tile.positionedBounds.x = boundsTL.x; + tile.positionedBounds.y = boundsTL.y; + tile.positionedBounds.width = boundsSize.x; + tile.positionedBounds.height = boundsSize.y; + + 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(this.viewer.drawer.minimumOverlapRequired()){ + 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 + } } - if (b === null) { - return -1; + + tile.position = positionC; + tile.size = sizeC; + tile.squaredDistance = tileSquaredDistance; + tile.visibility = levelVisibility; + }, + + /** + * Update a single tile at a particular resolution level. + * @private + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} levelVisibility + * @param {OpenSeadragon.Point} viewportCenter + * @param {Number} numberOfTiles + * @param {Number} currentTime + * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. + * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile[] - the current best tiles, tile: OpenSeadragon.Tile the current tile} + */ + _updateTile: function( x, y, level, + levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = this._getTile( + x, y, + level, + currentTime, + numberOfTiles + ); + + 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 + }); } - if (a.visibility === b.visibility) { - // sort by smallest squared distance - return (a.squaredDistance - b.squaredDistance); - } else { - // sort by largest visibility value - return (b.visibility - a.visibility); + + this._setCoverage( this.coverage, level, x, y, false ); + + var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y); + this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage); + + if ( !tile.exists ) { + return { + bestTiles: best, + tile: tile + }; + } + if (tile.loaded && tile.opacity === 1){ + this._setCoverage( this.coverage, level, x, y, true ); } - }); - }, + this._positionTile( + tile, + this.source.tileOverlap, + this.viewport, + viewportCenter, + levelVisibility + ); - /** - * 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. - * @private - * - * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. - * @param {Number} level - The resolution level of the tile. - * @param {Number} x - The X position of the tile. - * @param {Number} y - The Y position of the tile. - * @returns {Boolean} - */ - _providesCoverage: function( 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 ( Object.prototype.hasOwnProperty.call( rows, i ) ) { - cols = rows[ i ]; - for ( j in cols ) { - if ( Object.prototype.hasOwnProperty.call( cols, j ) && !cols[ j ] ) { - return false; - } + if (!tile.loaded) { + if (tile.context2D) { + this._setTileLoaded(tile); + } else { + var imageRecord = this._tileCache.getImageRecord(tile.cacheKey); + if (imageRecord) { + this._setTileLoaded(tile, imageRecord.getData()); } } } - return true; - } + if ( tile.loading ) { + // the tile is already in the download queue + this._tilesLoading++; + } else if (!loadingCoverage) { + best = this._compareTiles( best, tile, this.maxTilesPerFrame ); + } - return ( - coverage[ level ][ x] === undefined || - coverage[ level ][ x ][ y ] === undefined || - coverage[ level ][ x ][ y ] === true - ); - }, + return { + bestTiles: best, + tile: tile + }; + }, + + // private + _getCornerTiles: function(level, topLeftBound, bottomRightBound) { + var leftX; + var rightX; + if (this.wrapHorizontal) { + leftX = $.positiveModulo(topLeftBound.x, 1); + rightX = $.positiveModulo(bottomRightBound.x, 1); + } else { + leftX = Math.max(0, topLeftBound.x); + rightX = Math.min(1, bottomRightBound.x); + } + var topY; + var bottomY; + var aspectRatio = 1 / this.source.aspectRatio; + if (this.wrapVertical) { + topY = $.positiveModulo(topLeftBound.y, aspectRatio); + bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio); + } else { + topY = Math.max(0, topLeftBound.y); + bottomY = Math.min(aspectRatio, bottomRightBound.y); + } + + var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY)); + var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY)); + var numTiles = this.source.getNumTiles(level); + + if (this.wrapHorizontal) { + topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x); + bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x); + } + if (this.wrapVertical) { + topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio); + bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio); + } + + return { + topLeft: topLeftTile, + bottomRight: bottomRightTile, + }; + }, + + /** + * Obtains a tile at the given location. + * @private + * @param {Number} x + * @param {Number} y + * @param {Number} level + * @param {Number} time + * @param {Number} numTiles + * @returns {OpenSeadragon.Tile} + */ + _getTile: function( + x, y, + level, + time, + numTiles + ) { + var xMod, + yMod, + bounds, + sourceBounds, + exists, + urlOrGetter, + 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 ); + urlOrGetter = 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, + urlOrGetter, + context2D, + this.loadTilesWithAjax, + ajaxHeaders, + sourceBounds, + post, + tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, 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; + }, + + /** + * Dispatch a job to the ImageLoader to load the Image for a Tile. + * @private + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + */ + _loadTile: function(tile, time ) { + var _this = this; + tile.loading = true; + this._imageLoader.addJob({ + src: tile.getUrl(), + tile: tile, + source: this.source, + postData: tile.postData, + loadWithAjax: tile.loadWithAjax, + ajaxHeaders: tile.ajaxHeaders, + crossOriginPolicy: this.crossOriginPolicy, + ajaxWithCredentials: this.ajaxWithCredentials, + callback: function( data, errorMsg, tileRequest ){ + _this._onTileLoad( tile, time, data, errorMsg, tileRequest ); + }, + abort: function() { + tile.loading = false; + } + }); + }, + + /** + * Callback fired when a Tile's Image finished downloading. + * @private + * @param {OpenSeadragon.Tile} tile + * @param {Number} time + * @param {*} data image data + * @param {String} errorMsg + * @param {XMLHttpRequest} tileRequest + */ + _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) { + if ( !data ) { + $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), 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; + } else { + tile.exists = true; + } + + if ( time < this.lastResetTime ) { + $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() ); + tile.loading = false; + return; + } + + var _this = this, + finish = function() { + var ccc = _this.source; + var cutoff = ccc.getClosestLevel(); + _this._setTileLoaded(tile, data, cutoff, tileRequest); + }; + + + finish(); + }, + + /** + * @private + * @param {OpenSeadragon.Tile} tile + * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object + * @param {Number|undefined} cutoff + * @param {XMLHttpRequest|undefined} tileRequest + */ + _setTileLoaded: function(tile, data, cutoff, tileRequest) { + var increment = 0, + eventFinished = false, + _this = this; + + function getCompletionCallback() { + if (eventFinished) { + $.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " + + "Its return value should be called asynchronously."); + } + increment++; + return completionCallback; + } + + function completionCallback() { + increment--; + if (increment === 0) { + tile.loading = false; + tile.loaded = true; + tile.hasTransparency = _this.source.hasTransparency( + tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData + ); + if (!tile.context2D) { + _this._tileCache.cacheTile({ + data: data, + tile: tile, + cutoff: cutoff, + tiledImage: _this + }); + } + /** + * Triggered when a tile is loaded and pre-processing is compelete, + * and the tile is ready to draw. + * + * @event tile-ready + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. + * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). + */ + _this.viewer.raiseEvent("tile-ready", { + tile: tile, + tiledImage: _this, + tileRequest: tileRequest + }); + _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 (data) of the tile. Deprecated. + * @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object + * @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. + */ + + var fallbackCompletion = getCompletionCallback(); + this.viewer.raiseEvent("tile-loaded", { + tile: tile, + tiledImage: this, + tileRequest: tileRequest, + get image() { + $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead."); + return data; + }, + data: data, + getCompletionCallback: getCompletionCallback + }); + eventFinished = true; + // In case the completion callback is never called, we at least force it once. + fallbackCompletion(); + }, + + + /** + * Determines the 'best tiles' from the given 'last best' tiles and the + * tile in question. + * @private + * + * @param {OpenSeadragon.Tile[]} previousBest The best tiles so far. + * @param {OpenSeadragon.Tile} tile The new tile to consider. + * @param {Number} maxNTiles The max number of best tiles. + * @returns {OpenSeadragon.Tile[]} The new best tiles. + */ + _compareTiles: function( previousBest, tile, maxNTiles ) { + if ( !previousBest ) { + return [tile]; + } + previousBest.push(tile); + this._sortTiles(previousBest); + if (previousBest.length > maxNTiles) { + previousBest.pop(); + } + return previousBest; + }, + + /** + * Sorts tiles in an array according to distance and visibility. + * @private + * + * @param {OpenSeadragon.Tile[]} tiles The tiles. + */ + _sortTiles: function( tiles ) { + tiles.sort(function (a, b) { + if (a === null) { + return 1; + } + if (b === null) { + return -1; + } + if (a.visibility === b.visibility) { + // sort by smallest squared distance + return (a.squaredDistance - b.squaredDistance); + } else { + // sort by largest visibility value + return (b.visibility - a.visibility); + } + }); + }, + + + /** + * 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. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @returns {Boolean} + */ + _providesCoverage: function( 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 ( Object.prototype.hasOwnProperty.call( rows, i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( Object.prototype.hasOwnProperty.call( cols, j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } - /** - * 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. - * @private - * - * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. - * @param {Number} level - The resolution level of the tile. - * @param {Number} x - The X position of the tile. - * @param {Number} y - The Y position of the tile. - * @returns {Boolean} - */ - _isCovered: function( coverage, level, x, y ) { - if ( x === undefined || y === undefined ) { - return this._providesCoverage( coverage, level + 1 ); - } else { return ( - 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 ) + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true ); + }, + + /** + * 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. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @returns {Boolean} + */ + _isCovered: function( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return this._providesCoverage( coverage, level + 1 ); + } else { + return ( + 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 ) + ); + } + }, + + /** + * Sets whether the given tile provides coverage or not. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of the tile. + * @param {Number} x - The X position of the tile. + * @param {Number} y - The Y position of the tile. + * @param {Boolean} covers - Whether the tile provides coverage. + */ + _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", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; + }, + + /** + * 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. + * @private + * + * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. + * @param {Number} level - The resolution level of tiles to completely reset. + */ + _resetCoverage: function( coverage, level ) { + coverage[ level ] = {}; } - }, - - /** - * Sets whether the given tile provides coverage or not. - * @private - * - * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. - * @param {Number} level - The resolution level of the tile. - * @param {Number} x - The X position of the tile. - * @param {Number} y - The Y position of the tile. - * @param {Boolean} covers - Whether the tile provides coverage. - */ - _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", - level - ); - return; - } - - if ( !coverage[ level ][ x ] ) { - coverage[ level ][ x ] = {}; - } - - coverage[ level ][ x ][ y ] = covers; - }, - - /** - * 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. - * @private - * - * @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean. - * @param {Number} level - The resolution level of tiles to completely reset. - */ - _resetCoverage: function( coverage, level ) { - coverage[ level ] = {}; - } -}); + }); -}( OpenSeadragon )); + }( OpenSeadragon ));