Fixes for issues #198, #201, #202, and #203

This commit is contained in:
Mark Salsbery 2013-08-26 15:25:57 -07:00
parent 6ca934d0ca
commit 04a0197dc7
4 changed files with 712 additions and 699 deletions

View File

@ -32,123 +32,125 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
(function($){ (function ($) {
/**
* For use by classes which want to support custom, non-browser events.
* TODO: This is an aweful name! This thing represents an "event source",
* not an "event handler". PLEASE change the to EventSource. Also please
* change 'addHandler', 'removeHandler' and 'raiseEvent' to 'bind',
* 'unbind', and 'trigger' respectively. Finally add a method 'one' which
* automatically unbinds a listener after the first triggered event that
* matches.
* @class
*/
$.EventHandler = function() {
this.events = {};
};
$.EventHandler.prototype = {
/** /**
* Add an event handler for a given event. * For use by classes which want to support custom, non-browser events.
* @function * TODO: This is an awful name! This thing represents an "event source",
* @param {String} eventName - Name of event to register. * not an "event handler". PLEASE change the to EventSource. Also please
* @param {Function} handler - Function to call when event is triggered. * change 'addHandler', 'removeHandler' and 'raiseEvent' to 'bind',
*/ * 'unbind', and 'trigger' respectively. Finally add a method 'one' which
addHandler: function( eventName, handler ) { * automatically unbinds a listener after the first triggered event that
var events = this.events[ eventName ]; * matches.
if( !events ){ * @class
this.events[ eventName ] = events = []; */
} $.EventHandler = function () {
if( handler && $.isFunction( handler ) ){ this.events = {};
events[ events.length ] = handler; };
}
},
/** $.EventHandler.prototype = {
* Remove a specific event handler for a given event.
* @function /**
* @param {String} eventName - Name of event for which the handler is to be removed. * Add an event handler for a given event.
* @param {Function} handler - Function to be removed. * @function
*/ * @param {String} eventName - Name of event to register.
removeHandler: function( eventName, handler ) { * @param {Function} handler - Function to call when event is triggered.
var events = this.events[ eventName ], * @param {Object} optional userData - Arbitrary object to be passed to the handler.
*/
addHandler: function (eventName, handler, userData) {
var events = this.events[eventName];
if (!events) {
this.events[eventName] = events = [];
}
if (handler && $.isFunction(handler)) {
events[events.length] = { handler: handler, userData: userData || null };
}
},
/**
* Remove a specific event handler for a given event.
* @function
* @param {String} eventName - Name of event for which the handler is to be removed.
* @param {Function} handler - Function to be removed.
*/
removeHandler: function (eventName, handler) {
var events = this.events[eventName],
handlers = [], handlers = [],
i; i;
if ( !events ){ if (!events) {
return; return;
} }
if( $.isArray( events ) ){ if ($.isArray(events)) {
for( i = 0; i < events.length; i++ ){ for (i = 0; i < events.length; i++) {
if( events[ i ] !== handler ){ if (events[i].handler !== handler) {
handlers.push( events[ i ] ); handlers.push(events[i]);
}
}
this.events[eventName] = handlers;
}
},
/**
* Remove all event handlers for a given event type. If no type is given all
* event handlers for every event type are removed.
* @function
* @param {String} eventName - Name of event for which all handlers are to be removed.
*/
removeAllHandlers: function (eventName) {
if (eventName) {
this.events[eventName] = [];
} else {
for (var eventType in this.events) {
this.events[eventType] = [];
} }
} }
this.events[ eventName ] = handlers; },
}
},
/**
/** * Retrieve the list of all handlers registered for a given event.
* Remove all event handlers for a given event type. If no type is given all * @function
* event handlers for every event type are removed. * @param {String} eventName - Name of event to get handlers for.
* @function */
* @param {String} eventName - Name of event for which all handlers are to be removed. getHandler: function (eventName) {
*/ var events = this.events[eventName];
removeAllHandlers: function( eventName ) { if (!events || !events.length) {
if (eventName){ return null;
this.events[ eventName ] = [];
} else{
for (var eventType in this.events) {
this.events[eventType] = [];
} }
} events = events.length === 1 ?
}, [events[0]] :
Array.apply(null, events);
/** return function (source, args) {
* Retrive the list of all handlers registered for a given event. var i,
* @function
* @param {String} eventName - Name of event to get handlers for.
*/
getHandler: function( eventName ) {
var events = this.events[ eventName ];
if ( !events || !events.length ){
return null;
}
events = events.length === 1 ?
[ events[ 0 ] ] :
Array.apply( null, events );
return function( source, args ) {
var i,
length = events.length; length = events.length;
for ( i = 0; i < length; i++ ) { for (i = 0; i < length; i++) {
if( events[ i ] ){ if (events[i]) {
events[ i ]( source, args ); args.userData = events[i].userData;
events[i].handler(source, args);
}
} }
};
},
/**
* Trigger an event, optionally passing additional information.
* @function
* @param {String} eventName - Name of event to register.
* @param {Function} handler - Function to call when event is triggered.
*/
raiseEvent: function (eventName, eventArgs) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
var handler = this.getHandler(eventName);
if (handler) {
if (!eventArgs) {
eventArgs = {};
}
handler(this, eventArgs);
} }
};
},
/**
* Trigger an event, optionally passing additional information.
* @function
* @param {String} eventName - Name of event to register.
* @param {Function} handler - Function to call when event is triggered.
*/
raiseEvent: function( eventName, eventArgs ) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
var handler = this.getHandler( eventName );
if ( handler ) {
if ( !eventArgs ) {
eventArgs = {};
}
handler( this, eventArgs );
} }
} };
};
}( OpenSeadragon )); } (OpenSeadragon));

View File

@ -32,267 +32,278 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
(function( $ ){ (function ($) {
/** /**
* The LegacyTileSource allows simple, traditional image pyramids to be loaded * The LegacyTileSource allows simple, traditional image pyramids to be loaded
* into an OpenSeadragon Viewer. Basically, this translates to the historically * into an OpenSeadragon Viewer. Basically, this translates to the historically
* common practice of starting with a 'master' image, maybe a tiff for example, * common practice of starting with a 'master' image, maybe a tiff for example,
* and generating a set of 'service' images like one or more thumbnails, a medium * and generating a set of 'service' images like one or more thumbnails, a medium
* resolution image and a high resolution image in standard web formats like * resolution image and a high resolution image in standard web formats like
* png or jpg. * png or jpg.
* @class * @class
* @extends OpenSeadragon.TileSource * @extends OpenSeadragon.TileSource
* @param {Array} levels An array of file descriptions, each is an object with * @param {Array} levels An array of file descriptions, each is an object with
* a 'url', a 'width', and a 'height'. Overriding classes can expect more * a 'url', a 'width', and a 'height'. Overriding classes can expect more
* properties but these properties are sufficient for this implementation. * properties but these properties are sufficient for this implementation.
* Additionally, the levels are required to be listed in order from * Additionally, the levels are required to be listed in order from
* smallest to largest. * smallest to largest.
* @property {Number} aspectRatio * @property {Number} aspectRatio
* @property {Number} dimensions * @property {Number} dimensions
* @property {Number} tileSize * @property {Number} tileSize
* @property {Number} tileOverlap * @property {Number} tileOverlap
* @property {Number} minLevel * @property {Number} minLevel
* @property {Number} maxLevel * @property {Number} maxLevel
* @property {Array} levels * @property {Array} levels
*/ */
$.LegacyTileSource = function( levels ) { $.LegacyTileSource = function (levels) {
var options, var options,
width, width,
height; height;
if( $.isArray( levels ) ){ if ($.isArray(levels)) {
options = { options = {
type: 'legacy-image-pyramid', type: 'legacy-image-pyramid',
levels: levels levels: levels
}; };
} }
//clean up the levels to make sure we support all formats //clean up the levels to make sure we support all formats
options.levels = filterFiles( options.levels ); options.levels = filterFiles(options.levels);
width = options.levels[ options.levels.length - 1 ].width;
height = options.levels[ options.levels.length - 1 ].height;
$.extend( true, options, { if (options.levels.length > 0) {
width: width, width = options.levels[options.levels.length - 1].width;
height: height, height = options.levels[options.levels.length - 1].height;
tileSize: Math.max( height, width ), }
tileOverlap: 0, else {
minLevel: 0, width = 0;
maxLevel: options.levels.length - 1 height = 0;
}); $.console.error("No supported image formats found");
}
$.TileSource.apply( this, [ options ] ); $.extend(true, options, {
width: width,
height: height,
tileSize: Math.max(height, width),
tileOverlap: 0,
minLevel: 0,
maxLevel: options.levels.length > 0 ? options.levels.length - 1 : 0
});
this.levels = options.levels; $.TileSource.apply(this, [options]);
};
$.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, { this.levels = options.levels;
/** };
* Determine if the data and/or url imply the image service is supported by
* this tile source. $.extend($.LegacyTileSource.prototype, $.TileSource.prototype, {
* @function /**
* @name OpenSeadragon.LegacyTileSource.prototype.supports * Determine if the data and/or url imply the image service is supported by
* @param {Object|Array} data * this tile source.
* @param {String} optional - url * @function
*/ * @name OpenSeadragon.LegacyTileSource.prototype.supports
supports: function( data, url ){ * @param {Object|Array} data
return ( * @param {String} optional - url
*/
supports: function (data, url) {
return (
data.type && data.type &&
"legacy-image-pyramid" == data.type "legacy-image-pyramid" == data.type
) || ( ) || (
data.documentElement && data.documentElement &&
"legacy-image-pyramid" == data.documentElement.getAttribute('type') "legacy-image-pyramid" == data.documentElement.getAttribute('type')
); );
}, },
/** /**
* *
* @function * @function
* @name OpenSeadragon.LegacyTileSource.prototype.configure * @name OpenSeadragon.LegacyTileSource.prototype.configure
* @param {Object|XMLDocument} configuration - the raw configuration * @param {Object|XMLDocument} configuration - the raw configuration
* @param {String} dataUrl - the url the data was retreived from if any. * @param {String} dataUrl - the url the data was retreived from if any.
* @return {Object} options - A dictionary of keyword arguments sufficient * @return {Object} options - A dictionary of keyword arguments sufficient
* to configure this tile sources constructor. * to configure this tile sources constructor.
*/ */
configure: function( configuration, dataUrl ){ configure: function (configuration, dataUrl) {
var options; var options;
if( !$.isPlainObject(configuration) ){ if (!$.isPlainObject(configuration)) {
options = configureFromXML( this, configuration ); options = configureFromXML(this, configuration);
}else{ } else {
options = configureFromObject( this, configuration ); options = configureFromObject(this, configuration);
}
return options;
},
/**
* @function
* @name OpenSeadragon.LegacyTileSource.prototype.getLevelScale
* @param {Number} level
*/
getLevelScale: function (level) {
var levelScale = NaN;
if (this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel) {
levelScale =
this.levels[level].width /
this.levels[this.maxLevel].width;
}
return levelScale;
},
/**
* @function
* @name OpenSeadragon.LegacyTileSource.prototype.getNumTiles
* @param {Number} level
*/
getNumTiles: function (level) {
var scale = this.getLevelScale(level);
if (scale) {
return new $.Point(1, 1);
} else {
return new $.Point(0, 0);
}
},
/**
* @function
* @name OpenSeadragon.LegacyTileSource.prototype.getTileAtPoint
* @param {Number} level
* @param {OpenSeadragon.Point} point
*/
getTileAtPoint: function (level, point) {
return new $.Point(0, 0);
},
/**
* 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
* @name OpenSeadragon.LegacyTileSource.prototype.getTileUrl
* @param {Number} level
* @param {Number} x
* @param {Number} y
* @throws {Error}
*/
getTileUrl: function (level, x, y) {
var url = null;
if (this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel) {
url = this.levels[level].url;
}
return url;
} }
});
return options;
},
/** /**
* @function * This method removes any files from the Array which dont conform to our
* @name OpenSeadragon.LegacyTileSource.prototype.getLevelScale * basic requirements for a 'level' in the LegacyTileSource.
* @param {Number} level * @private
*/ * @inner
getLevelScale: function( level ) { * @function
var levelScale = NaN; */
if ( level >= this.minLevel && level <= this.maxLevel ){ function filterFiles(files) {
levelScale = var filtered = [],
this.levels[ level ].width /
this.levels[ this.maxLevel ].width;
}
return levelScale;
},
/**
* @function
* @name OpenSeadragon.LegacyTileSource.prototype.getNumTiles
* @param {Number} level
*/
getNumTiles: function( level ) {
var scale = this.getLevelScale( level );
if ( scale ){
return new $.Point( 1, 1 );
} else {
return new $.Point( 0, 0 );
}
},
/**
* @function
* @name OpenSeadragon.LegacyTileSource.prototype.getTileAtPoint
* @param {Number} level
* @param {OpenSeadragon.Point} point
*/
getTileAtPoint: function( level, point ) {
return new $.Point( 0, 0 );
},
/**
* 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
* @name OpenSeadragon.LegacyTileSource.prototype.getTileUrl
* @param {Number} level
* @param {Number} x
* @param {Number} y
* @throws {Error}
*/
getTileUrl: function( level, x, y ) {
var url = null;
if( level >= this.minLevel && level <= this.maxLevel ){
url = this.levels[ level ].url;
}
return url;
}
});
/**
* This method removes any files from the Array which dont conform to our
* basic requirements for a 'level' in the LegacyTileSource.
* @private
* @inner
* @function
*/
function filterFiles( files ){
var filtered = [],
file, file,
i; i;
for( i = 0; i < files.length; i++ ){ for (i = 0; i < files.length; i++) {
file = files[ i ]; file = files[i];
if( file.height && if (file.height &&
file.width && file.width &&
file.url && ( file.url && (
file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || ( file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || (
file.mimetype && file.mimetype &&
file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/) file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/)
) )
) ){ )) {
//This is sufficient to serve as a level //This is sufficient to serve as a level
filtered.push({ filtered.push({
url: file.url, url: file.url,
width: Number( file.width ), width: Number(file.width),
height: Number( file.height ) height: Number(file.height)
}); });
}
else {
$.console.error('Unsupported image format: %s', file.url ? file.url : '<no URL>');
}
} }
return filtered.sort(function (a, b) {
return a.height - b.height;
});
} }
return filtered.sort(function(a,b){ /**
return a.height - b.height; * @private
}); * @inner
* @function
*/
function configureFromXML(tileSource, xmlDoc) {
} if (!xmlDoc || !xmlDoc.documentElement) {
throw new Error($.getString("Errors.Xml"));
}
/** var root = xmlDoc.documentElement,
* @private rootName = root.tagName,
* @inner conf = null,
* @function levels = [],
*/
function configureFromXML( tileSource, xmlDoc ){
if ( !xmlDoc || !xmlDoc.documentElement ) {
throw new Error( $.getString( "Errors.Xml" ) );
}
var root = xmlDoc.documentElement,
rootName = root.tagName,
conf = null,
levels = [],
level, level,
i; i;
if ( rootName == "image" ) { if (rootName == "image") {
try { try {
conf = { conf = {
type: root.getAttribute( "type" ), type: root.getAttribute("type"),
levels: [] levels: []
}; };
levels = root.getElementsByTagName( "level" ); levels = root.getElementsByTagName("level");
for ( i = 0; i < levels.length; i++ ) { for (i = 0; i < levels.length; i++) {
level = levels[ i ]; level = levels[i];
conf.levels .push({ conf.levels.push({
url: level.getAttribute( "url" ), url: level.getAttribute("url"),
width: parseInt( level.getAttribute( "width" ), 10 ), width: parseInt(level.getAttribute("width"), 10),
height: parseInt( level.getAttribute( "height" ), 10 ) height: parseInt(level.getAttribute("height"), 10)
}); });
} }
return configureFromObject( tileSource, conf ); return configureFromObject(tileSource, conf);
} catch ( e ) { } catch (e) {
throw (e instanceof Error) ? throw (e instanceof Error) ?
e : e :
new Error( 'Unknown error parsing Legacy Image Pyramid XML.' ); new Error('Unknown error parsing Legacy Image Pyramid XML.');
}
} else if (rootName == "collection") {
throw new Error('Legacy Image Pyramid Collections not yet supported.');
} else if (rootName == "error") {
throw new Error('Error: ' + xmlDoc);
} }
} else if ( rootName == "collection" ) {
throw new Error( 'Legacy Image Pyramid Collections not yet supported.' ); throw new Error('Unknown element ' + rootName);
} else if ( rootName == "error" ) {
throw new Error( 'Error: ' + xmlDoc );
} }
throw new Error( 'Unknown element ' + rootName ); /**
} * @private
* @inner
* @function
*/
function configureFromObject(tileSource, configuration) {
/** return configuration.levels;
* @private
* @inner
* @function
*/
function configureFromObject( tileSource, configuration ){
return configuration.levels; }
} } (OpenSeadragon));
}( OpenSeadragon ));

View File

@ -32,247 +32,247 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
(function( $ ){ (function ($) {
/** /**
* The TileSource contains the most basic implementation required to create a * 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 * smooth transition between layer in an image pyramid. It has only a single key
* interface that must be implemented to complete it key functionality: * interface that must be implemented to complete it key functionality:
* 'getTileUrl'. It also has several optional interfaces that can be * 'getTileUrl'. It also has several optional interfaces that can be
* implemented if a new TileSource wishes to support configuration via a simple * implemented if a new TileSource wishes to support configuration via a simple
* object or array ('configure') and if the tile source supports or requires * 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, * configuration via retreival of a document on the network ala AJAX or JSONP,
* ('getImageInfo'). * ('getImageInfo').
* <br/> * <br/>
* By default the image pyramid is split into N layers where the images longest * 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 * side in M (in pixels), where N is the smallest integer which satisfies
* <strong>2^(N+1) >= M</strong>. * <strong>2^(N+1) >= M</strong>.
* @class * @class
* @extends OpenSeadragon.EventHandler * @extends OpenSeadragon.EventHandler
* @param {Number|Object|Array|String} width * @param {Number|Object|Array|String} width
* If more than a single argument is supplied, the traditional use of * If more than a single argument is supplied, the traditional use of
* positional parameters is supplied and width is expected to be the width * 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 * 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 * it is an Object or Array, the construction is assumed to occur through
* the extending classes implementation of 'configure'. Finally if only a * the extending classes implementation of 'configure'. Finally if only a
* single argument is supplied and it is a String, the extending class is * single argument is supplied and it is a String, the extending class is
* expected to implement 'getImageInfo' and 'configure'. * expected to implement 'getImageInfo' and 'configure'.
* @param {Number} height * @param {Number} height
* Width of the source image at max resolution in pixels. * Width of the source image at max resolution in pixels.
* @param {Number} tileSize * @param {Number} tileSize
* The size of the tiles to assumed to make up each pyramid layer in pixels. * 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 * Tile size determines the point at which the image pyramid must be
* divided into a matrix of smaller images. * divided into a matrix of smaller images.
* @param {Number} tileOverlap * @param {Number} tileOverlap
* The number of pixels each tile is expected to overlap touching tiles. * The number of pixels each tile is expected to overlap touching tiles.
* @param {Number} minLevel * @param {Number} minLevel
* The minimum level to attempt to load. * The minimum level to attempt to load.
* @param {Number} maxLevel * @param {Number} maxLevel
* The maximum level to attempt to load. * The maximum level to attempt to load.
* @property {Number} aspectRatio * @property {Number} aspectRatio
* Ratio of width to height * Ratio of width to height
* @property {OpenSeadragon.Point} dimensions * @property {OpenSeadragon.Point} dimensions
* Vector storing x and y dimensions ( width and height respectively ). * Vector storing x and y dimensions ( width and height respectively ).
* @property {Number} tileSize * @property {Number} tileSize
* The size of the image tiles used to compose the image. * The size of the image tiles used to compose the image.
* @property {Number} tileOverlap * @property {Number} tileOverlap
* The overlap in pixels each tile shares with it's adjacent neighbors. * The overlap in pixels each tile shares with it's adjacent neighbors.
* @property {Number} minLevel * @property {Number} minLevel
* The minimum pyramid level this tile source supports or should attempt to load. * The minimum pyramid level this tile source supports or should attempt to load.
* @property {Number} maxLevel * @property {Number} maxLevel
* The maximum pyramid level this tile source supports or should attempt to load. * The maximum pyramid level this tile source supports or should attempt to load.
*/ */
$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { $.TileSource = function (width, height, tileSize, tileOverlap, minLevel, maxLevel) {
var callback = null, var callback = null,
args = arguments, args = arguments,
options, options,
i; i;
if( $.isPlainObject( width ) ){ if ($.isPlainObject(width)) {
options = width; options = width;
}else{ } else {
options = { options = {
width: args[0], width: args[0],
height: args[1], height: args[1],
tileSize: args[2], tileSize: args[2],
tileOverlap: args[3], tileOverlap: args[3],
minLevel: args[4], minLevel: args[4],
maxLevel: args[5] 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 ] ) ){ //Tile sources supply some events, namely 'ready' when they must be configured
//in case the getImageInfo method is overriden and/or implies an //by asyncronously fetching their configuration data.
//async mechanism set some safe defaults first $.EventHandler.call(this);
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 { //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);
//explicit configuration via positional args in constructor //Any functions that are passed as arguments are bound to the ready callback
//or the more idiomatic 'options' object /*jshint loopfunc:true*/
this.ready = true; for (i = 0; i < arguments.length; i++) {
this.aspectRatio = ( options.width && options.height ) ? if ($.isFunction(arguments[i])) {
( options.width / options.height ) : 1; callback = arguments[i];
this.dimensions = new $.Point( options.width, options.height ); this.addHandler('ready', function (placeHolderSource, placeHolderArgs) {
this.tileSize = options.tileSize ? options.tileSize : 0; callback(placeHolderArgs.tileSource);
this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0; });
this.minLevel = options.minLevel ? options.minLevel : 0; //only one callback per constructor
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; break;
} }
} }
return Math.max( 0, i - 1 );
},
/** if ('string' == $.type(arguments[0])) {
* @function //in case the getImageInfo method is overriden and/or implies an
* @param {Number} level //async mechanism set some safe defaults first
* @param {OpenSeadragon.Point} point this.aspectRatio = 1;
*/ this.dimensions = new $.Point(10, 10);
getTileAtPoint: function( level, point ) { this.tileSize = 0;
var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ), this.tileOverlap = 0;
tx = Math.floor( pixel.x / this.tileSize ), this.minLevel = 0;
ty = Math.floor( pixel.y / this.tileSize ); this.maxLevel = 0;
this.ready = false;
//configuration via url implies the extending class
//implements and 'configure'
this.getImageInfo(arguments[0]);
return new $.Point( tx, ty ); } else {
},
/** //explicit configuration via positional args in constructor
* @function //or the more idiomatic 'options' object
* @param {Number} level this.ready = true;
* @param {Number} x this.aspectRatio = (options.width && options.height) ?
* @param {Number} y (options.width / options.height) : 1;
*/ this.dimensions = new $.Point(options.width, options.height);
getTileBounds: function( level, x, y ) { this.tileSize = options.tileSize ? options.tileSize : 0;
var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap, this.minLevel = options.minLevel ? options.minLevel : 0;
py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap, this.maxLevel = (undefined !== options.maxLevel && null !== options.maxLevel) ?
sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap, options.maxLevel : (
sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap, (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; scale = 1.0 / dimensionsScaled.x;
sx = Math.min( sx, dimensionsScaled.x - px ); sx = Math.min(sx, dimensionsScaled.x - px);
sy = Math.min( sy, dimensionsScaled.y - py ); sy = Math.min(sy, dimensionsScaled.y - py);
return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); return new $.Rect(px * scale, py * scale, sx * scale, sy * scale);
}, },
/** /**
* Responsible for retrieving, and caching the * Responsible for retrieving, and caching the
* image metadata pertinent to this TileSources implementation. * image metadata pertinent to this TileSources implementation.
* @function * @function
* @param {String} url * @param {String} url
* @throws {Error} * @throws {Error}
*/ */
getImageInfo: function( url ) { getImageInfo: function (url) {
var _this = this, var _this = this,
callbackName, callbackName,
callback, callback,
readySource, readySource,
@ -282,195 +282,195 @@ $.TileSource.prototype = {
lastDot; lastDot;
if( url ) { if (url) {
urlParts = url.split( '/' ); urlParts = url.split('/');
filename = urlParts[ urlParts.length - 1 ]; filename = urlParts[urlParts.length - 1];
lastDot = filename.lastIndexOf( '.' ); lastDot = filename.lastIndexOf('.');
if ( lastDot > -1 ) { if (lastDot > -1) {
urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); urlParts[urlParts.length - 1] = filename.slice(0, lastDot);
} }
}
callback = function( data ){
var $TileSource = $.TileSource.determineType( _this, data, url );
if ( !$TileSource ) {
_this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } );
return;
} }
options = $TileSource.prototype.configure.apply( _this, [ data, url ]); callback = function (data) {
readySource = new $TileSource( options ); var $TileSource = $.TileSource.determineType(_this, data, url);
_this.ready = true; if (!$TileSource) {
_this.raiseEvent( 'ready', readySource ); _this.raiseEvent('open-failed', { message: "Unable to load TileSource", source: url });
}; return;
}
if( url.match(/\.js$/) ){ options = $TileSource.prototype.configure.apply(_this, [data, url]);
//TODO: Its not very flexible to require tile sources to end jsonp readySource = new $TileSource(options);
// request for info with a url that ends with '.js' but for _this.ready = true;
// now it's the only way I see to distinguish uniformly. _this.raiseEvent('ready', { tileSource: readySource });
callbackName = url.split( '/' ).pop().replace('.js',''); };
$.jsonp({
url: url, if (url.match(/\.js$/)) {
async: false, //TODO: Its not very flexible to require tile sources to end jsonp
callbackName: callbackName, // request for info with a url that ends with '.js' but for
callback: callback // now it's the only way I see to distinguish uniformly.
}); callbackName = url.split('/').pop().replace('.js', '');
} else { $.jsonp({
// request info via xhr asyncronously. url: url,
$.makeAjaxRequest( url, function( xhr ) { async: false,
var data = processResponse( xhr ); callbackName: callbackName,
callback( data ); callback: callback
}, function ( xhr ) {
_this.raiseEvent( 'open-failed', {
message: "HTTP " + xhr.status + " attempting to load TileSource",
source: url
}); });
}); } else {
} // request info via xhr asyncronously.
$.makeAjaxRequest(url, function (xhr) {
var data = processResponse(xhr);
callback(data);
}, function (xhr) {
_this.raiseEvent('open-failed', {
message: "HTTP " + xhr.status + " attempting to load TileSource",
source: url
});
});
}
}, },
/** /**
* Responsible determining if a the particular TileSource supports the * Responsible determining if a the particular TileSource supports the
* data format ( and allowed to apply logic against the url the data was * data format ( and allowed to apply logic against the url the data was
* loaded from, if any ). Overriding implementations are expected to do * loaded from, if any ). Overriding implementations are expected to do
* something smart with data and / or url to determine support. Also * something smart with data and / or url to determine support. Also
* understand that iteration order of TileSources is not guarunteed so * understand that iteration order of TileSources is not guarunteed so
* please make sure your data or url is expressive enough to ensure a simple * please make sure your data or url is expressive enough to ensure a simple
* and sufficient mechanisim for clear determination. * and sufficient mechanisim for clear determination.
* @function * @function
* @param {String|Object|Array|Document} data * @param {String|Object|Array|Document} data
* @param {String} url - the url the data was loaded * @param {String} url - the url the data was loaded
* from if any. * from if any.
* @return {Boolean} * @return {Boolean}
*/ */
supports: function( data, url ) { supports: function (data, url) {
return false; return false;
}, },
/** /**
* Responsible for parsing and configuring the * Responsible for parsing and configuring the
* image metadata pertinent to this TileSources implementation. * image metadata pertinent to this TileSources implementation.
* This method is not implemented by this class other than to throw an Error * 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 * announcing you have to implement it. Because of the variety of tile
* server technologies, and various specifications for building image * server technologies, and various specifications for building image
* pyramids, this method is here to allow easy integration. * pyramids, this method is here to allow easy integration.
* @function * @function
* @param {String|Object|Array|Document} data * @param {String|Object|Array|Document} data
* @param {String} url - the url the data was loaded * @param {String} url - the url the data was loaded
* from if any. * from if any.
* @return {Object} options - A dictionary of keyword arguments sufficient * @return {Object} options - A dictionary of keyword arguments sufficient
* to configure this tile sources constructor. * to configure this tile sources constructor.
* @throws {Error} * @throws {Error}
*/ */
configure: function( data, url ) { configure: function (data, url) {
throw new Error( "Method not implemented." ); throw new Error("Method not implemented.");
}, },
/** /**
* Responsible for retriving the url which will return an image for the * Responsible for retriving the url which will return an image for the
* region speified by the given x, y, and level components. * region speified by the given x, y, and level components.
* This method is not implemented by this class other than to throw an Error * 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 * announcing you have to implement it. Because of the variety of tile
* server technologies, and various specifications for building image * server technologies, and various specifications for building image
* pyramids, this method is here to allow easy integration. * pyramids, this method is here to allow easy integration.
* @function * @function
* @param {Number} level * @param {Number} level
* @param {Number} x * @param {Number} x
* @param {Number} y * @param {Number} y
* @throws {Error} * @throws {Error}
*/ */
getTileUrl: function( level, x, y ) { getTileUrl: function (level, x, y) {
throw new Error( "Method not implemented." ); throw new Error("Method not implemented.");
}, },
/** /**
* @function * @function
* @param {Number} level * @param {Number} level
* @param {Number} x * @param {Number} x
* @param {Number} y * @param {Number} y
*/ */
tileExists: function( level, x, y ) { tileExists: function (level, x, y) {
var numTiles = this.getNumTiles( level ); var numTiles = this.getNumTiles(level);
return level >= this.minLevel && return level >= this.minLevel &&
level <= this.maxLevel && level <= this.maxLevel &&
x >= 0 && x >= 0 &&
y >= 0 && y >= 0 &&
x < numTiles.x && x < numTiles.x &&
y < numTiles.y; y < numTiles.y;
} }
}; };
$.extend( true, $.TileSource.prototype, $.EventHandler.prototype ); $.extend(true, $.TileSource.prototype, $.EventHandler.prototype);
/** /**
* Decides whether to try to process the response as xml, json, or hand back * Decides whether to try to process the response as xml, json, or hand back
* the text * the text
* @eprivate * @eprivate
* @inner * @inner
* @function * @function
* @param {XMLHttpRequest} xhr - the completed network request * @param {XMLHttpRequest} xhr - the completed network request
*/ */
function processResponse( xhr ){ function processResponse(xhr) {
var responseText = xhr.responseText, var responseText = xhr.responseText,
status = xhr.status, status = xhr.status,
statusText, statusText,
data; data;
if ( !xhr ) { if (!xhr) {
throw new Error( $.getString( "Errors.Security" ) ); throw new Error($.getString("Errors.Security"));
} else if ( xhr.status !== 200 && xhr.status !== 0 ) { } else if (xhr.status !== 200 && xhr.status !== 0) {
status = xhr.status; status = xhr.status;
statusText = ( status == 404 ) ? statusText = (status == 404) ?
"Not Found" : "Not Found" :
xhr.statusText; xhr.statusText;
throw new Error( $.getString( "Errors.Status", status, statusText ) ); throw new Error($.getString("Errors.Status", status, statusText));
} }
if( responseText.match(/\s*<.*/) ){ if (responseText.match(/\s*<.*/)) {
try{ try {
data = ( xhr.responseXML && xhr.responseXML.documentElement ) ? data = (xhr.responseXML && xhr.responseXML.documentElement) ?
xhr.responseXML : xhr.responseXML :
$.parseXml( responseText ); $.parseXml(responseText);
} catch (e){ } catch (e) {
data = xhr.responseText; data = xhr.responseText;
} }
}else if( responseText.match(/\s*[\{\[].*/) ){ } else if (responseText.match(/\s*[\{\[].*/)) {
/*jshint evil:true*/ /*jshint evil:true*/
data = eval( '('+responseText+')' ); data = eval('(' + responseText + ')');
}else{ } else {
data = responseText; 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 ];
} }
return data;
} }
$.console.error( "No TileSource was able to open %s %s", url, 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];
}
}
$.console.error("No TileSource was able to open %s %s", url, data);
};
}( OpenSeadragon )); } (OpenSeadragon));

View File

@ -1463,7 +1463,7 @@ function onCanvasDrag( tracker, position, delta, shift ) {
this.viewport.applyConstraints(); this.viewport.applyConstraints();
} }
} }
this.raiseEvent( 'canvas-click', { this.raiseEvent( 'canvas-drag', {
tracker: tracker, tracker: tracker,
position: position, position: position,
delta: delta, delta: delta,