mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-29 00:26:10 +03:00
574 lines
20 KiB
JavaScript
574 lines
20 KiB
JavaScript
/*
|
|
* OpenSeadragon - TileSource
|
|
*
|
|
* 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( $ ){
|
|
|
|
|
|
/**
|
|
* @class TileSource
|
|
* @classdesc The TileSource contains the most basic implementation required to create a
|
|
* smooth transition between layer in an image pyramid. It has only a single key
|
|
* interface that must be implemented to complete it key functionality:
|
|
* '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
|
|
* configuration via retreival of a document on the network ala AJAX or JSONP,
|
|
* ('getImageInfo').
|
|
* <br/>
|
|
* By default the image pyramid is split into N layers where the images longest
|
|
* side in M (in pixels), where N is the smallest integer which satisfies
|
|
* <strong>2^(N+1) >= M</strong>.
|
|
*
|
|
* @memberof OpenSeadragon
|
|
* @extends OpenSeadragon.EventSource
|
|
* @param {Number|Object|Array|String} width
|
|
* If more than a single argument is supplied, the traditional use of
|
|
* positional parameters is supplied and width is expected to be the width
|
|
* source image at its max resolution in pixels. If a single argument is supplied and
|
|
* it is an Object or Array, the construction is assumed to occur through
|
|
* the extending classes implementation of 'configure'. Finally if only a
|
|
* single argument is supplied and it is a String, the extending class is
|
|
* expected to implement 'getImageInfo' and 'configure'.
|
|
* @param {Number} height
|
|
* Width of the source image at max resolution in pixels.
|
|
* @param {Number} tileSize
|
|
* 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.
|
|
* @param {Number} tileOverlap
|
|
* The number of pixels each tile is expected to overlap touching tiles.
|
|
* @param {Number} minLevel
|
|
* The minimum level to attempt to load.
|
|
* @param {Number} maxLevel
|
|
* The maximum level to attempt to load.
|
|
*/
|
|
$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
|
|
var callback = null,
|
|
args = arguments,
|
|
options,
|
|
i;
|
|
|
|
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]
|
|
};
|
|
}
|
|
|
|
//Tile sources supply some events, namely 'ready' when they must be configured
|
|
//by asynchronously fetching their configuration data.
|
|
$.EventSource.call( this );
|
|
|
|
//we allow options to override anything we dont treat as
|
|
//required via idiomatic options or which is functionally
|
|
//set depending on the state of the readiness of this tile
|
|
//source
|
|
$.extend( true, this, options );
|
|
|
|
//Any functions that are passed as arguments are bound to the ready callback
|
|
/*jshint loopfunc:true*/
|
|
for ( i = 0; i < arguments.length; i++ ) {
|
|
if ( $.isFunction( arguments[ i ] ) ) {
|
|
callback = arguments[ i ];
|
|
this.addHandler( 'ready', function ( event ) {
|
|
callback( event );
|
|
} );
|
|
//only one callback per constructor
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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#
|
|
*/
|
|
/**
|
|
* The size of the image tiles used to compose the image.
|
|
* Please note that tileSize may be deprecated in a future release.
|
|
* Instead the getTileSize(level) function should be used.
|
|
* @member {Number} tileSize
|
|
* @memberof OpenSeadragon.TileSource#
|
|
*/
|
|
/**
|
|
* The overlap in pixels each tile shares with its adjacent neighbors.
|
|
* @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#
|
|
*/
|
|
/**
|
|
*
|
|
* @member {Boolean} ready
|
|
* @memberof OpenSeadragon.TileSource#
|
|
*/
|
|
|
|
if( 'string' == $.type( arguments[ 0 ] ) ){
|
|
//in case the getImageInfo method is overriden and/or implies an
|
|
//async mechanism set some safe defaults first
|
|
this.aspectRatio = 1;
|
|
this.dimensions = new $.Point( 10, 10 );
|
|
this.tileSize = 0;
|
|
this.tileOverlap = 0;
|
|
this.minLevel = 0;
|
|
this.maxLevel = 0;
|
|
this.ready = false;
|
|
//configuration via url implies the extending class
|
|
//implements and 'configure'
|
|
this.getImageInfo( arguments[ 0 ] );
|
|
|
|
} else {
|
|
|
|
//explicit configuration via positional args in constructor
|
|
//or the more idiomatic 'options' object
|
|
this.ready = true;
|
|
this.aspectRatio = ( options.width && options.height ) ?
|
|
( options.width / options.height ) : 1;
|
|
this.dimensions = new $.Point( options.width, options.height );
|
|
this.tileSize = options.tileSize ? options.tileSize : 0;
|
|
this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
|
|
this.minLevel = options.minLevel ? options.minLevel : 0;
|
|
this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ?
|
|
options.maxLevel : (
|
|
( options.width && options.height ) ? Math.ceil(
|
|
Math.log( Math.max( options.width, options.height ) ) /
|
|
Math.log( 2 )
|
|
) : 0
|
|
);
|
|
if( callback && $.isFunction( callback ) ){
|
|
callback( this );
|
|
}
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
$.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{
|
|
|
|
/**
|
|
* Return the tileSize for a given level.
|
|
* Subclasses should override this if tileSizes can be different at different levels
|
|
* such as in IIIFTileSource. Code should use this function rather than reading
|
|
* from .tileSize directly. tileSize may be deprecated in a future release.
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getTileSize: function( level ) {
|
|
return this.tileSize;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getLevelScale: function( level ) {
|
|
|
|
// see https://github.com/openseadragon/openseadragon/issues/22
|
|
// we use the tilesources implementation of getLevelScale to generate
|
|
// a memoized re-implementation
|
|
var levelScaleCache = {},
|
|
i;
|
|
for( i = 0; i <= this.maxLevel; i++ ){
|
|
levelScaleCache[ i ] = 1 / Math.pow(2, this.maxLevel - i);
|
|
}
|
|
this.getLevelScale = function( _level ){
|
|
return levelScaleCache[ _level ];
|
|
};
|
|
return this.getLevelScale( level );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getNumTiles: function( level ) {
|
|
var scale = this.getLevelScale( level ),
|
|
x = Math.ceil( scale * this.dimensions.x / this.getTileSize(level) ),
|
|
y = Math.ceil( scale * this.dimensions.y / this.getTileSize(level) );
|
|
|
|
return new $.Point( x, y );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getPixelRatio: function( level ) {
|
|
var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
|
rx = 1.0 / imageSizeScaled.x,
|
|
ry = 1.0 / imageSizeScaled.y;
|
|
|
|
return new $.Point(rx, ry);
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getClosestLevel: function( rect ) {
|
|
var i,
|
|
tilesPerSide,
|
|
tiles;
|
|
for( i = this.minLevel; i < this.maxLevel; i++ ){
|
|
tiles = this.getNumTiles( i );
|
|
tilesPerSide = Math.floor( Math.max( rect.x, rect.y ) / this.getTileSize(i) );
|
|
if( Math.max( tiles.x, tiles.y ) + 1 >= tilesPerSide ){
|
|
break;
|
|
}
|
|
}
|
|
return Math.max( 0, i - 1 );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {OpenSeadragon.Point} point
|
|
*/
|
|
getTileAtPoint: function( level, point ) {
|
|
var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ),
|
|
tx = Math.floor( pixel.x / this.getTileSize(level) ),
|
|
ty = Math.floor( pixel.y / this.getTileSize(level) );
|
|
|
|
return new $.Point( tx, ty );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
getTileBounds: function( level, x, y ) {
|
|
var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
|
tileSize = this.getTileSize(level),
|
|
px = ( x === 0 ) ? 0 : tileSize * x - this.tileOverlap,
|
|
py = ( y === 0 ) ? 0 : tileSize * y - this.tileOverlap,
|
|
sx = tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
|
|
sy = tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap,
|
|
scale = 1.0 / dimensionsScaled.x;
|
|
|
|
sx = Math.min( sx, dimensionsScaled.x - px );
|
|
sy = Math.min( sy, dimensionsScaled.y - py );
|
|
|
|
return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
|
|
},
|
|
|
|
|
|
/**
|
|
* 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,
|
|
callbackName,
|
|
callback,
|
|
readySource,
|
|
options,
|
|
urlParts,
|
|
filename,
|
|
lastDot;
|
|
|
|
|
|
if( url ) {
|
|
urlParts = url.split( '/' );
|
|
filename = urlParts[ urlParts.length - 1 ];
|
|
lastDot = filename.lastIndexOf( '.' );
|
|
if ( lastDot > -1 ) {
|
|
urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
|
|
}
|
|
}
|
|
|
|
callback = function( data ){
|
|
if( typeof(data) === "string" ) {
|
|
data = $.parseXml( data );
|
|
}
|
|
var $TileSource = $.TileSource.determineType( _this, data, url );
|
|
if ( !$TileSource ) {
|
|
/**
|
|
* 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
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
*/
|
|
_this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } );
|
|
return;
|
|
}
|
|
|
|
options = $TileSource.prototype.configure.apply( _this, [ data, url ]);
|
|
readySource = new $TileSource( options );
|
|
_this.ready = true;
|
|
/**
|
|
* Raised when a TileSource is opened and initialized.
|
|
*
|
|
* @event ready
|
|
* @memberof OpenSeadragon.TileSource
|
|
* @type {object}
|
|
* @property {OpenSeadragon.TileSource} eventSource - A reference to the TileSource which raised the event.
|
|
* @property {Object} tileSource
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
*/
|
|
_this.raiseEvent( 'ready', { tileSource: readySource } );
|
|
};
|
|
|
|
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.
|
|
callbackName = url.split( '/' ).pop().replace('.js','');
|
|
$.jsonp({
|
|
url: url,
|
|
async: false,
|
|
callbackName: callbackName,
|
|
callback: callback
|
|
});
|
|
} else {
|
|
// request info via xhr asynchronously.
|
|
$.makeAjaxRequest( url, function( xhr ) {
|
|
var data = processResponse( xhr );
|
|
callback( data );
|
|
}, 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;
|
|
if ( typeof( exc ) == "undefined" || !exc.toString ) {
|
|
formattedExc = "Unknown error";
|
|
} else {
|
|
formattedExc = exc.toString();
|
|
}
|
|
|
|
msg = formattedExc + " attempting to load TileSource";
|
|
}
|
|
|
|
/***
|
|
* 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
|
|
* @property {?Object} userData - Arbitrary subscriber-defined object.
|
|
*/
|
|
_this.raiseEvent( 'open-failed', {
|
|
message: msg,
|
|
source: url
|
|
});
|
|
});
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
* @return {Object} options - A dictionary of keyword arguments sufficient
|
|
* to configure this tile sources constructor.
|
|
* @throws {Error}
|
|
*/
|
|
configure: function( data, url ) {
|
|
throw new Error( "Method not implemented." );
|
|
},
|
|
|
|
/**
|
|
* Responsible for retriving the url which will return an image for the
|
|
* region speified by the given x, y, and level components.
|
|
* 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." );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
tileExists: function( level, x, y ) {
|
|
var numTiles = this.getNumTiles( level );
|
|
return level >= this.minLevel &&
|
|
level <= this.maxLevel &&
|
|
x >= 0 &&
|
|
y >= 0 &&
|
|
x < numTiles.x &&
|
|
y < numTiles.y;
|
|
}
|
|
};
|
|
|
|
|
|
$.extend( true, $.TileSource.prototype, $.EventSource.prototype );
|
|
|
|
|
|
/**
|
|
* Decides whether to try to process the response as xml, json, or hand back
|
|
* the text
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {XMLHttpRequest} xhr - the completed network request
|
|
*/
|
|
function processResponse( xhr ){
|
|
var responseText = xhr.responseText,
|
|
status = xhr.status,
|
|
statusText,
|
|
data;
|
|
|
|
if ( !xhr ) {
|
|
throw new Error( $.getString( "Errors.Security" ) );
|
|
} else if ( xhr.status !== 200 && xhr.status !== 0 ) {
|
|
status = xhr.status;
|
|
statusText = ( status == 404 ) ?
|
|
"Not Found" :
|
|
xhr.statusText;
|
|
throw new Error( $.getString( "Errors.Status", status, statusText ) );
|
|
}
|
|
|
|
if( responseText.match(/\s*<.*/) ){
|
|
try{
|
|
data = ( xhr.responseXML && xhr.responseXML.documentElement ) ?
|
|
xhr.responseXML :
|
|
$.parseXml( responseText );
|
|
} catch (e){
|
|
data = xhr.responseText;
|
|
}
|
|
}else if( responseText.match(/\s*[\{\[].*/) ){
|
|
/*jshint evil:true*/
|
|
data = eval( '('+responseText+')' );
|
|
}else{
|
|
data = responseText;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines the TileSource Implementation by introspection of OpenSeadragon
|
|
* namespace, calling each TileSource implementation of 'isType'
|
|
* @private
|
|
* @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 ];
|
|
}
|
|
}
|
|
|
|
$.console.error( "No TileSource was able to open %s %s", url, data );
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|