openseadragon/src/tiledimage.js

2390 lines
90 KiB
JavaScript

/*
* OpenSeadragon - TiledImage
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
* You shouldn't have to create a TiledImage 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.
* @param {Boolean} [options.callTileLoadedWithCachedData]
* Invoke tile-loded event for also for tiles loaded from cache if true.
*/
$.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.
_zombieCache: false, // Allow cache to stay in memory upon deletion.
_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,
callTileLoadedWithCachedData: $.DEFAULT_SETTINGS.callTileLoadedWithCachedData
}, 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
});
},
/**
* Forces the system consider all tiles in this tiled image
* as outdated, and fire tile update event on relevant tiles
* Detailed description is available within the 'tile-needs-update'
* event. TODO: consider re-using update function instead?
* @param {boolean} [viewportOnly=false] optionally invalidate only viewport-visible tiles if true
* @param {number} [tStamp=OpenSeadragon.now()] optionally provide tStamp of the update event
*/
invalidate: function (viewportOnly, tStamp) {
tStamp = tStamp || $.now();
this.invalidatedAt = tStamp; //todo document, or remove by something nicer
//always invalidate active tiles
for (let tile of this.lastDrawn) {
$.invalidateTile(tile, this, tStamp, this.viewer);
}
//if not called from world or not desired, avoid update of offscreen data
if (viewportOnly) {
return;
}
const tiles = this.tileCache.getLoadedTilesFor(this);
$.invalidateTilesLater(tiles, tStamp, this.viewer);
},
/**
* 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 ||
(this.opacity > 0 && this._lastDrawn.length < 1);
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();
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
* (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;
}
}
},
/**
* Enable cache preservation even without this tile image,
* by default disabled. It means that upon removing,
* the tile cache does not get immediately erased but
* stays in the memory to be potentially re-used by other
* TiledImages.
* @param {boolean} allow
*/
allowZombieCache: function(allow) {
this._zombieCache = allow;
},
// 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;
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;
}
//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;
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) {
for (let tile of bestTiles) {
if (tile) {
this._loadTile(tile, currentTime);
}
}
this._needsDraw = true;
return false;
} else {
return this._tilesLoading === 0;
}
},
/**
* 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) {
/**
* <em>- Needs documentation -</em>
*
* @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(
drawLevel,
haveDrawn,
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 (tile.isRightMost && this.wrapHorizontal) {
sizeC.x += 0.75; // Otherwise Firefox and Safari show seams
}
if (tile.isBottomMost && this.wrapVertical) {
sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
}
}
tile.position = positionC;
tile.size = sizeC;
tile.squaredDistance = tileSquaredDistance;
tile.visibility = levelVisibility;
},
/**
* 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){
const tile = this._getTile(
x, y,
level,
currentTime,
numberOfTiles
);
let drawTile = drawLevel;
if( this.viewer ){
/**
* <em>- Needs documentation -</em>
*
* @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 ( haveDrawn && !drawTile ) {
if ( this._isCovered( this.coverage, level, x, y ) ) {
this._setCoverage( this.coverage, level, x, y, true );
} else {
drawTile = true;
}
}
if ( !drawTile ) {
return {
bestTiles: best,
tile: tile
};
}
this._positionTile(
tile,
this.source.tileOverlap,
this.viewport,
viewportCenter,
levelVisibility
);
if (!tile.loaded && !tile.loading) {
// Tile was created or its data removed: check whether cache has the data.
// this method sets tile.loading=true if data available, which prevents
// job creation later on
this._tryFindTileCacheRecord(tile);
}
if ( tile.loading ) {
// the tile is already in the download queue
this._tilesLoading++;
} else if (!loadingCoverage) {
// add tile to best tiles to load only when not loaded already
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,
};
},
/**
* @private
* @inner
* Try to find existing cache of the tile
* @param {OpenSeadragon.Tile} tile
*/
_tryFindTileCacheRecord: function(tile) {
if (tile.cacheKey !== tile.originalCacheKey) {
//we found original data: this data will be used to re-execute the pipeline
let record = this._tileCache.getCacheRecord(tile.originalCacheKey);
if (record) {
tile.loading = true;
tile.loaded = false;
this._setTileLoaded(tile, record.data, null, null, record.type);
return true;
}
}
let record = this._tileCache.getCacheRecord(tile.cacheKey);
if (record) {
// setup without calling tile loaded event! tile cache is ready for usage,
tile.loading = true;
tile.loaded = false;
// we could send null as data (cache not re-created), but deprecated events access the data
this._setTileLoaded(tile, record.data, null, null, record.type,
this.callTileLoadedWithCachedData);
return true;
}
return false;
},
/**
* @private
* @inner
* 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,
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;
}
tile = new $.Tile(
level,
x,
y,
bounds,
exists,
urlOrGetter,
undefined,
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;
tile.tiledImage = this;
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, dataType ){
_this._onTileLoad( tile, time, data, errorMsg, tileRequest, dataType );
},
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
* @param {String} [dataType=undefined] data type, derived automatically if not set
*/
_onTileLoad: function( tile, time, data, errorMsg, tileRequest, dataType ) {
//data is set to null on error by image loader, allow custom falsey values (e.g. 0)
if ( data === null || data === undefined ) {
$.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;
}
this._setTileLoaded(tile, data, null, tileRequest, dataType);
//TODO aiosa missing timeout might damage the cache system
},
/**
* @private
* @param {OpenSeadragon.Tile} tile
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object,
* can be null: in that case, cache is assigned to a tile without further processing
* @param {?Number} cutoff ignored, @deprecated
* @param {?XMLHttpRequest} tileRequest
* @param {?String} [dataType=undefined] data type, derived automatically if not set
* @param {?Boolean} [withEvent=true] do not trigger event if true
*/
_setTileLoaded: function(tile, data, cutoff, tileRequest, dataType, withEvent = true) {
tile.tiledImage = this; //unloaded with tile.unload(), so we need to set it back
// does nothing if tile.cacheKey already present
tile.addCache(tile.cacheKey, data, dataType, false, false);
let resolver = null,
increment = 0,
eventFinished = false;
const _this = this,
finishPromise = new $.Promise(r => {
resolver = r;
});
function completionCallback() {
increment--;
if (increment > 0) {
return;
}
//do not override true if set (false is default)
tile.hasTransparency = tile.hasTransparency || _this.source.hasTransparency(
undefined, tile.getUrl(), tile.ajaxHeaders, tile.postData
);
//make sure cache data is ready for drawing, if not, request the desired format
const cache = tile.getCache(tile.cacheKey),
requiredTypes = _this._drawer.getSupportedDataFormats();
if (!cache) {
$.console.warn("Tile %s not cached or not loaded at the end of tile-loaded event: tile will not be drawn - it has no data!", tile);
resolver(tile);
} else if (!requiredTypes.includes(cache.type)) {
//initiate conversion as soon as possible if incompatible with the drawer
cache.prepareForRendering(requiredTypes, _this._drawer.options.detachedCache).then(cacheRef => {
if (!cacheRef) {
return cache.transformTo(requiredTypes);
}
return cacheRef;
}).then(_ => {
tile.loading = false;
tile.loaded = true;
resolver(tile);
});
} else {
tile.loading = false;
tile.loaded = true;
resolver(tile);
}
}
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;
}
const fallbackCompletion = getCompletionCallback();
if (!withEvent) {
fallbackCompletion();
return;
}
/**
* 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.
* This event is _awaiting_, it supports asynchronous functions or functions that return a promise.
*
* @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. Deprecated
* @property {String} dataType type of the data
* @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 {OpenSeadragon.Promise} - Promise resolved when the tile gets fully loaded.
* @property {function} getCompletionCallback - deprecated
*/
this.viewer.raiseEventAwaiting("tile-loaded", {
tile: tile,
tiledImage: this,
tileRequest: tileRequest,
promise: finishPromise,
get image() {
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'tile.getData()' instead.");
return data;
},
get data() {
$.console.error("[tile-loaded] event 'data' has been deprecated. Use 'tile.getData()' instead.");
return data;
},
getCompletionCallback: function () {
$.console.error("[tile-loaded] getCompletionCallback is deprecated: it introduces race conditions: " +
"use async event handlers instead, execution order is deducted by addHandler(...) priority");
return getCompletionCallback();
},
}).catch(() => {
$.console.error("[tile-loaded] event finished with failure: there might be a problem with a plugin you are using.");
}).then(() => {
eventFinished = true;
}).then(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) {
return (a.squaredDistance - b.squaredDistance);
} else {
return (a.visibility - b.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;
}
return (
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 ] = {};
}
});
}( OpenSeadragon ));