2013-05-01 08:46:16 +04:00
|
|
|
/*
|
2013-05-14 08:00:24 +04:00
|
|
|
* OpenSeadragon - TileSource
|
2013-05-01 08:46:16 +04:00
|
|
|
*
|
|
|
|
* Copyright (C) 2009 CodePlex Foundation
|
2013-05-14 07:32:09 +04:00
|
|
|
* Copyright (C) 2010-2013 OpenSeadragon contributors
|
2013-05-01 08:46:16 +04:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
(function( $ ){
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-11-25 20:48:44 +04:00
|
|
|
* @class TileSource
|
|
|
|
* @classdesc The TileSource contains the most basic implementation required to create a
|
2015-01-03 03:07:11 +03:00
|
|
|
* smooth transition between layers in an image pyramid. It has only a single key
|
|
|
|
* interface that must be implemented to complete its key functionality:
|
2013-08-27 02:47:21 +04:00
|
|
|
* 'getTileUrl'. It also has several optional interfaces that can be
|
|
|
|
* implemented if a new TileSource wishes to support configuration via a simple
|
|
|
|
* object or array ('configure') and if the tile source supports or requires
|
2015-01-03 03:07:11 +03:00
|
|
|
* configuration via retrieval of a document on the network ala AJAX or JSONP,
|
2013-08-27 02:47:21 +04:00
|
|
|
* ('getImageInfo').
|
|
|
|
* <br/>
|
2015-01-03 03:07:11 +03:00
|
|
|
* By default the image pyramid is split into N layers where the image's longest
|
2013-08-27 02:47:21 +04:00
|
|
|
* side in M (in pixels), where N is the smallest integer which satisfies
|
|
|
|
* <strong>2^(N+1) >= M</strong>.
|
2013-11-25 20:48:44 +04:00
|
|
|
*
|
2013-11-16 10:19:53 +04:00
|
|
|
* @memberof OpenSeadragon
|
2013-09-25 00:36:13 +04:00
|
|
|
* @extends OpenSeadragon.EventSource
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Object} options
|
|
|
|
* You can either specify a URL, or literally define the TileSource (by specifying
|
|
|
|
* width, height, tileSize, tileOverlap, minLevel, and maxLevel). For the former,
|
|
|
|
* the extending class is expected to implement 'getImageInfo' and 'configure'.
|
|
|
|
* For the latter, the construction is assumed to occur through
|
|
|
|
* the extending classes implementation of 'configure'.
|
|
|
|
* @param {String} [options.url]
|
|
|
|
* The URL for the data necessary for this TileSource.
|
2017-07-10 21:19:13 +03:00
|
|
|
* @param {String} [options.referenceStripThumbnailUrl]
|
2017-07-11 21:53:41 +03:00
|
|
|
* The URL for a thumbnail image to be used by the reference strip
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Function} [options.success]
|
|
|
|
* A function to be called upon successful creation.
|
|
|
|
* @param {Boolean} [options.ajaxWithCredentials]
|
|
|
|
* If this TileSource needs to make an AJAX call, this specifies whether to set
|
|
|
|
* the XHR's withCredentials (for accessing secure data).
|
2016-11-08 20:27:30 +03:00
|
|
|
* @param {Object} [options.ajaxHeaders]
|
2016-10-22 00:28:12 +03:00
|
|
|
* A set of headers to include in AJAX requests.
|
2022-01-13 00:31:13 +03:00
|
|
|
* @param {Boolean} [options.splitHashDataForPost]
|
|
|
|
* First occurrence of '#' in the options.url is used to split URL
|
|
|
|
* and the latter part is treated as POST data (applies to getImageInfo(...))
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Number} [options.width]
|
2013-08-27 02:47:21 +04:00
|
|
|
* Width of the source image at max resolution in pixels.
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Number} [options.height]
|
|
|
|
* Height of the source image at max resolution in pixels.
|
|
|
|
* @param {Number} [options.tileSize]
|
2013-08-27 02:47:21 +04:00
|
|
|
* The size of the tiles to assumed to make up each pyramid layer in pixels.
|
|
|
|
* Tile size determines the point at which the image pyramid must be
|
|
|
|
* divided into a matrix of smaller images.
|
2015-06-26 21:17:40 +03:00
|
|
|
* Use options.tileWidth and options.tileHeight to support non-square tiles.
|
|
|
|
* @param {Number} [options.tileWidth]
|
|
|
|
* The width of the tiles to assumed to make up each pyramid layer in pixels.
|
|
|
|
* @param {Number} [options.tileHeight]
|
|
|
|
* The height of the tiles to assumed to make up each pyramid layer in pixels.
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Number} [options.tileOverlap]
|
2013-08-27 02:47:21 +04:00
|
|
|
* The number of pixels each tile is expected to overlap touching tiles.
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Number} [options.minLevel]
|
2013-08-27 02:47:21 +04:00
|
|
|
* The minimum level to attempt to load.
|
2015-01-03 03:07:11 +03:00
|
|
|
* @param {Number} [options.maxLevel]
|
2013-08-27 02:47:21 +04:00
|
|
|
* The maximum level to attempt to load.
|
|
|
|
*/
|
|
|
|
$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
|
2015-01-03 02:45:46 +03:00
|
|
|
var _this = this;
|
|
|
|
|
|
|
|
var args = arguments,
|
2012-06-05 15:52:00 +04:00
|
|
|
options,
|
|
|
|
i;
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
if( $.isPlainObject( width ) ){
|
|
|
|
options = width;
|
|
|
|
}else{
|
|
|
|
options = {
|
|
|
|
width: args[0],
|
|
|
|
height: args[1],
|
|
|
|
tileSize: args[2],
|
|
|
|
tileOverlap: args[3],
|
|
|
|
minLevel: args[4],
|
|
|
|
maxLevel: args[5]
|
|
|
|
};
|
|
|
|
}
|
2012-06-05 15:52:00 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
//Tile sources supply some events, namely 'ready' when they must be configured
|
2013-12-14 01:11:00 +04:00
|
|
|
//by asynchronously fetching their configuration data.
|
2013-09-25 00:36:13 +04:00
|
|
|
$.EventSource.call( this );
|
2013-08-27 02:47:21 +04:00
|
|
|
|
2018-09-01 11:59:20 +03:00
|
|
|
//we allow options to override anything we don't treat as
|
2013-08-27 02:47:21 +04:00
|
|
|
//required via idiomatic options or which is functionally
|
|
|
|
//set depending on the state of the readiness of this tile
|
|
|
|
//source
|
|
|
|
$.extend( true, this, options );
|
|
|
|
|
2015-01-03 02:45:46 +03:00
|
|
|
if (!this.success) {
|
|
|
|
//Any functions that are passed as arguments are bound to the ready callback
|
|
|
|
for ( i = 0; i < arguments.length; i++ ) {
|
|
|
|
if ( $.isFunction( arguments[ i ] ) ) {
|
|
|
|
this.success = arguments[ i ];
|
|
|
|
//only one callback per constructor
|
|
|
|
break;
|
|
|
|
}
|
2012-06-05 15:52:00 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
2012-06-05 15:52:00 +04:00
|
|
|
|
2015-01-03 02:45:46 +03:00
|
|
|
if (this.success) {
|
|
|
|
this.addHandler( 'ready', function ( event ) {
|
|
|
|
_this.success( event );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2013-11-25 20:48:44 +04:00
|
|
|
/**
|
|
|
|
* Ratio of width to height
|
|
|
|
* @member {Number} aspectRatio
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* Vector storing x and y dimensions ( width and height respectively ).
|
|
|
|
* @member {OpenSeadragon.Point} dimensions
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
/**
|
2013-12-14 01:11:00 +04:00
|
|
|
* The overlap in pixels each tile shares with its adjacent neighbors.
|
2013-11-25 20:48:44 +04:00
|
|
|
* @member {Number} tileOverlap
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* The minimum pyramid level this tile source supports or should attempt to load.
|
|
|
|
* @member {Number} minLevel
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* The maximum pyramid level this tile source supports or should attempt to load.
|
|
|
|
* @member {Number} maxLevel
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
/**
|
2015-01-03 02:45:46 +03:00
|
|
|
*
|
2013-11-25 20:48:44 +04:00
|
|
|
* @member {Boolean} ready
|
|
|
|
* @memberof OpenSeadragon.TileSource#
|
|
|
|
*/
|
|
|
|
|
2020-06-26 02:01:14 +03:00
|
|
|
if( 'string' === $.type( arguments[ 0 ] ) ){
|
2015-01-03 02:45:46 +03:00
|
|
|
this.url = arguments[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.url) {
|
2018-09-01 11:59:20 +03:00
|
|
|
//in case the getImageInfo method is overridden and/or implies an
|
2013-08-27 02:47:21 +04:00
|
|
|
//async mechanism set some safe defaults first
|
|
|
|
this.aspectRatio = 1;
|
|
|
|
this.dimensions = new $.Point( 10, 10 );
|
2015-07-01 00:56:06 +03:00
|
|
|
this._tileWidth = 0;
|
|
|
|
this._tileHeight = 0;
|
2013-08-27 02:47:21 +04:00
|
|
|
this.tileOverlap = 0;
|
|
|
|
this.minLevel = 0;
|
|
|
|
this.maxLevel = 0;
|
|
|
|
this.ready = false;
|
|
|
|
//configuration via url implies the extending class
|
|
|
|
//implements and 'configure'
|
2015-01-03 02:45:46 +03:00
|
|
|
this.getImageInfo( this.url );
|
2013-08-27 02:47:21 +04:00
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
//explicit configuration via positional args in constructor
|
|
|
|
//or the more idiomatic 'options' object
|
|
|
|
this.ready = true;
|
2017-01-08 17:52:57 +03:00
|
|
|
this.aspectRatio = (options.width && options.height) ?
|
|
|
|
(options.width / options.height) : 1;
|
2013-08-27 02:47:21 +04:00
|
|
|
this.dimensions = new $.Point( options.width, options.height );
|
2016-04-21 17:02:02 +03:00
|
|
|
|
2015-07-14 21:49:52 +03:00
|
|
|
if ( this.tileSize ){
|
|
|
|
this._tileWidth = this._tileHeight = this.tileSize;
|
|
|
|
delete this.tileSize;
|
2015-07-01 00:56:06 +03:00
|
|
|
} else {
|
2015-07-14 21:49:52 +03:00
|
|
|
if( this.tileWidth ){
|
|
|
|
// We were passed tileWidth in options, but we want to rename it
|
|
|
|
// with a leading underscore to make clear that it is not safe to directly modify it
|
|
|
|
this._tileWidth = this.tileWidth;
|
|
|
|
delete this.tileWidth;
|
|
|
|
} else {
|
|
|
|
this._tileWidth = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( this.tileHeight ){
|
|
|
|
// See note above about renaming this.tileWidth
|
|
|
|
this._tileHeight = this.tileHeight;
|
|
|
|
delete this.tileHeight;
|
|
|
|
} else {
|
|
|
|
this._tileHeight = 0;
|
|
|
|
}
|
2015-07-01 00:56:06 +03:00
|
|
|
}
|
2016-04-21 17:02:02 +03:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
|
|
|
|
this.minLevel = options.minLevel ? options.minLevel : 0;
|
|
|
|
this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ?
|
2012-08-29 22:46:34 +04:00
|
|
|
options.maxLevel : (
|
2013-08-27 02:47:21 +04:00
|
|
|
( options.width && options.height ) ? Math.ceil(
|
|
|
|
Math.log( Math.max( options.width, options.height ) ) /
|
|
|
|
Math.log( 2 )
|
2012-08-29 22:46:34 +04:00
|
|
|
) : 0
|
|
|
|
);
|
2015-01-03 02:45:46 +03:00
|
|
|
if( this.success && $.isFunction( this.success ) ){
|
|
|
|
this.success( this );
|
2012-06-05 15:52:00 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
2012-06-05 15:52:00 +04:00
|
|
|
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
};
|
2012-06-05 15:52:00 +04:00
|
|
|
|
2016-01-25 00:09:18 +03:00
|
|
|
/** @lends OpenSeadragon.TileSource.prototype */
|
|
|
|
$.TileSource.prototype = {
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2014-07-26 03:31:13 +04:00
|
|
|
getTileSize: function( level ) {
|
2015-06-29 20:42:09 +03:00
|
|
|
$.console.error(
|
2018-10-29 00:02:45 +03:00
|
|
|
"[TileSource.getTileSize] is deprecated. " +
|
2015-06-29 20:42:09 +03:00
|
|
|
"Use TileSource.getTileWidth() and TileSource.getTileHeight() instead"
|
|
|
|
);
|
2015-07-01 00:56:06 +03:00
|
|
|
return this._tileWidth;
|
2014-07-26 03:31:13 +04:00
|
|
|
},
|
2016-04-21 17:02:02 +03:00
|
|
|
|
2015-06-29 20:42:09 +03:00
|
|
|
/**
|
|
|
|
* Return the tileWidth for a given level.
|
|
|
|
* Subclasses should override this if tileWidth can be different at different levels
|
|
|
|
* such as in IIIFTileSource. Code should use this function rather than reading
|
2015-07-14 21:49:52 +03:00
|
|
|
* from ._tileWidth directly.
|
2015-06-29 20:42:09 +03:00
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
getTileWidth: function( level ) {
|
2015-07-01 00:56:06 +03:00
|
|
|
if (!this._tileWidth) {
|
|
|
|
return this.getTileSize(level);
|
2015-06-29 20:42:09 +03:00
|
|
|
}
|
2015-07-01 00:56:06 +03:00
|
|
|
return this._tileWidth;
|
2015-06-29 20:42:09 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the tileHeight for a given level.
|
|
|
|
* Subclasses should override this if tileHeight can be different at different levels
|
|
|
|
* such as in IIIFTileSource. Code should use this function rather than reading
|
2015-07-14 21:49:52 +03:00
|
|
|
* from ._tileHeight directly.
|
2015-06-29 20:42:09 +03:00
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
getTileHeight: function( level ) {
|
2015-07-01 00:56:06 +03:00
|
|
|
if (!this._tileHeight) {
|
|
|
|
return this.getTileSize(level);
|
2015-06-29 20:42:09 +03:00
|
|
|
}
|
2015-07-01 00:56:06 +03:00
|
|
|
return this._tileHeight;
|
2015-06-29 20:42:09 +03:00
|
|
|
},
|
2014-07-26 03:31:13 +04:00
|
|
|
|
2021-11-23 10:58:00 +03:00
|
|
|
/**
|
2021-11-30 07:23:30 +03:00
|
|
|
* Set the maxLevel to the given level, and perform the memoization of
|
|
|
|
* getLevelScale with the new maxLevel. This function can be useful if the
|
|
|
|
* memoization is required before the first call of getLevelScale, or both
|
|
|
|
* memoized getLevelScale and maxLevel should be changed accordingly.
|
2021-11-23 10:58:00 +03:00
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
setMaxLevel: function( level ) {
|
|
|
|
this.maxLevel = level;
|
2021-11-30 05:40:06 +03:00
|
|
|
this._memoizeLevelScale();
|
2021-11-23 10:58:00 +03:00
|
|
|
},
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
getLevelScale: function( level ) {
|
2021-11-30 05:40:06 +03:00
|
|
|
// if getLevelScale is not memoized, we generate the memoized version
|
|
|
|
// at the first call and return the result
|
|
|
|
this._memoizeLevelScale();
|
2021-11-23 10:58:00 +03:00
|
|
|
return this.getLevelScale( level );
|
|
|
|
},
|
2013-08-27 02:47:21 +04:00
|
|
|
|
2021-11-23 10:58:00 +03:00
|
|
|
// private
|
2021-11-30 05:40:06 +03:00
|
|
|
_memoizeLevelScale: function() {
|
2013-08-27 02:47:21 +04:00
|
|
|
// see https://github.com/openseadragon/openseadragon/issues/22
|
|
|
|
// we use the tilesources implementation of getLevelScale to generate
|
|
|
|
// a memoized re-implementation
|
|
|
|
var levelScaleCache = {},
|
2013-08-27 02:25:57 +04:00
|
|
|
i;
|
2013-08-27 02:47:21 +04:00
|
|
|
for( i = 0; i <= this.maxLevel; i++ ){
|
|
|
|
levelScaleCache[ i ] = 1 / Math.pow(2, this.maxLevel - i);
|
|
|
|
}
|
|
|
|
this.getLevelScale = function( _level ){
|
|
|
|
return levelScaleCache[ _level ];
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
getNumTiles: function( level ) {
|
|
|
|
var scale = this.getLevelScale( level ),
|
2015-06-29 20:42:09 +03:00
|
|
|
x = Math.ceil( scale * this.dimensions.x / this.getTileWidth(level) ),
|
|
|
|
y = Math.ceil( scale * this.dimensions.y / this.getTileHeight(level) );
|
2013-08-27 02:47:21 +04:00
|
|
|
|
|
|
|
return new $.Point( x, y );
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
*/
|
|
|
|
getPixelRatio: function( level ) {
|
|
|
|
var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
2021-03-26 14:08:50 +03:00
|
|
|
rx = 1.0 / imageSizeScaled.x * $.pixelDensityRatio,
|
|
|
|
ry = 1.0 / imageSizeScaled.y * $.pixelDensityRatio;
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
return new $.Point(rx, ry);
|
|
|
|
},
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2013-03-06 14:51:31 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
/**
|
|
|
|
* @function
|
2017-05-19 21:12:01 +03:00
|
|
|
* @returns {Number} The highest level in this tile source that can be contained in a single tile.
|
2013-08-27 02:47:21 +04:00
|
|
|
*/
|
2017-05-16 23:49:59 +03:00
|
|
|
getClosestLevel: function() {
|
2013-08-27 02:47:21 +04:00
|
|
|
var i,
|
2015-06-29 20:42:09 +03:00
|
|
|
tiles;
|
2015-06-26 21:17:40 +03:00
|
|
|
|
2017-05-19 21:12:01 +03:00
|
|
|
for (i = this.minLevel + 1; i <= this.maxLevel; i++){
|
|
|
|
tiles = this.getNumTiles(i);
|
2017-05-16 23:49:59 +03:00
|
|
|
if (tiles.x > 1 || tiles.y > 1) {
|
2013-08-27 02:47:21 +04:00
|
|
|
break;
|
2013-03-06 14:51:31 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
2017-05-19 21:12:01 +03:00
|
|
|
|
|
|
|
return i - 1;
|
2013-08-27 02:47:21 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
* @param {OpenSeadragon.Point} point
|
|
|
|
*/
|
2016-10-09 15:05:22 +03:00
|
|
|
getTileAtPoint: function(level, point) {
|
2016-11-01 23:43:00 +03:00
|
|
|
var validPoint = point.x >= 0 && point.x <= 1 &&
|
|
|
|
point.y >= 0 && point.y <= 1 / this.aspectRatio;
|
2016-11-03 00:35:23 +03:00
|
|
|
$.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point.");
|
2016-11-01 23:43:00 +03:00
|
|
|
|
2016-10-09 15:05:22 +03:00
|
|
|
var widthScaled = this.dimensions.x * this.getLevelScale(level);
|
2016-11-01 23:43:00 +03:00
|
|
|
var pixelX = point.x * widthScaled;
|
|
|
|
var pixelY = point.y * widthScaled;
|
2016-10-23 23:25:16 +03:00
|
|
|
|
2016-11-09 00:20:12 +03:00
|
|
|
var x = Math.floor(pixelX / this.getTileWidth(level));
|
|
|
|
var y = Math.floor(pixelY / this.getTileHeight(level));
|
2016-10-23 23:25:16 +03:00
|
|
|
|
2017-02-04 17:37:50 +03:00
|
|
|
// When point.x == 1 or point.y == 1 / this.aspectRatio we want to
|
|
|
|
// return the last tile of the row/column
|
|
|
|
if (point.x >= 1) {
|
|
|
|
x = this.getNumTiles(level).x - 1;
|
|
|
|
}
|
2017-12-24 12:57:29 +03:00
|
|
|
var EPSILON = 1e-15;
|
2017-08-06 16:00:50 +03:00
|
|
|
if (point.y >= 1 / this.aspectRatio - EPSILON) {
|
2017-02-04 17:37:50 +03:00
|
|
|
y = this.getNumTiles(level).y - 1;
|
|
|
|
}
|
|
|
|
|
2016-10-09 15:05:22 +03:00
|
|
|
return new $.Point(x, y);
|
2013-08-27 02:47:21 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
* @param {Number} x
|
|
|
|
* @param {Number} y
|
2018-03-22 00:07:00 +03:00
|
|
|
* @param {Boolean} [isSource=false] Whether to return the source bounds of the tile.
|
|
|
|
* @returns {OpenSeadragon.Rect} Either where this tile fits (in normalized coordinates) or the
|
|
|
|
* portion of the tile to use as the source of the drawing operation (in pixels), depending on
|
|
|
|
* the isSource parameter.
|
2013-08-27 02:47:21 +04:00
|
|
|
*/
|
2018-03-22 00:07:00 +03:00
|
|
|
getTileBounds: function( level, x, y, isSource ) {
|
2013-08-27 02:47:21 +04:00
|
|
|
var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
2015-06-29 20:42:09 +03:00
|
|
|
tileWidth = this.getTileWidth(level),
|
|
|
|
tileHeight = this.getTileHeight(level),
|
|
|
|
px = ( x === 0 ) ? 0 : tileWidth * x - this.tileOverlap,
|
|
|
|
py = ( y === 0 ) ? 0 : tileHeight * y - this.tileOverlap,
|
|
|
|
sx = tileWidth + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
|
|
|
|
sy = tileHeight + ( y === 0 ? 1 : 2 ) * this.tileOverlap,
|
2012-01-24 07:48:45 +04:00
|
|
|
scale = 1.0 / dimensionsScaled.x;
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
sx = Math.min( sx, dimensionsScaled.x - px );
|
|
|
|
sy = Math.min( sy, dimensionsScaled.y - py );
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2018-03-22 00:07:00 +03:00
|
|
|
if (isSource) {
|
2018-04-02 21:09:23 +03:00
|
|
|
return new $.Rect(0, 0, sx, sy);
|
2018-03-22 00:07:00 +03:00
|
|
|
}
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
|
|
|
|
},
|
2013-06-19 21:33:25 +04:00
|
|
|
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
/**
|
|
|
|
* Responsible for retrieving, and caching the
|
|
|
|
* image metadata pertinent to this TileSources implementation.
|
|
|
|
* @function
|
|
|
|
* @param {String} url
|
|
|
|
* @throws {Error}
|
|
|
|
*/
|
|
|
|
getImageInfo: function( url ) {
|
|
|
|
var _this = this,
|
2012-06-05 15:52:00 +04:00
|
|
|
callbackName,
|
|
|
|
callback,
|
|
|
|
readySource,
|
|
|
|
options,
|
|
|
|
urlParts,
|
|
|
|
filename,
|
2013-06-19 01:55:19 +04:00
|
|
|
lastDot;
|
2012-06-05 15:52:00 +04:00
|
|
|
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
if( url ) {
|
|
|
|
urlParts = url.split( '/' );
|
|
|
|
filename = urlParts[ urlParts.length - 1 ];
|
|
|
|
lastDot = filename.lastIndexOf( '.' );
|
|
|
|
if ( lastDot > -1 ) {
|
|
|
|
urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
|
2013-06-28 02:10:23 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
2013-06-28 02:10:23 +04:00
|
|
|
|
2021-12-08 11:54:14 +03:00
|
|
|
var postData = null;
|
2022-01-13 00:37:59 +03:00
|
|
|
if (this.splitHashDataForPost) {
|
2022-01-13 00:31:13 +03:00
|
|
|
var hashIdx = url.indexOf("#");
|
|
|
|
if (hashIdx !== -1) {
|
|
|
|
postData = url.substring(hashIdx + 1);
|
|
|
|
url = url.substr(0, hashIdx);
|
|
|
|
}
|
2021-12-08 11:54:14 +03:00
|
|
|
}
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
callback = function( data ){
|
2018-05-11 20:53:16 +03:00
|
|
|
if( typeof (data) === "string" ) {
|
2013-09-25 00:54:54 +04:00
|
|
|
data = $.parseXml( data );
|
2013-09-07 20:08:39 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
var $TileSource = $.TileSource.determineType( _this, data, url );
|
|
|
|
if ( !$TileSource ) {
|
2013-11-16 10:19:53 +04:00
|
|
|
/**
|
2013-11-22 00:19:07 +04:00
|
|
|
* Raised when an error occurs loading a TileSource.
|
|
|
|
*
|
2013-11-16 10:19:53 +04:00
|
|
|
* @event open-failed
|
|
|
|
* @memberof OpenSeadragon.TileSource
|
|
|
|
* @type {object}
|
|
|
|
* @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
|
|
|
|
* @property {String} message
|
|
|
|
* @property {String} source
|
2013-11-18 18:56:32 +04:00
|
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
2013-11-16 10:19:53 +04:00
|
|
|
*/
|
2013-08-27 02:47:21 +04:00
|
|
|
_this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } );
|
|
|
|
return;
|
2013-08-27 02:25:57 +04:00
|
|
|
}
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2021-12-08 11:54:14 +03:00
|
|
|
options = $TileSource.prototype.configure.apply( _this, [ data, url, postData ]);
|
2015-01-03 02:45:46 +03:00
|
|
|
if (options.ajaxWithCredentials === undefined) {
|
|
|
|
options.ajaxWithCredentials = _this.ajaxWithCredentials;
|
|
|
|
}
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
readySource = new $TileSource( options );
|
|
|
|
_this.ready = true;
|
2013-11-16 10:19:53 +04:00
|
|
|
/**
|
2013-11-22 00:19:07 +04:00
|
|
|
* Raised when a TileSource is opened and initialized.
|
|
|
|
*
|
2013-11-16 10:19:53 +04:00
|
|
|
* @event ready
|
|
|
|
* @memberof OpenSeadragon.TileSource
|
|
|
|
* @type {object}
|
|
|
|
* @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
|
|
|
|
* @property {Object} tileSource
|
2013-11-18 18:56:32 +04:00
|
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
2013-11-16 10:19:53 +04:00
|
|
|
*/
|
2013-10-03 00:09:40 +04:00
|
|
|
_this.raiseEvent( 'ready', { tileSource: readySource } );
|
2013-08-27 02:47:21 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
if( url.match(/\.js$/) ){
|
|
|
|
//TODO: Its not very flexible to require tile sources to end jsonp
|
|
|
|
// request for info with a url that ends with '.js' but for
|
|
|
|
// now it's the only way I see to distinguish uniformly.
|
2017-01-08 17:52:57 +03:00
|
|
|
callbackName = url.split('/').pop().replace('.js', '');
|
2013-08-27 02:47:21 +04:00
|
|
|
$.jsonp({
|
|
|
|
url: url,
|
|
|
|
async: false,
|
|
|
|
callbackName: callbackName,
|
|
|
|
callback: callback
|
|
|
|
});
|
|
|
|
} else {
|
2013-12-14 01:11:00 +04:00
|
|
|
// request info via xhr asynchronously.
|
2015-01-03 02:45:46 +03:00
|
|
|
$.makeAjaxRequest( {
|
|
|
|
url: url,
|
2021-12-08 11:54:14 +03:00
|
|
|
postData: postData,
|
2015-01-03 02:45:46 +03:00
|
|
|
withCredentials: this.ajaxWithCredentials,
|
2016-11-08 20:27:30 +03:00
|
|
|
headers: this.ajaxHeaders,
|
2015-01-03 02:45:46 +03:00
|
|
|
success: function( xhr ) {
|
|
|
|
var data = processResponse( xhr );
|
|
|
|
callback( data );
|
|
|
|
},
|
|
|
|
error: function ( xhr, exc ) {
|
|
|
|
var msg;
|
|
|
|
|
|
|
|
/*
|
|
|
|
IE < 10 will block XHR requests to different origins. Any property access on the request
|
|
|
|
object will raise an exception which we'll attempt to handle by formatting the original
|
|
|
|
exception rather than the second one raised when we try to access xhr.status
|
|
|
|
*/
|
|
|
|
try {
|
|
|
|
msg = "HTTP " + xhr.status + " attempting to load TileSource";
|
|
|
|
} catch ( e ) {
|
|
|
|
var formattedExc;
|
2020-06-26 02:01:14 +03:00
|
|
|
if ( typeof ( exc ) === "undefined" || !exc.toString ) {
|
2015-01-03 02:45:46 +03:00
|
|
|
formattedExc = "Unknown error";
|
|
|
|
} else {
|
|
|
|
formattedExc = exc.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
msg = formattedExc + " attempting to load TileSource";
|
2013-08-30 00:15:52 +04:00
|
|
|
}
|
|
|
|
|
2015-01-03 02:45:46 +03:00
|
|
|
/***
|
|
|
|
* Raised when an error occurs loading a TileSource.
|
|
|
|
*
|
|
|
|
* @event open-failed
|
|
|
|
* @memberof OpenSeadragon.TileSource
|
|
|
|
* @type {object}
|
|
|
|
* @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
|
|
|
|
* @property {String} message
|
|
|
|
* @property {String} source
|
2022-03-23 12:22:13 +03:00
|
|
|
* @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
|
|
|
|
* see TileSrouce::getPostData) or null
|
2015-01-03 02:45:46 +03:00
|
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
|
|
*/
|
|
|
|
_this.raiseEvent( 'open-failed', {
|
|
|
|
message: msg,
|
2022-01-13 00:31:13 +03:00
|
|
|
source: url,
|
|
|
|
postData: postData
|
2015-01-03 02:45:46 +03:00
|
|
|
});
|
2013-08-30 00:15:52 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Responsible determining if a the particular TileSource supports the
|
|
|
|
* data format ( and allowed to apply logic against the url the data was
|
|
|
|
* loaded from, if any ). Overriding implementations are expected to do
|
|
|
|
* something smart with data and / or url to determine support. Also
|
|
|
|
* understand that iteration order of TileSources is not guarunteed so
|
|
|
|
* please make sure your data or url is expressive enough to ensure a simple
|
|
|
|
* and sufficient mechanisim for clear determination.
|
|
|
|
* @function
|
|
|
|
* @param {String|Object|Array|Document} data
|
|
|
|
* @param {String} url - the url the data was loaded
|
|
|
|
* from if any.
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
|
|
|
supports: function( data, url ) {
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Responsible for parsing and configuring the
|
|
|
|
* image metadata pertinent to this TileSources implementation.
|
|
|
|
* This method is not implemented by this class other than to throw an Error
|
|
|
|
* announcing you have to implement it. Because of the variety of tile
|
|
|
|
* server technologies, and various specifications for building image
|
|
|
|
* pyramids, this method is here to allow easy integration.
|
|
|
|
* @function
|
|
|
|
* @param {String|Object|Array|Document} data
|
|
|
|
* @param {String} url - the url the data was loaded
|
|
|
|
* from if any.
|
2022-01-21 22:57:14 +03:00
|
|
|
* @param {String} postData - HTTP POST data in k=v&k2=v2... form or null value obtained from
|
2022-01-20 20:03:08 +03:00
|
|
|
* the protocol URL after '#' sign if flag splitHashDataForPost set to 'true'
|
2013-08-27 02:47:21 +04:00
|
|
|
* @return {Object} options - A dictionary of keyword arguments sufficient
|
2022-01-13 00:31:13 +03:00
|
|
|
* to configure the tile source constructor (include all values you want to
|
|
|
|
* instantiate the TileSource subclass with - what _options_ object should contain).
|
2013-08-27 02:47:21 +04:00
|
|
|
* @throws {Error}
|
|
|
|
*/
|
2021-12-08 11:54:14 +03:00
|
|
|
configure: function( data, url, postData ) {
|
2013-08-27 02:47:21 +04:00
|
|
|
throw new Error( "Method not implemented." );
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-10-22 00:28:12 +03:00
|
|
|
* Responsible for retrieving the url which will return an image for the
|
2016-04-09 17:14:09 +03:00
|
|
|
* region specified by the given x, y, and level components.
|
2013-08-27 02:47:21 +04:00
|
|
|
* This method is not implemented by this class other than to throw an Error
|
|
|
|
* announcing you have to implement it. Because of the variety of tile
|
|
|
|
* server technologies, and various specifications for building image
|
|
|
|
* pyramids, this method is here to allow easy integration.
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
* @param {Number} x
|
|
|
|
* @param {Number} y
|
|
|
|
* @throws {Error}
|
|
|
|
*/
|
|
|
|
getTileUrl: function( level, x, y ) {
|
|
|
|
throw new Error( "Method not implemented." );
|
|
|
|
},
|
|
|
|
|
2021-12-08 11:54:14 +03:00
|
|
|
/**
|
|
|
|
* Must use AJAX in order to work, i.e. loadTilesWithAjax = true is set.
|
2022-03-23 12:22:13 +03:00
|
|
|
* If a value is returned, ajax issues POST request to the tile url.
|
|
|
|
* If null is returned, ajax issues GET request.
|
|
|
|
* The return value must comply to the header 'content type'.
|
|
|
|
*
|
|
|
|
* Examples (USED HEADER --> getTilePostData CODE):
|
|
|
|
* 'Content-type': 'application/x-www-form-urlencoded' -->
|
|
|
|
* return "key1=value=1&key2=value2";
|
|
|
|
*
|
|
|
|
* 'Content-type': 'application/x-www-form-urlencoded' -->
|
|
|
|
* return JSON.stringify({key: "value", number: 5});
|
|
|
|
*
|
|
|
|
* 'Content-type': 'multipart/form-data' -->
|
|
|
|
* let result = new FormData();
|
|
|
|
* result.append("data", myData);
|
|
|
|
* return result;
|
|
|
|
|
2021-12-08 11:54:14 +03:00
|
|
|
* @param level
|
|
|
|
* @param x
|
|
|
|
* @param y
|
2022-03-23 12:22:13 +03:00
|
|
|
* @return {* || null} post data to send with tile configuration request
|
2021-12-08 11:54:14 +03:00
|
|
|
*/
|
|
|
|
getTilePostData: function( level, x, y ) {
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2016-10-22 00:28:12 +03:00
|
|
|
/**
|
|
|
|
* Responsible for retrieving the headers which will be attached to the image request for the
|
|
|
|
* region specified by the given x, y, and level components.
|
|
|
|
* This option is only relevant if {@link OpenSeadragon.Options}.loadTilesWithAjax is set to true.
|
2016-12-19 08:39:32 +03:00
|
|
|
* The headers returned here will override headers specified at the Viewer or TiledImage level.
|
|
|
|
* Specifying a falsy value for a header will clear its existing value set at the Viewer or
|
|
|
|
* TiledImage level (if any).
|
2016-10-22 00:28:12 +03:00
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
* @param {Number} x
|
|
|
|
* @param {Number} y
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
2016-11-08 20:27:30 +03:00
|
|
|
getTileAjaxHeaders: function( level, x, y ) {
|
2016-10-22 00:28:12 +03:00
|
|
|
return {};
|
|
|
|
},
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
/**
|
|
|
|
* @function
|
|
|
|
* @param {Number} level
|
|
|
|
* @param {Number} x
|
|
|
|
* @param {Number} y
|
|
|
|
*/
|
|
|
|
tileExists: function( level, x, y ) {
|
|
|
|
var numTiles = this.getNumTiles( level );
|
2017-01-08 17:52:57 +03:00
|
|
|
return level >= this.minLevel &&
|
|
|
|
level <= this.maxLevel &&
|
|
|
|
x >= 0 &&
|
|
|
|
y >= 0 &&
|
|
|
|
x < numTiles.x &&
|
|
|
|
y < numTiles.y;
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
|
|
|
};
|
2011-12-06 07:50:25 +04:00
|
|
|
|
2012-06-05 15:52:00 +04:00
|
|
|
|
2013-09-25 00:36:13 +04:00
|
|
|
$.extend( true, $.TileSource.prototype, $.EventSource.prototype );
|
2012-06-05 15:52:00 +04:00
|
|
|
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
/**
|
|
|
|
* Decides whether to try to process the response as xml, json, or hand back
|
|
|
|
* the text
|
2013-11-16 10:19:53 +04:00
|
|
|
* @private
|
2013-08-27 02:47:21 +04:00
|
|
|
* @inner
|
|
|
|
* @function
|
|
|
|
* @param {XMLHttpRequest} xhr - the completed network request
|
|
|
|
*/
|
|
|
|
function processResponse( xhr ){
|
|
|
|
var responseText = xhr.responseText,
|
|
|
|
status = xhr.status,
|
2012-06-05 15:52:00 +04:00
|
|
|
statusText,
|
|
|
|
data;
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
if ( !xhr ) {
|
|
|
|
throw new Error( $.getString( "Errors.Security" ) );
|
|
|
|
} else if ( xhr.status !== 200 && xhr.status !== 0 ) {
|
|
|
|
status = xhr.status;
|
2020-06-26 02:01:14 +03:00
|
|
|
statusText = ( status === 404 ) ?
|
2013-06-19 21:33:25 +04:00
|
|
|
"Not Found" :
|
2012-06-05 15:52:00 +04:00
|
|
|
xhr.statusText;
|
2013-08-27 02:47:21 +04:00
|
|
|
throw new Error( $.getString( "Errors.Status", status, statusText ) );
|
|
|
|
}
|
2012-06-05 15:52:00 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
if( responseText.match(/\s*<.*/) ){
|
|
|
|
try{
|
|
|
|
data = ( xhr.responseXML && xhr.responseXML.documentElement ) ?
|
2013-06-19 21:33:25 +04:00
|
|
|
xhr.responseXML :
|
2013-08-27 02:47:21 +04:00
|
|
|
$.parseXml( responseText );
|
|
|
|
} catch (e){
|
|
|
|
data = xhr.responseText;
|
2012-06-05 15:52:00 +04:00
|
|
|
}
|
2020-06-30 20:25:38 +03:00
|
|
|
}else if( responseText.match(/\s*[{[].*/) ){
|
2017-05-11 00:51:59 +03:00
|
|
|
try{
|
|
|
|
data = $.parseJSON(responseText);
|
|
|
|
} catch(e){
|
|
|
|
data = responseText;
|
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}else{
|
|
|
|
data = responseText;
|
2012-06-05 15:52:00 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the TileSource Implementation by introspection of OpenSeadragon
|
|
|
|
* namespace, calling each TileSource implementation of 'isType'
|
2013-11-16 10:19:53 +04:00
|
|
|
* @private
|
2013-08-27 02:47:21 +04:00
|
|
|
* @inner
|
|
|
|
* @function
|
|
|
|
* @param {Object|Array|Document} data - the tile source configuration object
|
|
|
|
* @param {String} url - the url where the tile source configuration object was
|
|
|
|
* loaded from, if any.
|
|
|
|
*/
|
|
|
|
$.TileSource.determineType = function( tileSource, data, url ){
|
|
|
|
var property;
|
|
|
|
for( property in OpenSeadragon ){
|
|
|
|
if( property.match(/.+TileSource$/) &&
|
|
|
|
$.isFunction( OpenSeadragon[ property ] ) &&
|
|
|
|
$.isFunction( OpenSeadragon[ property ].prototype.supports ) &&
|
|
|
|
OpenSeadragon[ property ].prototype.supports.call( tileSource, data, url )
|
|
|
|
){
|
|
|
|
return OpenSeadragon[ property ];
|
2012-06-05 15:52:00 +04:00
|
|
|
}
|
2013-08-27 02:47:21 +04:00
|
|
|
}
|
2013-06-28 02:10:23 +04:00
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
$.console.error( "No TileSource was able to open %s %s", url, data );
|
2020-06-26 02:01:14 +03:00
|
|
|
|
|
|
|
return null;
|
2013-08-27 02:47:21 +04:00
|
|
|
};
|
2012-06-05 15:52:00 +04:00
|
|
|
|
|
|
|
|
2013-08-27 02:47:21 +04:00
|
|
|
}( OpenSeadragon ));
|