openseadragon/src/tilesource.js

468 lines
16 KiB
JavaScript
Raw Normal View History

/*
* OpenSeadragon
*
* Copyright (C) 2009 CodePlex Foundation
*
* 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( $ ){
/**
* 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>.
* @class
* @extends OpenSeadragon.EventHandler
* @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 it's 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.
* @property {Number} aspectRatio
* Ratio of width to height
* @property {OpenSeadragon.Point} dimensions
* Vector storing x and y dimensions ( width and height respectively ).
* @property {Number} tileSize
* The size of the image tiles used to compose the image.
* @property {Number} tileOverlap
* The overlap in pixels each tile shares with it's adjacent neighbors.
* @property {Number} minLevel
* The minimum pyramid level this tile source supports or should attempt to load.
* @property {Number} maxLevel
* The maximum pyramid level this tile source supports or should attempt to load.
*/
$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
var _this = this,
callback = null,
readyHandler = 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 asyncronously fetching their configuration data.
$.EventHandler.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( placeHolderSource, readySource ){
callback( readySource );
});
//only one callback per constructor
break;
}
}
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 = {
/**
* @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.tileSize ),
y = Math.ceil( scale * this.dimensions.y / this.tileSize );
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 = Math.floor( Math.max( rect.x, rect.y ) / this.tileSize ),
tiles;
for( i = this.minLevel; i < this.maxLevel; i++ ){
tiles = this.getNumTiles( 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.tileSize ),
ty = Math.floor( pixel.y / this.tileSize );
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 ) ),
px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap,
py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap,
sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
sy = this.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,
error,
callbackName,
callback,
readySource,
options,
urlParts,
filename,
lastDot,
tilesUrl;
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 ){
var $TileSource = $.TileSource.determineType( _this, data, url );
options = $TileSource.prototype.configure.apply( _this, [ data, url ]);
readySource = new $TileSource( options );
_this.ready = true;
_this.raiseEvent( 'ready', 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 asyncronously.
$.makeAjaxRequest( url, function( xhr ) {
var data = processResponse( xhr );
callback( data );
});
}
},
/**
* 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, $.EventHandler.prototype );
/**
* Decides whether to try to process the response as xml, json, or hand back
* the text
* @eprivate
* @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'
* @eprivate
* @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 ];
}
}
};
}( OpenSeadragon ));