2014-08-07 00:38:56 +04:00
|
|
|
/*
|
2014-08-07 00:48:18 +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-08-07 00:48:18 +04:00
|
|
|
* @class TiledImage
|
2014-11-04 22:53:39 +03:00
|
|
|
* @memberof OpenSeadragon
|
2014-08-07 00:38:56 +04:00
|
|
|
* @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}.
|
2014-08-07 00:48:18 +04:00
|
|
|
* 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 world coordinates.
|
|
|
|
* @param {Number} [options.y=0] - Top position, in world coordinates.
|
|
|
|
* @param {Number} [options.width=1] - Width, in world coordinates.
|
|
|
|
* @param {Number} [options.height] - Height, in world coordinates.
|
|
|
|
* @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 {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}.
|
|
|
|
* @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}.
|
2014-08-07 00:38:56 +04:00
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
$.TiledImage = function( options ) {
|
|
|
|
$.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" );
|
2014-08-07 00:48:18 +04:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2014-08-07 00:38:56 +04:00
|
|
|
this._worldX = options.x || 0;
|
|
|
|
delete options.x;
|
|
|
|
this._worldY = options.y || 0;
|
|
|
|
delete options.y;
|
|
|
|
|
|
|
|
// Ratio of zoomable image height to width.
|
|
|
|
this.normHeight = options.source.dimensions.y / options.source.dimensions.x;
|
|
|
|
|
|
|
|
if ( options.width ) {
|
|
|
|
this._scale = options.width;
|
|
|
|
delete options.width;
|
|
|
|
|
|
|
|
if ( options.height ) {
|
2014-08-07 00:48:18 +04:00
|
|
|
$.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 ) {
|
|
|
|
this._scale = options.height / this.normHeight;
|
|
|
|
delete options.height;
|
|
|
|
} else {
|
|
|
|
this._scale = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._worldWidth = this._scale;
|
|
|
|
this._worldHeight = this.normHeight * this._scale;
|
|
|
|
|
|
|
|
$.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.
|
2014-08-07 00:48:18 +04:00
|
|
|
lastResetTime: 0, // Last time for which the tiledImage was reset.
|
|
|
|
midUpdate: false, // Is the tiledImage currently updating the viewport?
|
|
|
|
updateAgain: true, // Does the tiledImage need to update the viewport again?
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
//configurable settings
|
|
|
|
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,
|
|
|
|
debugMode: $.DEFAULT_SETTINGS.debugMode,
|
|
|
|
crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy
|
|
|
|
|
|
|
|
}, options );
|
|
|
|
};
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
$.TiledImage.prototype = /** @lends OpenSeadragon.TiledImage.prototype */{
|
2014-08-07 00:38:56 +04:00
|
|
|
/**
|
2014-11-04 22:53:39 +03:00
|
|
|
* @returns {Boolean} Whether the TiledImage is scheduled for an update at the
|
|
|
|
* soonest possible opportunity.
|
2014-08-07 00:38:56 +04:00
|
|
|
*/
|
|
|
|
needsUpdate: function() {
|
|
|
|
return this.updateAgain;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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() {
|
2014-08-07 00:48:18 +04:00
|
|
|
this._tileCache.clearTilesFor(this);
|
2014-08-07 00:38:56 +04:00
|
|
|
this.lastResetTime = $.now();
|
|
|
|
this.updateAgain = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2014-08-07 00:48:18 +04:00
|
|
|
* Forces the TiledImage to update.
|
2014-08-07 00:38:56 +04:00
|
|
|
*/
|
|
|
|
update: function() {
|
|
|
|
this.midUpdate = true;
|
|
|
|
updateViewport( this );
|
|
|
|
this.midUpdate = false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
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() {
|
2014-08-07 00:48:18 +04:00
|
|
|
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 world coordinates.
|
|
|
|
*/
|
2014-08-13 03:04:55 +04:00
|
|
|
getWorldBounds: function() {
|
|
|
|
return new $.Rect( this._worldX, this._worldY, this._worldWidth, this._worldHeight );
|
2014-08-19 03:04:49 +04:00
|
|
|
},
|
|
|
|
|
2014-11-04 22:53:39 +03:00
|
|
|
/**
|
|
|
|
* @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels.
|
|
|
|
*/
|
2014-08-19 03:04:49 +04:00
|
|
|
getContentSize: function() {
|
|
|
|
return new $.Point(this.source.dimensions.x, this.source.dimensions.y);
|
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.
|
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
function updateViewport( tiledImage ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.updateAgain = 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 ),
|
2014-08-07 00:38:56 +04:00
|
|
|
viewportTL = viewportBounds.getTopLeft(),
|
|
|
|
viewportBR = viewportBounds.getBottomRight(),
|
2014-08-07 00:48:18 +04:00
|
|
|
zeroRatioC = tiledImage.viewport.deltaPixelsFromPoints(
|
|
|
|
tiledImage.source.getPixelRatio( 0 ),
|
2014-08-07 00:38:56 +04:00
|
|
|
true
|
2014-08-07 00:48:18 +04:00
|
|
|
).x * tiledImage._scale,
|
2014-08-07 00:38:56 +04:00
|
|
|
lowestLevel = Math.max(
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.source.minLevel,
|
2014-08-07 00:38:56 +04:00
|
|
|
Math.floor(
|
2014-08-07 00:48:18 +04:00
|
|
|
Math.log( tiledImage.minZoomImageRatio ) /
|
2014-08-07 00:38:56 +04:00
|
|
|
Math.log( 2 )
|
|
|
|
)
|
|
|
|
),
|
|
|
|
highestLevel = Math.min(
|
2014-08-07 00:48:18 +04:00
|
|
|
Math.abs(tiledImage.source.maxLevel),
|
2014-08-07 00:38:56 +04:00
|
|
|
Math.abs(Math.floor(
|
2014-08-07 00:48:18 +04:00
|
|
|
Math.log( zeroRatioC / tiledImage.minPixelRatio ) /
|
2014-08-07 00:38:56 +04:00
|
|
|
Math.log( 2 )
|
|
|
|
))
|
|
|
|
),
|
2014-08-07 00:48:18 +04:00
|
|
|
degrees = tiledImage.viewport.degrees,
|
2014-08-07 00:38:56 +04:00
|
|
|
renderPixelRatioC,
|
|
|
|
renderPixelRatioT,
|
|
|
|
zeroRatioT,
|
|
|
|
optimalRatio,
|
|
|
|
levelOpacity,
|
|
|
|
levelVisibility;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
viewportTL.x -= tiledImage._worldX;
|
|
|
|
viewportTL.y -= tiledImage._worldY;
|
|
|
|
viewportBR.x -= tiledImage._worldX;
|
|
|
|
viewportBR.y -= tiledImage._worldY;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
// Reset tile's internal drawn state
|
2014-08-07 00:48:18 +04:00
|
|
|
while ( tiledImage.lastDrawn.length > 0 ) {
|
|
|
|
tile = tiledImage.lastDrawn.pop();
|
2014-08-07 00:38:56 +04:00
|
|
|
tile.beingDrawn = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Change bounds for rotation
|
|
|
|
if (degrees === 90 || degrees === 270) {
|
|
|
|
var rotatedBounds = viewportBounds.rotate( degrees );
|
|
|
|
viewportTL = rotatedBounds.getTopLeft();
|
|
|
|
viewportBR = rotatedBounds.getBottomRight();
|
2014-08-26 22:53:03 +04:00
|
|
|
} else if (degrees !== 0) {
|
|
|
|
// This is just an approximation.
|
|
|
|
var orthBounds = viewportBounds.rotate(90);
|
|
|
|
viewportBounds.x -= orthBounds.width / 2;
|
|
|
|
viewportBounds.y -= orthBounds.height / 2;
|
|
|
|
viewportBounds.width += orthBounds.width;
|
|
|
|
viewportBounds.height += orthBounds.height;
|
|
|
|
viewportTL = viewportBounds.getTopLeft();
|
|
|
|
viewportBR = viewportBounds.getBottomRight();
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
//Don't draw if completely outside of the viewport
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( !tiledImage.wrapHorizontal &&
|
|
|
|
( viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidth ) ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
return;
|
|
|
|
} else if
|
2014-08-07 00:48:18 +04:00
|
|
|
( !tiledImage.wrapVertical &&
|
|
|
|
( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeight ) ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate viewport rect / bounds
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( !tiledImage.wrapHorizontal ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
viewportTL.x = Math.max( viewportTL.x, 0 );
|
2014-08-07 00:48:18 +04:00
|
|
|
viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidth );
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( !tiledImage.wrapVertical ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
viewportTL.y = Math.max( viewportTL.y, 0 );
|
2014-08-07 00:48:18 +04:00
|
|
|
viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeight );
|
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
|
2014-08-07 00:48:18 +04:00
|
|
|
renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPoints(
|
|
|
|
tiledImage.source.getPixelRatio( level ),
|
2014-08-07 00:38:56 +04:00
|
|
|
true
|
2014-08-07 00:48:18 +04:00
|
|
|
).x * tiledImage._scale;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +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
|
2014-08-07 00:48:18 +04:00
|
|
|
renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPoints(
|
|
|
|
tiledImage.source.getPixelRatio( level ),
|
2014-08-07 00:38:56 +04:00
|
|
|
false
|
2014-08-07 00:48:18 +04:00
|
|
|
).x * tiledImage._scale;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
zeroRatioT = tiledImage.viewport.deltaPixelsFromPoints(
|
|
|
|
tiledImage.source.getPixelRatio(
|
2014-08-07 00:38:56 +04:00
|
|
|
Math.max(
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1,
|
2014-08-07 00:38:56 +04:00
|
|
|
0
|
|
|
|
)
|
|
|
|
),
|
|
|
|
false
|
2014-08-07 00:48:18 +04:00
|
|
|
).x * tiledImage._scale;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +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(
|
2014-08-07 00:48:18 +04:00
|
|
|
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
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( providesCoverage( tiledImage.coverage, level ) ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the actual drawing
|
2014-08-07 00:48:18 +04:00
|
|
|
drawTiles( tiledImage, tiledImage.lastDrawn );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
// Load the new 'best' tile
|
|
|
|
if ( best ) {
|
2014-08-07 00:48:18 +04:00
|
|
|
loadTile( tiledImage, best, currentTime );
|
2014-08-07 00:38:56 +04:00
|
|
|
// because we haven't finished drawing, so
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.updateAgain = true;
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-08-07 00:48:18 +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,
|
2014-08-07 00:48:18 +04:00
|
|
|
viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
|
2014-08-07 00:48:18 +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 {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.
|
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.viewer.raiseEvent( 'update-level', {
|
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-08-07 00:48:18 +04:00
|
|
|
tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scale ));
|
|
|
|
tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scale ));
|
|
|
|
numberOfTiles = tiledImage.source.getNumTiles( level );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
resetCoverage( tiledImage.coverage, level );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( !tiledImage.wrapHorizontal ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
|
|
|
|
}
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( !tiledImage.wrapVertical ) {
|
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(
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage,
|
2014-08-07 00:38:56 +04:00
|
|
|
drawLevel,
|
|
|
|
haveDrawn,
|
|
|
|
x, y,
|
|
|
|
level,
|
|
|
|
levelOpacity,
|
|
|
|
levelVisibility,
|
|
|
|
viewportCenter,
|
|
|
|
numberOfTiles,
|
|
|
|
currentTime,
|
|
|
|
best
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return best;
|
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
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,
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.source,
|
|
|
|
tiledImage.tilesMatrix,
|
2014-08-07 00:38:56 +04:00
|
|
|
currentTime,
|
|
|
|
numberOfTiles,
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage._worldWidth,
|
|
|
|
tiledImage._worldHeight
|
2014-08-07 00:38:56 +04:00
|
|
|
),
|
|
|
|
drawTile = drawLevel;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
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.Tile} tile
|
|
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.viewer.raiseEvent( 'update-tile', {
|
2014-08-07 00:38:56 +04:00
|
|
|
tile: tile
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
setCoverage( tiledImage.coverage, level, x, y, false );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
if ( !tile.exists ) {
|
|
|
|
return best;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( haveDrawn && !drawTile ) {
|
2014-08-07 00:48:18 +04:00
|
|
|
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,
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.source.tileOverlap,
|
|
|
|
tiledImage.viewport,
|
2014-08-07 00:38:56 +04:00
|
|
|
viewportCenter,
|
|
|
|
levelVisibility,
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage
|
2014-08-07 00:38:56 +04:00
|
|
|
);
|
|
|
|
|
|
|
|
if ( tile.loaded ) {
|
|
|
|
var needsUpdate = blendTile(
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage,
|
2014-08-07 00:38:56 +04:00
|
|
|
tile,
|
|
|
|
x, y,
|
|
|
|
level,
|
|
|
|
levelOpacity,
|
|
|
|
currentTime
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( needsUpdate ) {
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.updateAgain = 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,
|
|
|
|
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 );
|
|
|
|
|
|
|
|
bounds.x += worldWidth * ( x - xMod ) / numTiles.x;
|
|
|
|
bounds.y += worldHeight * ( y - yMod ) / numTiles.y;
|
|
|
|
|
|
|
|
tilesMatrix[ level ][ x ][ y ] = new $.Tile(
|
|
|
|
level,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
bounds,
|
|
|
|
exists,
|
|
|
|
url
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
tile = tilesMatrix[ level ][ x ][ y ];
|
|
|
|
tile.lastTouchTime = time;
|
|
|
|
|
|
|
|
return tile;
|
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
function loadTile( tiledImage, tile, time ) {
|
|
|
|
tile.loading = true;
|
2014-08-12 04:04:20 +04:00
|
|
|
tiledImage._imageLoader.addJob({
|
2014-08-07 00:48:18 +04:00
|
|
|
src: tile.url,
|
|
|
|
crossOriginPolicy: tiledImage.crossOriginPolicy,
|
|
|
|
callback: function( image ){
|
|
|
|
onTileLoad( tiledImage, tile, time, image );
|
|
|
|
}
|
|
|
|
});
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
function onTileLoad( tiledImage, tile, time, image ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
tile.loading = false;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( tiledImage.midUpdate ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
$.console.warn( "Tile load callback in middle of drawing routine." );
|
|
|
|
return;
|
2014-08-07 00:48:18 +04:00
|
|
|
} else if ( !image ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
$.console.log( "Tile %s failed to load: %s", tile, tile.url );
|
2014-08-07 00:48:18 +04:00
|
|
|
if( !tiledImage.debugMode ){
|
2014-08-07 00:38:56 +04:00
|
|
|
tile.exists = false;
|
|
|
|
return;
|
|
|
|
}
|
2014-08-07 00:48:18 +04:00
|
|
|
} else if ( time < tiledImage.lastResetTime ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
$.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
tile.loaded = true;
|
|
|
|
tile.image = image;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
var cutoff = Math.ceil( Math.log( tiledImage.source.getTileSize(tile.level) ) / Math.log( 2 ) );
|
|
|
|
tiledImage._tileCache.cacheTile({
|
|
|
|
tile: tile,
|
|
|
|
cutoff: cutoff,
|
|
|
|
tiledImage: tiledImage
|
|
|
|
});
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.updateAgain = true;
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){
|
2014-08-07 00:38:56 +04:00
|
|
|
var boundsTL = tile.bounds.getTopLeft();
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
boundsTL.x *= tiledImage._scale;
|
|
|
|
boundsTL.y *= tiledImage._scale;
|
|
|
|
boundsTL.x += tiledImage._worldX;
|
|
|
|
boundsTL.y += tiledImage._worldY;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
var boundsSize = tile.bounds.getSize();
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
boundsSize.x *= tiledImage._scale;
|
|
|
|
boundsSize.y *= tiledImage._scale;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
var positionC = viewport.pixelFromPoint( boundsTL, true ),
|
|
|
|
positionT = viewport.pixelFromPoint( boundsTL, false ),
|
|
|
|
sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ),
|
|
|
|
sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ),
|
|
|
|
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
|
|
|
|
tileDistance = viewportCenter.distanceTo( tileCenter );
|
|
|
|
|
|
|
|
if ( !overlap ) {
|
|
|
|
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
tile.position = positionC;
|
|
|
|
tile.size = sizeC;
|
|
|
|
tile.distance = tileDistance;
|
|
|
|
tile.visibility = levelVisibility;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
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;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
if ( tiledImage.alwaysBlend ) {
|
2014-08-07 00:38:56 +04:00
|
|
|
opacity *= levelOpacity;
|
|
|
|
}
|
|
|
|
|
|
|
|
tile.opacity = opacity;
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.lastDrawn.push( tile );
|
2014-08-07 00:38:56 +04:00
|
|
|
|
|
|
|
if ( opacity == 1 ) {
|
2014-08-07 00:48:18 +04:00
|
|
|
setCoverage( tiledImage.coverage, level, x, y, 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;
|
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
function drawTiles( tiledImage, lastDrawn ){
|
2014-08-07 00:38:56 +04:00
|
|
|
var i,
|
|
|
|
tile,
|
|
|
|
tileKey,
|
|
|
|
viewer,
|
|
|
|
viewport,
|
|
|
|
position,
|
|
|
|
tileSource,
|
|
|
|
collectionTileSource;
|
|
|
|
|
|
|
|
// We need a callback to give image manipulation a chance to happen
|
|
|
|
var drawingHandler = function(args) {
|
2014-08-07 00:48:18 +04:00
|
|
|
if (tiledImage.viewer) {
|
2014-08-07 00:38:56 +04:00
|
|
|
/**
|
|
|
|
* This event is fired just before the tile is drawn giving the application a chance to alter the image.
|
|
|
|
*
|
2014-08-07 00:48:18 +04:00
|
|
|
* NOTE: This event is only fired when the tiledImage is using a <canvas>.
|
2014-08-07 00:38:56 +04:00
|
|
|
*
|
|
|
|
* @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
|
|
|
|
* @property {?Object} userData - 'context', 'tile' and 'rendered'.
|
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.viewer.raiseEvent('tile-drawing', args);
|
2014-08-07 00:38:56 +04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for ( i = lastDrawn.length - 1; i >= 0; i-- ) {
|
|
|
|
tile = lastDrawn[ i ];
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage._drawer.drawTile( tile );
|
|
|
|
tile.beingDrawn = true;
|
2014-08-07 00:38:56 +04:00
|
|
|
|
2014-08-07 00:48:18 +04:00
|
|
|
if( tiledImage.debugMode ){
|
2014-08-07 00:38:56 +04:00
|
|
|
try{
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i );
|
2014-08-07 00:38:56 +04:00
|
|
|
}catch(e){
|
|
|
|
$.console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-08-07 00:48:18 +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.Tile} tile
|
|
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
|
|
*/
|
2014-08-07 00:48:18 +04:00
|
|
|
tiledImage.viewer.raiseEvent( 'tile-drawn', {
|
2014-08-07 00:38:56 +04:00
|
|
|
tile: tile
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}( OpenSeadragon ));
|