openseadragon/src/tiledimage.js

1611 lines
57 KiB
JavaScript
Raw Normal View History

2014-08-07 00:38:56 +04:00
/*
* OpenSeadragon - TiledImage
2014-08-07 00:38:56 +04:00
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2013 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
2014-11-06 01:48:37 +03:00
* You shouldn't have to create a TiledImage directly; use {@link OpenSeadragon.Viewer#open}
* or {@link OpenSeadragon.Viewer#addTiledImage} instead.
* @class TiledImage
2014-11-04 22:53:39 +03:00
* @memberof OpenSeadragon
2014-11-12 04:14:48 +03:00
* @extends OpenSeadragon.EventSource
2014-08-07 00:38:56 +04:00
* @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}.
* A new instance is created for each TileSource opened.
2014-11-04 22:53:39 +03:00
* @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.
2014-12-02 22:44:02 +03:00
* @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}.
* @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}.
2014-11-04 22:53:39 +03:00
* @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}.
2015-04-21 02:25:36 +03:00
* @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at.
* @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values.
2014-11-04 22:53:39 +03:00
* @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}.
* @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}.
2014-11-04 22:53:39 +03:00
* @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}.
2014-08-07 00:38:56 +04:00
*/
$.TiledImage = function( options ) {
var _this = this;
$.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" );
$.console.assert( options.drawer, "[TiledImage] options.drawer is required" );
2014-08-12 04:04:20 +04:00
$.console.assert( options.viewer, "[TiledImage] options.viewer is required" );
$.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" );
2014-11-04 22:53:39 +03:00
$.console.assert( options.source, "[TiledImage] options.source is required" );
2015-03-18 20:03:44 +03:00
$.console.assert(!options.clip || options.clip instanceof $.Rect,
"[TiledImage] options.clip must be an OpenSeadragon.Rect if present");
2014-11-12 04:14:48 +03:00
$.EventSource.call( this );
this._tileCache = options.tileCache;
delete options.tileCache;
this._drawer = options.drawer;
delete options.drawer;
2014-08-07 00:38:56 +04:00
2014-08-12 04:04:20 +04:00
this._imageLoader = options.imageLoader;
delete options.imageLoader;
if (options.clip instanceof $.Rect) {
this._clip = options.clip.clone();
}
2015-03-18 20:03:44 +03:00
delete options.clip;
2014-11-25 04:47:16 +03:00
var x = options.x || 0;
2014-08-07 00:38:56 +04:00
delete options.x;
2014-11-25 04:47:16 +03:00
var y = options.y || 0;
2014-08-07 00:38:56 +04:00
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;
2014-08-07 00:38:56 +04:00
2014-11-25 04:47:16 +03:00
var scale = 1;
2014-08-07 00:38:56 +04:00
if ( options.width ) {
2014-11-25 04:47:16 +03:00
scale = options.width;
2014-08-07 00:38:56 +04:00
delete options.width;
if ( options.height ) {
$.console.error( "specifying both width and height to a tiledImage is not supported" );
2014-08-07 00:38:56 +04:00
delete options.height;
}
} else if ( options.height ) {
2014-11-25 04:47:16 +03:00
scale = options.height / this.normHeight;
2014-08-07 00:38:56 +04:00
delete options.height;
}
var fitBounds = options.fitBounds;
delete options.fitBounds;
var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
delete options.fitBoundsPlacement;
2014-08-07 00:38:56 +04:00
$.extend( true, this, {
//internal state properties
viewer: null,
tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile.
coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean.
lastDrawn: [], // An unordered list of Tiles drawn last frame.
lastResetTime: 0, // Last time for which the tiledImage was reset.
_midDraw: false, // Is the tiledImage currently updating the viewport?
_needsDraw: true, // Does the tiledImage need to update the viewport again?
_hasOpaqueTile: false, // Do we have even one fully opaque tile?
2014-08-07 00:38:56 +04:00
//configurable settings
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
animationTime: $.DEFAULT_SETTINGS.animationTime,
minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio,
wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
wrapVertical: $.DEFAULT_SETTINGS.wrapVertical,
immediateRender: $.DEFAULT_SETTINGS.immediateRender,
blendTime: $.DEFAULT_SETTINGS.blendTime,
alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend,
minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio,
smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom,
iOSDevice: $.DEFAULT_SETTINGS.iOSDevice,
debugMode: $.DEFAULT_SETTINGS.debugMode,
crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy,
placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle,
2016-01-14 23:42:02 +03:00
opacity: $.DEFAULT_SETTINGS.opacity,
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation
2014-08-07 00:38:56 +04:00
}, options );
2016-08-10 20:35:08 +03:00
this._fullyLoaded = false;
2014-11-25 04:47:16 +03:00
this._xSpring = new $.Spring({
initial: x,
springStiffness: this.springStiffness,
animationTime: this.animationTime
});
this._ySpring = new $.Spring({
initial: y,
springStiffness: this.springStiffness,
animationTime: this.animationTime
});
this._scaleSpring = new $.Spring({
initial: scale,
springStiffness: this.springStiffness,
animationTime: this.animationTime
});
this._updateForScale();
if (fitBounds) {
this.fitBounds(fitBounds, fitBoundsPlacement, true);
}
// We need a callback to give image manipulation a chance to happen
this._drawingHandler = function(args) {
/**
* This event is fired just before the tile is drawn giving the application a chance to alter the image.
*
2016-01-20 00:23:29 +03:00
* NOTE: This event is only fired when the drawer is using a <canvas>.
*
* @event tile-drawing
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.Tile} tile - The Tile being drawn.
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
* @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into.
* @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.viewer.raiseEvent('tile-drawing', $.extend({
tiledImage: _this
}, args));
};
2014-08-07 00:38:56 +04:00
};
2014-11-12 04:14:48 +03:00
$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{
2014-08-07 00:38:56 +04:00
/**
2014-12-02 22:44:02 +03:00
* @returns {Boolean} Whether the TiledImage needs to be drawn.
2014-08-07 00:38:56 +04:00
*/
2014-12-02 22:44:02 +03:00
needsDraw: function() {
return this._needsDraw;
2014-08-07 00:38:56 +04:00
},
2016-08-10 20:35:08 +03:00
/**
* @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
});
},
2014-08-07 00:38:56 +04:00
/**
* Clears all tiles and triggers an update on the next call to
2014-11-04 22:53:39 +03:00
* {@link OpenSeadragon.TiledImage#update}.
2014-08-07 00:38:56 +04:00
*/
reset: function() {
this._tileCache.clearTilesFor(this);
2014-08-07 00:38:56 +04:00
this.lastResetTime = $.now();
2014-12-02 22:44:02 +03:00
this._needsDraw = true;
2014-08-07 00:38:56 +04:00
},
/**
2014-12-02 22:44:02 +03:00
* Updates the TiledImage's bounds, animating if needed.
* @returns {Boolean} Whether the TiledImage animated.
2014-08-07 00:38:56 +04:00
*/
update: function() {
2014-12-02 22:44:02 +03:00
var oldX = this._xSpring.current.value;
var oldY = this._ySpring.current.value;
var oldScale = this._scaleSpring.current.value;
this._xSpring.update();
this._ySpring.update();
this._scaleSpring.update();
if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY ||
this._scaleSpring.current.value !== oldScale) {
2014-12-03 00:17:56 +03:00
this._updateForScale();
2014-12-02 22:44:02 +03:00
this._needsDraw = true;
return true;
}
return false;
},
/**
* Draws the TiledImage to its Drawer.
*/
draw: function() {
if (this.opacity !== 0) {
this._midDraw = true;
2016-02-23 20:47:16 +03:00
updateViewport(this);
this._midDraw = false;
}
2014-08-07 00:38:56 +04:00
},
/**
2014-11-04 22:53:39 +03:00
* Destroy the TiledImage (unload current loaded tiles).
2014-08-07 00:38:56 +04:00
*/
destroy: function() {
this.reset();
2014-08-13 03:04:55 +04:00
},
2014-11-04 22:53:39 +03:00
/**
* @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates.
2014-11-25 04:47:16 +03:00
* @param {Boolean} [current=false] - Pass true for the current location; false for target location.
2014-11-04 22:53:39 +03:00
*/
2014-11-25 04:47:16 +03:00
getBounds: function(current) {
if (current) {
return new $.Rect( this._xSpring.current.value, this._ySpring.current.value,
this._worldWidthCurrent, this._worldHeightCurrent );
}
return 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.getBounds(current);
if (this._clip) {
var ratio = this._worldWidthCurrent / 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;
},
2014-11-04 22:53:39 +03:00
/**
* @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels.
*/
getContentSize: function() {
return new $.Point(this.source.dimensions.x, this.source.dimensions.y);
2014-11-12 04:14:48 +03:00
},
// private
2014-11-25 04:47:16 +03:00
_viewportToImageDelta: function( viewerX, viewerY, current ) {
var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value);
2014-11-25 04:47:16 +03:00
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.
2014-11-25 04:47:16 +03:00
* This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}.
* @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system.
* @param {Number} [viewerY] - The Y coordinate in viewport coordinate system.
* @param {Boolean} [current=false] - Pass true to use the current location; false for target location.
* @return {OpenSeadragon.Point} A point representing the coordinates in the image.
*/
2014-11-25 04:47:16 +03:00
viewportToImageCoordinates: function( viewerX, viewerY, current ) {
if (viewerX instanceof $.Point) {
//they passed a point instead of individual components
2014-11-25 04:47:16 +03:00
current = viewerY;
viewerY = viewerX.y;
viewerX = viewerX.x;
}
2014-11-25 04:47:16 +03:00
if (current) {
return this._viewportToImageDelta(viewerX - this._xSpring.current.value,
viewerY - this._ySpring.current.value);
}
return this._viewportToImageDelta(viewerX - this._xSpring.target.value,
viewerY - this._ySpring.target.value);
},
// private
2014-11-25 04:47:16 +03:00
_imageToViewportDelta: function( imageX, imageY, current ) {
var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value);
2014-11-25 04:47:16 +03:00
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
2014-11-25 04:47:16 +03:00
* This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}.
* @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system.
* @param {Number} [imageY] - The Y coordinate in image coordinate system.
* @param {Boolean} [current=false] - Pass true to use the current location; false for target location.
* @return {OpenSeadragon.Point} A point representing the coordinates in the viewport.
*/
2014-11-25 04:47:16 +03:00
imageToViewportCoordinates: function( imageX, imageY, current ) {
if (imageX instanceof $.Point) {
//they passed a point instead of individual components
2014-11-25 04:47:16 +03:00
current = imageY;
imageY = imageX.y;
imageX = imageX.x;
}
var point = this._imageToViewportDelta(imageX, imageY);
2014-11-25 04:47:16 +03:00
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;
},
/**
* Translates from a rectangle which describes a portion of the image in
* pixel coordinates to OpenSeadragon viewport rectangle coordinates.
2014-11-25 04:47:16 +03:00
* This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}.
* @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system.
* @param {Number} [imageY] - The top coordinate in image coordinate system.
* @param {Number} [pixelWidth] - The width in pixel of the rectangle.
* @param {Number} [pixelHeight] - The height in pixel of the rectangle.
* @param {Boolean} [current=false] - Pass true to use the current location; false for target location.
* @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport.
*/
2014-11-25 04:47:16 +03:00
imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) {
var rect = imageX;
if (rect instanceof $.Rect) {
2014-11-25 04:47:16 +03:00
//they passed a rect instead of individual components
current = imageY;
} else {
rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight);
}
2014-11-25 04:47:16 +03:00
var coordA = this.imageToViewportCoordinates(rect.getTopLeft(), current);
var coordB = this._imageToViewportDelta(rect.width, rect.height, current);
2014-11-25 04:47:16 +03:00
return new $.Rect(
coordA.x,
coordA.y,
coordB.x,
coordB.y,
rect.degrees
);
},
/**
* Translates from a rectangle which describes a portion of
* the viewport in point coordinates to image rectangle coordinates.
2014-11-25 04:47:16 +03:00
* This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}.
* @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system.
* @param {Number} [viewerY] - The top coordinate in viewport coordinate system.
* @param {Number} [pointWidth] - The width in viewport coordinate system.
* @param {Number} [pointHeight] - The height in viewport coordinate system.
* @param {Boolean} [current=false] - Pass true to use the current location; false for target location.
* @return {OpenSeadragon.Rect} A rect representing the coordinates in the image.
*/
2014-11-25 04:47:16 +03:00
viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight, current ) {
var rect = viewerX;
2014-11-25 04:47:16 +03:00
if (viewerX instanceof $.Rect) {
//they passed a rect instead of individual components
current = viewerY;
} else {
rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight);
}
2014-11-25 04:47:16 +03:00
var coordA = this.viewportToImageCoordinates(rect.getTopLeft(), current);
var coordB = this._viewportToImageDelta(rect.width, rect.height, current);
2014-11-25 04:47:16 +03:00
return new $.Rect(
coordA.x,
coordA.y,
coordB.x,
coordB.y,
rect.degrees
);
},
/**
* Convert pixel coordinates relative to the viewer element to image
* coordinates.
* @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point}
*/
viewerElementToImageCoordinates: function( pixel ) {
var point = this.viewport.pointFromPixel( pixel, true );
return this.viewportToImageCoordinates( point );
},
/**
* Convert pixel coordinates relative to the image to
* viewer element coordinates.
* @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point}
*/
imageToViewerElementCoordinates: function( pixel ) {
var point = this.imageToViewportCoordinates( pixel );
return this.viewport.pixelFromPoint( point, true );
},
/**
* Convert pixel coordinates relative to the window to image coordinates.
* @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point}
*/
windowToImageCoordinates: function( pixel ) {
var viewerCoordinates = pixel.minus(
OpenSeadragon.getElementPosition( this.viewer.element ));
return this.viewerElementToImageCoordinates( viewerCoordinates );
},
/**
* Convert image coordinates to pixel coordinates relative to the window.
* @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point}
*/
imageToWindowCoordinates: function( pixel ) {
var viewerCoordinates = this.imageToViewerElementCoordinates( pixel );
return viewerCoordinates.plus(
OpenSeadragon.getElementPosition( this.viewer.element ));
},
/**
* 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;
},
2014-11-12 04:14:48 +03:00
/**
* Sets the TiledImage's position in the world.
* @param {OpenSeadragon.Point} position - The new position, in viewport coordinates.
2014-11-25 04:47:16 +03:00
* @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately.
* @fires OpenSeadragon.TiledImage.event:bounds-change
2014-11-12 04:14:48 +03:00
*/
2014-11-25 04:47:16 +03:00
setPosition: function(position, immediately) {
2014-12-02 22:44:02 +03:00
var sameTarget = (this._xSpring.target.value === position.x &&
this._ySpring.target.value === position.y);
2014-11-25 04:47:16 +03:00
if (immediately) {
2014-12-02 22:44:02 +03:00
if (sameTarget && this._xSpring.current.value === position.x &&
this._ySpring.current.value === position.y) {
2014-11-25 04:47:16 +03:00
return;
}
this._xSpring.resetTo(position.x);
this._ySpring.resetTo(position.y);
this._needsDraw = true;
2014-11-25 04:47:16 +03:00
} else {
2014-12-02 22:44:02 +03:00
if (sameTarget) {
2014-11-25 04:47:16 +03:00
return;
}
this._xSpring.springTo(position.x);
this._ySpring.springTo(position.y);
this._needsDraw = true;
}
2014-12-02 22:44:02 +03:00
if (!sameTarget) {
this._raiseBoundsChange();
}
2014-11-12 04:14:48 +03:00
},
/**
* 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.
2014-11-25 04:47:16 +03:00
* @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately.
* @fires OpenSeadragon.TiledImage.event:bounds-change
*/
2014-11-25 04:47:16 +03:00
setWidth: function(width, immediately) {
2014-12-02 22:44:02 +03:00
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.
2014-11-25 04:47:16 +03:00
* @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately.
* @fires OpenSeadragon.TiledImage.event:bounds-change
*/
2014-11-25 04:47:16 +03:00
setHeight: function(height, immediately) {
2014-12-02 22:44:02 +03:00
this._setScale(height / this.normHeight, immediately);
},
2016-03-21 23:11:50 +03:00
/**
* 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) {
2016-03-21 23:11:50 +03:00
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) {
2016-03-21 23:11:50 +03:00
// We will have margins on the X axis
var height = bounds.height / displayedHeightRatio;
2016-03-21 23:11:50 +03:00
var marginLeft = 0;
if (anchorProperties.isHorizontallyCentered) {
marginLeft = (bounds.width - bounds.height * aspectRatio) / 2;
} else if (anchorProperties.isRight) {
marginLeft = bounds.width - bounds.height * aspectRatio;
2016-03-21 23:11:50 +03:00
}
this.setPosition(
new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset),
2016-03-21 23:11:50 +03:00
immediately);
this.setHeight(height, immediately);
2016-03-21 23:11:50 +03:00
} else {
// We will have margins on the Y axis
var width = bounds.width / displayedWidthRatio;
2016-03-21 23:11:50 +03:00
var marginTop = 0;
if (anchorProperties.isVerticallyCentered) {
marginTop = (bounds.height - bounds.width / aspectRatio) / 2;
} else if (anchorProperties.isBottom) {
marginTop = bounds.height - bounds.width / aspectRatio;
2016-03-21 23:11:50 +03:00
}
this.setPosition(
new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop),
2016-03-21 23:11:50 +03:00
immediately);
this.setWidth(width, immediately);
2016-03-21 23:11:50 +03:00
}
},
/**
* @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle,
* in image pixels, or null if none.
*/
getClip: function() {
if (this._clip) {
return this._clip.clone();
}
return null;
},
/**
* @param {OpenSeadragon.Rect|null} newClip - An area, in image pixels, to clip to
* (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas.
*/
setClip: function(newClip) {
$.console.assert(!newClip || newClip instanceof $.Rect,
"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null");
if (newClip instanceof $.Rect) {
this._clip = newClip.clone();
} else {
this._clip = null;
}
this._needsDraw = true;
},
2015-04-21 02:25:36 +03:00
/**
* @returns {Number} The TiledImage's current opacity.
*/
getOpacity: function() {
return this.opacity;
},
/**
* @param {Number} opacity Opacity the tiled image should be drawn at.
*/
setOpacity: function(opacity) {
this.opacity = opacity;
this._needsDraw = true;
},
/**
* @returns {String} The TiledImage's current compositeOperation.
*/
getCompositeOperation: function() {
return this.compositeOperation;
},
/**
* @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation.
*/
setCompositeOperation: function(compositeOperation) {
this.compositeOperation = compositeOperation;
this._needsDraw = true;
},
2014-12-02 22:44:02 +03:00
// private
_setScale: function(scale, immediately) {
var sameTarget = (this._scaleSpring.target.value === scale);
2014-11-25 04:47:16 +03:00
if (immediately) {
2014-12-02 22:44:02 +03:00
if (sameTarget && this._scaleSpring.current.value === scale) {
2014-11-25 04:47:16 +03:00
return;
}
this._scaleSpring.resetTo(scale);
this._updateForScale();
this._needsDraw = true;
2014-11-25 04:47:16 +03:00
} else {
2014-12-02 22:44:02 +03:00
if (sameTarget) {
2014-11-25 04:47:16 +03:00
return;
}
this._scaleSpring.springTo(scale);
this._updateForScale();
this._needsDraw = true;
}
2014-12-02 22:44:02 +03:00
if (!sameTarget) {
this._raiseBoundsChange();
}
},
2014-11-25 04:47:16 +03:00
// private
_updateForScale: function() {
2014-12-02 22:44:02 +03:00
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;
2014-11-25 04:47:16 +03:00
},
// private
_raiseBoundsChange: function() {
2014-11-12 04:14:48 +03:00
/**
* Raised when the TiledImage's bounds are changed.
2014-12-02 22:44:02 +03:00
* Note that this event is triggered only when the animation target is changed;
* not for every frame of animation.
* @event bounds-change
2014-11-12 04:14:48 +03:00
* @memberOf OpenSeadragon.TiledImage
* @type {object}
* @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent('bounds-change');
},
// private
_isBottomItem: function() {
return this.viewer.world.getItemAt(0) === this;
2014-08-07 00:38:56 +04:00
}
2014-11-12 04:14:48 +03:00
});
2014-08-07 00:38:56 +04:00
/**
* @private
* @inner
* Pretty much every other line in this needs to be documented so it's clear
* how each piece of this routine contributes to the drawing process. That's
* why there are so many TODO's inside this function.
*/
function updateViewport( tiledImage ) {
2014-08-07 00:38:56 +04:00
2014-12-02 22:44:02 +03:00
tiledImage._needsDraw = false;
2014-08-07 00:38:56 +04:00
var tile,
level,
best = null,
haveDrawn = false,
currentTime = $.now(),
2014-10-17 01:00:07 +04:00
viewportBounds = tiledImage.viewport.getBoundsWithMargins( true ),
zeroRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( 0 ),
2014-08-07 00:38:56 +04:00
true
2014-12-02 22:44:02 +03:00
).x * tiledImage._scaleSpring.current.value,
2014-08-07 00:38:56 +04:00
lowestLevel = Math.max(
tiledImage.source.minLevel,
2014-08-07 00:38:56 +04:00
Math.floor(
Math.log( tiledImage.minZoomImageRatio ) /
2014-08-07 00:38:56 +04:00
Math.log( 2 )
)
),
highestLevel = Math.min(
Math.abs(tiledImage.source.maxLevel),
2014-08-07 00:38:56 +04:00
Math.abs(Math.floor(
Math.log( zeroRatioC / tiledImage.minPixelRatio ) /
2014-08-07 00:38:56 +04:00
Math.log( 2 )
))
),
renderPixelRatioC,
renderPixelRatioT,
zeroRatioT,
optimalRatio,
levelOpacity,
levelVisibility;
// Reset tile's internal drawn state
while (tiledImage.lastDrawn.length > 0) {
tile = tiledImage.lastDrawn.pop();
2014-08-07 00:38:56 +04:00
tile.beingDrawn = false;
}
if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) {
var tiledImageBounds = tiledImage.getClippedBounds(true);
var intersection = viewportBounds.intersection(tiledImageBounds);
if (intersection === null) {
return;
}
viewportBounds = intersection;
2014-08-07 00:38:56 +04:00
}
viewportBounds = viewportBounds.getBoundingBox();
viewportBounds.x -= tiledImage._xSpring.current.value;
viewportBounds.y -= tiledImage._ySpring.current.value;
2014-08-07 00:38:56 +04:00
2014-11-06 03:39:35 +03:00
var viewportTL = viewportBounds.getTopLeft();
var viewportBR = viewportBounds.getBottomRight();
2014-08-07 00:38:56 +04:00
//Don't draw if completely outside of the viewport
2014-12-02 22:44:02 +03:00
if ( !tiledImage.wrapHorizontal && (viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidthCurrent ) ) {
2014-08-07 00:38:56 +04:00
return;
2014-11-06 03:39:35 +03:00
}
2014-12-02 22:44:02 +03:00
if ( !tiledImage.wrapVertical && ( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeightCurrent ) ) {
2014-08-07 00:38:56 +04:00
return;
}
// Calculate viewport rect / bounds
if ( !tiledImage.wrapHorizontal ) {
2014-08-07 00:38:56 +04:00
viewportTL.x = Math.max( viewportTL.x, 0 );
2014-12-02 22:44:02 +03:00
viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidthCurrent );
2014-08-07 00:38:56 +04:00
}
2014-11-06 03:39:35 +03:00
if ( !tiledImage.wrapVertical ) {
2014-08-07 00:38:56 +04:00
viewportTL.y = Math.max( viewportTL.y, 0 );
2014-12-02 22:44:02 +03:00
viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeightCurrent );
2014-08-07 00:38:56 +04:00
}
// Calculations for the interval of levels to draw
// (above in initial var statement)
// can return invalid intervals; fix that here if necessary
lowestLevel = Math.min( lowestLevel, highestLevel );
// Update any level that will be drawn
var drawLevel; // FIXME: drawLevel should have a more explanatory name
for ( level = highestLevel; level >= lowestLevel; level-- ) {
drawLevel = false;
//Avoid calculations for draw if we have already drawn this
renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ),
2014-08-07 00:38:56 +04:00
true
2014-12-02 22:44:02 +03:00
).x * tiledImage._scaleSpring.current.value;
2014-08-07 00:38:56 +04:00
if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) ||
2014-08-07 00:38:56 +04:00
( level == lowestLevel ) ) {
drawLevel = true;
haveDrawn = true;
} else if ( !haveDrawn ) {
continue;
}
//Perform calculations for draw if we haven't drawn this
renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ),
2014-08-07 00:38:56 +04:00
false
2014-12-02 22:44:02 +03:00
).x * tiledImage._scaleSpring.current.value;
2014-08-07 00:38:56 +04:00
zeroRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio(
2014-08-07 00:38:56 +04:00
Math.max(
tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1,
2014-08-07 00:38:56 +04:00
0
)
),
false
2014-12-02 22:44:02 +03:00
).x * tiledImage._scaleSpring.current.value;
2014-08-07 00:38:56 +04:00
optimalRatio = tiledImage.immediateRender ?
2014-08-07 00:38:56 +04:00
1 :
zeroRatioT;
levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
levelVisibility = optimalRatio / Math.abs(
optimalRatio - renderPixelRatioT
);
// Update the level and keep track of 'best' tile to load
best = updateLevel(
tiledImage,
2014-08-07 00:38:56 +04:00
haveDrawn,
drawLevel,
level,
levelOpacity,
levelVisibility,
viewportTL,
viewportBR,
currentTime,
best
);
// Stop the loop if lower-res tiles would all be covered by
// already drawn tiles
if ( providesCoverage( tiledImage.coverage, level ) ) {
2014-08-07 00:38:56 +04:00
break;
}
}
// Perform the actual drawing
drawTiles( tiledImage, tiledImage.lastDrawn );
2014-08-07 00:38:56 +04:00
// Load the new 'best' tile
if (best && !best.context2D) {
loadTile( tiledImage, best, currentTime );
tiledImage._needsDraw = true;
2016-08-10 20:35:08 +03:00
tiledImage._setFullyLoaded(false);
} else {
2016-08-10 20:35:08 +03:00
tiledImage._setFullyLoaded(true);
2014-08-07 00:38:56 +04:00
}
}
function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
2014-08-07 00:38:56 +04:00
var x, y,
tileTL,
tileBR,
numberOfTiles,
viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() );
2014-08-07 00:38:56 +04:00
if( tiledImage.viewer ){
2014-08-07 00:38:56 +04:00
/**
* <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.
2014-08-07 00:38:56 +04:00
* @property {Object} havedrawn
* @property {Object} level
* @property {Object} opacity
* @property {Object} visibility
* @property {Object} topleft
* @property {Object} bottomright
* @property {Object} currenttime
* @property {Object} best
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
tiledImage.viewer.raiseEvent( 'update-level', {
tiledImage: tiledImage,
2014-08-07 00:38:56 +04:00
havedrawn: haveDrawn,
level: level,
opacity: levelOpacity,
visibility: levelVisibility,
topleft: viewportTL,
bottomright: viewportBR,
currenttime: currentTime,
best: best
});
}
//OK, a new drawing so do your calculations
2014-12-02 22:44:02 +03:00
tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scaleSpring.current.value ));
tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scaleSpring.current.value ));
numberOfTiles = tiledImage.source.getNumTiles( level );
2014-08-07 00:38:56 +04:00
resetCoverage( tiledImage.coverage, level );
2014-08-07 00:38:56 +04:00
if ( tiledImage.wrapHorizontal ) {
tileTL.x -= 1; // left invisible column (othervise we will have empty space after scroll at left)
} else {
2014-08-07 00:38:56 +04:00
tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
}
if ( tiledImage.wrapVertical ) {
tileTL.y -= 1; // top invisible row (othervise we will have empty space after scroll at top)
} else {
2014-08-07 00:38:56 +04:00
tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
}
for ( x = tileTL.x; x <= tileBR.x; x++ ) {
for ( y = tileTL.y; y <= tileBR.y; y++ ) {
best = updateTile(
tiledImage,
2014-08-07 00:38:56 +04:00
drawLevel,
haveDrawn,
x, y,
level,
levelOpacity,
levelVisibility,
viewportCenter,
numberOfTiles,
currentTime,
best
);
}
}
return best;
}
function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
2014-08-07 00:38:56 +04:00
var tile = getTile(
x, y,
level,
tiledImage.source,
tiledImage.tilesMatrix,
2014-08-07 00:38:56 +04:00
currentTime,
numberOfTiles,
2014-12-02 22:44:02 +03:00
tiledImage._worldWidthCurrent,
tiledImage._worldHeightCurrent
2014-08-07 00:38:56 +04:00
),
drawTile = drawLevel;
if( tiledImage.viewer ){
2014-08-07 00:38:56 +04:00
/**
* <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.
2014-08-07 00:38:56 +04:00
* @property {OpenSeadragon.Tile} tile
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
tiledImage.viewer.raiseEvent( 'update-tile', {
tiledImage: tiledImage,
2014-08-07 00:38:56 +04:00
tile: tile
});
}
setCoverage( tiledImage.coverage, level, x, y, false );
2014-08-07 00:38:56 +04:00
if ( !tile.exists ) {
return best;
}
if ( haveDrawn && !drawTile ) {
if ( isCovered( tiledImage.coverage, level, x, y ) ) {
setCoverage( tiledImage.coverage, level, x, y, true );
2014-08-07 00:38:56 +04:00
} else {
drawTile = true;
}
}
if ( !drawTile ) {
return best;
}
positionTile(
tile,
tiledImage.source.tileOverlap,
tiledImage.viewport,
2014-08-07 00:38:56 +04:00
viewportCenter,
levelVisibility,
tiledImage
2014-08-07 00:38:56 +04:00
);
if (!tile.loaded) {
if (tile.context2D) {
setTileLoaded(tiledImage, tile);
} else {
var imageRecord = tiledImage._tileCache.getImageRecord(tile.url);
if (imageRecord) {
var image = imageRecord.getImage();
setTileLoaded(tiledImage, tile, image);
}
}
}
2014-08-07 00:38:56 +04:00
if ( tile.loaded ) {
2014-12-02 22:44:02 +03:00
var needsDraw = blendTile(
tiledImage,
2014-08-07 00:38:56 +04:00
tile,
x, y,
level,
levelOpacity,
currentTime
);
2014-12-02 22:44:02 +03:00
if ( needsDraw ) {
tiledImage._needsDraw = true;
2014-08-07 00:38:56 +04:00
}
} else if ( tile.loading ) {
// the tile is already in the download queue
// thanks josh1093 for finally translating this typo
} else {
best = compareTiles( best, tile );
}
return best;
}
function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) {
var xMod,
yMod,
bounds,
exists,
url,
context2D,
2014-08-07 00:38:56 +04:00
tile;
if ( !tilesMatrix[ level ] ) {
tilesMatrix[ level ] = {};
}
if ( !tilesMatrix[ level ][ x ] ) {
tilesMatrix[ level ][ x ] = {};
}
if ( !tilesMatrix[ level ][ x ][ y ] ) {
xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
bounds = tileSource.getTileBounds( level, xMod, yMod );
exists = tileSource.tileExists( level, xMod, yMod );
url = tileSource.getTileUrl( level, xMod, yMod );
context2D = tileSource.getContext2D ?
tileSource.getContext2D(level, xMod, yMod) : undefined;
2014-08-07 00:38:56 +04:00
bounds.x += ( x - xMod ) / numTiles.x;
bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y);
2014-08-07 00:38:56 +04:00
tilesMatrix[ level ][ x ][ y ] = new $.Tile(
level,
x,
y,
bounds,
exists,
url,
context2D
2014-08-07 00:38:56 +04:00
);
}
tile = tilesMatrix[ level ][ x ][ y ];
tile.lastTouchTime = time;
return tile;
}
function loadTile( tiledImage, tile, time ) {
tile.loading = true;
2014-08-12 04:04:20 +04:00
tiledImage._imageLoader.addJob({
src: tile.url,
crossOriginPolicy: tiledImage.crossOriginPolicy,
callback: function( image, errorMsg ){
onTileLoad( tiledImage, tile, time, image, errorMsg );
},
abort: function() {
tile.loading = false;
}
});
2014-08-07 00:38:56 +04:00
}
function onTileLoad( tiledImage, tile, time, image, errorMsg ) {
if ( !image ) {
$.console.log( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg );
/**
* Triggered when a tile fails to load.
*
2015-09-15 20:31:43 +03:00
* @event tile-load-failed
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Tile} tile - The tile that failed to load.
2015-09-15 20:31:43 +03:00
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to.
* @property {number} time - The time in milliseconds when the tile load began.
* @property {string} message - The error message.
*/
tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg});
tile.loading = false;
tile.exists = false;
return;
}
if ( time < tiledImage.lastResetTime ) {
2014-08-07 00:38:56 +04:00
$.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
tile.loading = false;
2014-08-07 00:38:56 +04:00
return;
}
var finish = function() {
var cutoff = Math.ceil( Math.log(
tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) );
2015-05-29 22:10:47 +03:00
setTileLoaded(tiledImage, tile, image, cutoff);
};
// Check if we're mid-update; this can happen on IE8 because image load events for
// cached images happen immediately there
2014-12-02 22:44:02 +03:00
if ( !tiledImage._midDraw ) {
finish();
} else {
// Wait until after the update, in case caching unloads any tiles
window.setTimeout( finish, 1);
}
2014-08-07 00:38:56 +04:00
}
2015-05-29 22:10:47 +03:00
function setTileLoaded(tiledImage, tile, image, cutoff) {
2015-06-02 19:59:38 +03:00
var increment = 0;
function getCompletionCallback() {
increment++;
return completionCallback;
}
function completionCallback() {
increment--;
if (increment === 0) {
tile.loading = false;
tile.loaded = true;
if (!tile.context2D) {
tiledImage._tileCache.cacheTile({
image: image,
tile: tile,
cutoff: cutoff,
tiledImage: tiledImage
});
}
2015-06-03 21:40:20 +03:00
tiledImage._needsDraw = true;
2015-06-02 19:59:38 +03:00
}
}
/**
* Triggered when a tile has just been loaded in memory. That means that the
2015-06-03 21:40:20 +03:00
* image has been downloaded and can be modified before being drawn to the canvas.
2015-06-02 19:59:38 +03:00
*
* @event tile-loaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {Image} image - The image of the tile.
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {function} getCompletionCallback - A function giving a callback to call
* when the asynchronous processing of the image is done. The image will be
2015-06-11 17:59:32 +03:00
* marked as entirely loaded when the callback has been called once for each
2015-06-03 21:40:20 +03:00
* call to getCompletionCallback.
2015-06-02 19:59:38 +03:00
*/
2015-05-29 22:10:47 +03:00
tiledImage.viewer.raiseEvent("tile-loaded", {
tile: tile,
tiledImage: tiledImage,
image: image,
2015-06-02 19:59:38 +03:00
getCompletionCallback: getCompletionCallback
2015-05-29 22:10:47 +03:00
});
2015-06-02 19:59:38 +03:00
// In case the completion callback is never called, we at least force it once.
getCompletionCallback()();
2015-05-29 22:10:47 +03:00
}
2014-08-07 00:38:56 +04:00
function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){
2014-08-07 00:38:56 +04:00
var boundsTL = tile.bounds.getTopLeft();
2014-12-02 22:44:02 +03:00
boundsTL.x *= tiledImage._scaleSpring.current.value;
boundsTL.y *= tiledImage._scaleSpring.current.value;
2014-11-25 04:47:16 +03:00
boundsTL.x += tiledImage._xSpring.current.value;
boundsTL.y += tiledImage._ySpring.current.value;
2014-08-07 00:38:56 +04:00
var boundsSize = tile.bounds.getSize();
2014-12-02 22:44:02 +03:00
boundsSize.x *= tiledImage._scaleSpring.current.value;
boundsSize.y *= tiledImage._scaleSpring.current.value;
2014-08-07 00:38:56 +04:00
var positionC = viewport.pixelFromPointNoRotate(boundsTL, true),
positionT = viewport.pixelFromPointNoRotate(boundsTL, false),
sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true),
sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
2014-08-07 00:38:56 +04:00
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
tileDistance = viewportCenter.distanceTo( tileCenter );
if ( !overlap ) {
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
}
tile.position = positionC;
tile.size = sizeC;
tile.distance = tileDistance;
tile.visibility = levelVisibility;
}
function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){
var blendTimeMillis = 1000 * tiledImage.blendTime,
2014-08-07 00:38:56 +04:00
deltaTime,
opacity;
if ( !tile.blendStart ) {
tile.blendStart = currentTime;
}
deltaTime = currentTime - tile.blendStart;
opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
if ( tiledImage.alwaysBlend ) {
2014-08-07 00:38:56 +04:00
opacity *= levelOpacity;
}
tile.opacity = opacity;
tiledImage.lastDrawn.push( tile );
2014-08-07 00:38:56 +04:00
if ( opacity == 1 ) {
setCoverage( tiledImage.coverage, level, x, y, true );
2015-09-16 06:52:44 +03:00
tiledImage._hasOpaqueTile = true;
2014-08-07 00:38:56 +04:00
} else if ( deltaTime < blendTimeMillis ) {
return true;
}
return false;
}
/**
* @private
* @inner
* Returns true if the given tile provides coverage to lower-level tiles of
* lower resolution representing the same content. If neither x nor y is
* given, returns true if the entire visible level provides coverage.
*
* Note that out-of-bounds tiles provide coverage in this sense, since
* there's no content that they would need to cover. Tiles at non-existent
* levels that are within the image bounds, however, do not.
*/
function providesCoverage( coverage, level, x, y ) {
var rows,
cols,
i, j;
if ( !coverage[ level ] ) {
return false;
}
if ( x === undefined || y === undefined ) {
rows = coverage[ level ];
for ( i in rows ) {
if ( rows.hasOwnProperty( i ) ) {
cols = rows[ i ];
for ( j in cols ) {
if ( cols.hasOwnProperty( j ) && !cols[ j ] ) {
return false;
}
}
}
}
return true;
}
return (
coverage[ level ][ x] === undefined ||
coverage[ level ][ x ][ y ] === undefined ||
coverage[ level ][ x ][ y ] === true
);
}
/**
* @private
* @inner
* Returns true if the given tile is completely covered by higher-level
* tiles of higher resolution representing the same content. If neither x
* nor y is given, returns true if the entire visible level is covered.
*/
function isCovered( coverage, level, x, y ) {
if ( x === undefined || y === undefined ) {
return providesCoverage( coverage, level + 1 );
} else {
return (
providesCoverage( coverage, level + 1, 2 * x, 2 * y ) &&
providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) &&
providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) &&
providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 )
);
}
}
/**
* @private
* @inner
* Sets whether the given tile provides coverage or not.
*/
function setCoverage( coverage, level, x, y, covers ) {
if ( !coverage[ level ] ) {
$.console.warn(
"Setting coverage for a tile before its level's coverage has been reset: %s",
level
);
return;
}
if ( !coverage[ level ][ x ] ) {
coverage[ level ][ x ] = {};
}
coverage[ level ][ x ][ y ] = covers;
}
/**
* @private
* @inner
* Resets coverage information for the given level. This should be called
* after every draw routine. Note that at the beginning of the next draw
* routine, coverage for every visible tile should be explicitly set.
*/
function resetCoverage( coverage, level ) {
coverage[ level ] = {};
}
/**
* @private
* @inner
* Determines whether the 'last best' tile for the area is better than the
* tile in question.
*/
function compareTiles( previousBest, tile ) {
if ( !previousBest ) {
return tile;
}
if ( tile.visibility > previousBest.visibility ) {
return tile;
} else if ( tile.visibility == previousBest.visibility ) {
if ( tile.distance < previousBest.distance ) {
return tile;
}
}
return previousBest;
}
function drawTiles( tiledImage, lastDrawn ) {
2016-02-25 03:48:11 +03:00
if (lastDrawn.length === 0) {
2015-04-19 23:10:37 +03:00
return;
}
2016-02-25 03:48:11 +03:00
var tile = lastDrawn[0];
var useSketch = tiledImage.opacity < 1 ||
(tiledImage.compositeOperation &&
tiledImage.compositeOperation !== 'source-over') ||
2016-02-25 03:53:35 +03:00
(!tiledImage._isBottomItem() && tile._hasTransparencyChannel());
var sketchScale;
var sketchTranslate;
var zoom = tiledImage.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom);
if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice) {
// When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true;
sketchScale = tile.getScaleForEdgeSmoothing();
2016-01-24 02:40:53 +03:00
sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
tiledImage._drawer.getCanvasSize(false),
tiledImage._drawer.getCanvasSize(true));
}
var bounds;
if (useSketch) {
if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the
// sketch canvas we are going to use for performance reasons.
bounds = tiledImage.viewport.viewportToViewerElementRectangle(
2016-04-28 18:26:09 +03:00
tiledImage.getClippedBounds(true))
.getIntegerBoundingBox()
.times($.pixelDensityRatio);
}
tiledImage._drawer._clear(true, bounds);
2015-04-23 01:30:49 +03:00
}
2015-04-08 22:45:38 +03:00
2016-01-24 02:40:53 +03:00
// When scaling, we must rotate only when blending the sketch canvas to avoid
// interpolation
if (tiledImage.viewport.degrees !== 0 && !sketchScale) {
tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, useSketch);
}
2015-04-13 20:02:04 +03:00
var usedClip = false;
2015-04-08 22:45:38 +03:00
if ( tiledImage._clip ) {
2015-04-19 23:10:37 +03:00
tiledImage._drawer.saveContext(useSketch);
2015-04-08 22:45:38 +03:00
var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box);
if (sketchScale) {
clipRect = clipRect.times(sketchScale);
}
if (sketchTranslate) {
clipRect = clipRect.translate(sketchTranslate);
}
2015-04-19 23:10:37 +03:00
tiledImage._drawer.setClip(clipRect, useSketch);
2015-04-13 20:02:04 +03:00
usedClip = true;
}
2015-09-16 06:52:44 +03:00
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
var placeholderRect = tiledImage._drawer.viewportToDrawerRectangle(tiledImage.getBounds(true));
if (sketchScale) {
placeholderRect = placeholderRect.times(sketchScale);
}
if (sketchTranslate) {
placeholderRect = placeholderRect.translate(sketchTranslate);
}
var fillStyle = null;
if ( typeof tiledImage.placeholderFillStyle === "function" ) {
fillStyle = tiledImage.placeholderFillStyle(tiledImage, tiledImage._drawer.context);
}
else {
fillStyle = tiledImage.placeholderFillStyle;
}
2015-04-19 23:10:37 +03:00
tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
2015-03-18 20:03:44 +03:00
}
2016-02-25 03:48:11 +03:00
for (var i = lastDrawn.length - 1; i >= 0; i--) {
2014-08-07 00:38:56 +04:00
tile = lastDrawn[ i ];
tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate );
tile.beingDrawn = true;
2014-08-07 00:38:56 +04:00
if( tiledImage.viewer ){
2014-08-07 00:38:56 +04:00
/**
* <em>- Needs documentation -</em>
*
* @event tile-drawn
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
2014-08-07 00:38:56 +04:00
* @property {OpenSeadragon.Tile} tile
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
tiledImage.viewer.raiseEvent( 'tile-drawn', {
tiledImage: tiledImage,
2014-08-07 00:38:56 +04:00
tile: tile
});
}
}
2015-03-18 20:03:44 +03:00
2015-04-13 20:02:04 +03:00
if ( usedClip ) {
2015-04-19 23:10:37 +03:00
tiledImage._drawer.restoreContext( useSketch );
}
2016-01-24 02:40:53 +03:00
if (tiledImage.viewport.degrees !== 0 && !sketchScale) {
tiledImage._drawer._restoreRotationChanges(useSketch);
}
2016-01-24 02:40:53 +03:00
if (useSketch) {
var offsetForRotation = tiledImage.viewport.degrees !== 0 && sketchScale;
if (offsetForRotation) {
tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false);
}
tiledImage._drawer.blendSketch({
opacity: tiledImage.opacity,
scale: sketchScale,
translate: sketchTranslate,
compositeOperation: tiledImage.compositeOperation,
bounds: bounds
});
2016-01-24 02:40:53 +03:00
if (offsetForRotation) {
tiledImage._drawer._restoreRotationChanges(false);
}
2015-04-21 02:25:36 +03:00
}
drawDebugInfo( tiledImage, lastDrawn );
}
function drawDebugInfo( tiledImage, lastDrawn ) {
if( tiledImage.debugMode ) {
for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
var tile = lastDrawn[ i ];
try {
tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i );
} catch(e) {
$.console.error(e);
}
}
2015-03-18 20:03:44 +03:00
}
2014-08-07 00:38:56 +04:00
}
}( OpenSeadragon ));