From 04a0197dc7926ed50c546431ba401b1d063fd734 Mon Sep 17 00:00:00 2001 From: Mark Salsbery Date: Mon, 26 Aug 2013 15:25:57 -0700 Subject: [PATCH] Fixes for issues #198, #201, #202, and #203 --- src/eventhandler.js | 214 +++++------ src/legacytilesource.js | 431 ++++++++++++----------- src/tilesource.js | 764 ++++++++++++++++++++-------------------- src/viewer.js | 2 +- 4 files changed, 712 insertions(+), 699 deletions(-) diff --git a/src/eventhandler.js b/src/eventhandler.js index 8e608806..08d78479 100644 --- a/src/eventhandler.js +++ b/src/eventhandler.js @@ -32,123 +32,125 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(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 = { +(function ($) { /** - * Add an event handler for a given event. - * @function - * @param {String} eventName - Name of event to register. - * @param {Function} handler - Function to call when event is triggered. - */ - addHandler: function( eventName, handler ) { - var events = this.events[ eventName ]; - if( !events ){ - this.events[ eventName ] = events = []; - } - if( handler && $.isFunction( handler ) ){ - events[ events.length ] = handler; - } - }, + * For use by classes which want to support custom, non-browser events. + * TODO: This is an awful 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 = {}; + }; - /** - * 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 ], + $.EventHandler.prototype = { + + /** + * Add an event handler for a given event. + * @function + * @param {String} eventName - Name of event to register. + * @param {Function} handler - Function to call when event is triggered. + * @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 = [], i; - if ( !events ){ - return; - } - if( $.isArray( events ) ){ - for( i = 0; i < events.length; i++ ){ - if( events[ i ] !== handler ){ - handlers.push( events[ i ] ); + if (!events) { + return; + } + if ($.isArray(events)) { + for (i = 0; i < events.length; i++) { + if (events[i].handler !== handler) { + 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; - } - }, + }, - - /** - * 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] = []; + /** + * Retrieve the list of all handlers registered for a given event. + * @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; } - } - }, - - /** - * Retrive the list of all handlers registered for a given event. - * @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, + events = events.length === 1 ? + [events[0]] : + Array.apply(null, events); + return function (source, args) { + var i, length = events.length; - for ( i = 0; i < length; i++ ) { - if( events[ i ] ){ - events[ i ]( source, args ); + for (i = 0; i < length; i++) { + if (events[i]) { + 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)); diff --git a/src/legacytilesource.js b/src/legacytilesource.js index 7c208e44..dbe150de 100644 --- a/src/legacytilesource.js +++ b/src/legacytilesource.js @@ -32,267 +32,278 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(function( $ ){ +(function ($) { -/** - * The LegacyTileSource allows simple, traditional image pyramids to be loaded - * into an OpenSeadragon Viewer. Basically, this translates to the historically - * 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 - * resolution image and a high resolution image in standard web formats like - * png or jpg. - * @class - * @extends OpenSeadragon.TileSource - * @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 - * properties but these properties are sufficient for this implementation. - * Additionally, the levels are required to be listed in order from - * smallest to largest. - * @property {Number} aspectRatio - * @property {Number} dimensions - * @property {Number} tileSize - * @property {Number} tileOverlap - * @property {Number} minLevel - * @property {Number} maxLevel - * @property {Array} levels - */ -$.LegacyTileSource = function( levels ) { + /** + * The LegacyTileSource allows simple, traditional image pyramids to be loaded + * into an OpenSeadragon Viewer. Basically, this translates to the historically + * 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 + * resolution image and a high resolution image in standard web formats like + * png or jpg. + * @class + * @extends OpenSeadragon.TileSource + * @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 + * properties but these properties are sufficient for this implementation. + * Additionally, the levels are required to be listed in order from + * smallest to largest. + * @property {Number} aspectRatio + * @property {Number} dimensions + * @property {Number} tileSize + * @property {Number} tileOverlap + * @property {Number} minLevel + * @property {Number} maxLevel + * @property {Array} levels + */ + $.LegacyTileSource = function (levels) { - var options, + var options, width, height; - if( $.isArray( levels ) ){ - options = { - type: 'legacy-image-pyramid', - levels: levels - }; - } + if ($.isArray(levels)) { + options = { + type: 'legacy-image-pyramid', + levels: levels + }; + } - //clean up the levels to make sure we support all formats - options.levels = filterFiles( options.levels ); - width = options.levels[ options.levels.length - 1 ].width; - height = options.levels[ options.levels.length - 1 ].height; + //clean up the levels to make sure we support all formats + options.levels = filterFiles(options.levels); - $.extend( true, options, { - width: width, - height: height, - tileSize: Math.max( height, width ), - tileOverlap: 0, - minLevel: 0, - maxLevel: options.levels.length - 1 - }); + if (options.levels.length > 0) { + width = options.levels[options.levels.length - 1].width; + height = options.levels[options.levels.length - 1].height; + } + else { + width = 0; + 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, { - /** - * Determine if the data and/or url imply the image service is supported by - * this tile source. - * @function - * @name OpenSeadragon.LegacyTileSource.prototype.supports - * @param {Object|Array} data - * @param {String} optional - url - */ - supports: function( data, url ){ - return ( + this.levels = options.levels; + }; + + $.extend($.LegacyTileSource.prototype, $.TileSource.prototype, { + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.supports + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function (data, url) { + return ( data.type && "legacy-image-pyramid" == data.type ) || ( data.documentElement && "legacy-image-pyramid" == data.documentElement.getAttribute('type') ); - }, + }, - /** - * - * @function - * @name OpenSeadragon.LegacyTileSource.prototype.configure - * @param {Object|XMLDocument} configuration - the raw configuration - * @param {String} dataUrl - the url the data was retreived from if any. - * @return {Object} options - A dictionary of keyword arguments sufficient - * to configure this tile sources constructor. - */ - configure: function( configuration, dataUrl ){ + /** + * + * @function + * @name OpenSeadragon.LegacyTileSource.prototype.configure + * @param {Object|XMLDocument} configuration - the raw configuration + * @param {String} dataUrl - the url the data was retreived from if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + 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 - * @name OpenSeadragon.LegacyTileSource.prototype.getLevelScale - * @param {Number} level - */ - getLevelScale: function( level ) { - var levelScale = NaN; - if ( 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( 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 = [], + * 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, i; - for( i = 0; i < files.length; i++ ){ - file = files[ i ]; - if( file.height && + for (i = 0; i < files.length; i++) { + file = files[i]; + if (file.height && file.width && file.url && ( file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || ( file.mimetype && file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/) ) - ) ){ - //This is sufficient to serve as a level - filtered.push({ - url: file.url, - width: Number( file.width ), - height: Number( file.height ) - }); + )) { + //This is sufficient to serve as a level + filtered.push({ + url: file.url, + width: Number(file.width), + height: Number(file.height) + }); + } + else { + $.console.error('Unsupported image format: %s', file.url ? file.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")); + } -/** - * @private - * @inner - * @function - */ -function configureFromXML( tileSource, xmlDoc ){ - - if ( !xmlDoc || !xmlDoc.documentElement ) { - throw new Error( $.getString( "Errors.Xml" ) ); - } - - var root = xmlDoc.documentElement, - rootName = root.tagName, - conf = null, - levels = [], + var root = xmlDoc.documentElement, + rootName = root.tagName, + conf = null, + levels = [], level, i; - if ( rootName == "image" ) { + if (rootName == "image") { - try { - conf = { - type: root.getAttribute( "type" ), - levels: [] - }; + try { + conf = { + type: root.getAttribute("type"), + levels: [] + }; - levels = root.getElementsByTagName( "level" ); - for ( i = 0; i < levels.length; i++ ) { - level = levels[ i ]; + levels = root.getElementsByTagName("level"); + for (i = 0; i < levels.length; i++) { + level = levels[i]; - conf.levels .push({ - url: level.getAttribute( "url" ), - width: parseInt( level.getAttribute( "width" ), 10 ), - height: parseInt( level.getAttribute( "height" ), 10 ) - }); - } + conf.levels.push({ + url: level.getAttribute("url"), + width: parseInt(level.getAttribute("width"), 10), + height: parseInt(level.getAttribute("height"), 10) + }); + } - return configureFromObject( tileSource, conf ); + return configureFromObject(tileSource, conf); - } catch ( e ) { - throw (e instanceof Error) ? + } catch (e) { + throw (e instanceof Error) ? 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.' ); - } else if ( rootName == "error" ) { - throw new Error( 'Error: ' + xmlDoc ); + + throw new Error('Unknown element ' + rootName); } - throw new Error( 'Unknown element ' + rootName ); -} + /** + * @private + * @inner + * @function + */ + function configureFromObject(tileSource, configuration) { -/** - * @private - * @inner - * @function - */ -function configureFromObject( tileSource, configuration ){ + return configuration.levels; - return configuration.levels; + } -} - -}( OpenSeadragon )); +} (OpenSeadragon)); diff --git a/src/tilesource.js b/src/tilesource.js index fb22b21c..c3986dd5 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -32,247 +32,247 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(function( $ ){ +(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'). - *
- * 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 - * 2^(N+1) >= M. - * @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 callback = null, + /** + * 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'). + *
+ * 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 + * 2^(N+1) >= M. + * @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 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 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 ($.isPlainObject(width)) { + options = width; + } else { + options = { + width: args[0], + height: args[1], + tileSize: args[2], + tileOverlap: args[3], + minLevel: args[4], + maxLevel: args[5] + }; } - } - 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 ] ); + //Tile sources supply some events, namely 'ready' when they must be configured + //by asyncronously fetching their configuration data. + $.EventHandler.call(this); - } 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 - //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 ){ + //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, placeHolderArgs) { + callback(placeHolderArgs.tileSource); + }); + //only one callback per constructor 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 ); + 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]); - return new $.Point( tx, ty ); - }, + } else { - /** - * @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, + //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 ); + 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 ); - }, + 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, + /** + * 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, @@ -282,195 +282,195 @@ $.TileSource.prototype = { 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 ){ - var $TileSource = $.TileSource.determineType( _this, data, url ); - if ( !$TileSource ) { - _this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } ); - return; + if (url) { + urlParts = url.split('/'); + filename = urlParts[urlParts.length - 1]; + lastDot = filename.lastIndexOf('.'); + if (lastDot > -1) { + urlParts[urlParts.length - 1] = filename.slice(0, lastDot); + } } - options = $TileSource.prototype.configure.apply( _this, [ data, url ]); - readySource = new $TileSource( options ); - _this.ready = true; - _this.raiseEvent( 'ready', readySource ); - }; + callback = function (data) { + var $TileSource = $.TileSource.determineType(_this, data, url); + if (!$TileSource) { + _this.raiseEvent('open-failed', { message: "Unable to load TileSource", source: url }); + return; + } - 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 ); - }, function ( xhr ) { - _this.raiseEvent( 'open-failed', { - message: "HTTP " + xhr.status + " attempting to load TileSource", - source: url + options = $TileSource.prototype.configure.apply(_this, [data, url]); + readySource = new $TileSource(options); + _this.ready = true; + _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 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 - * 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 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 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." ); - }, + /** + * 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 && + /** + * @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 ); + $.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, + /** + * 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 ) ? + 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 ) ); - } + throw new Error($.getString("Errors.Status", status, statusText)); + } - if( responseText.match(/\s*<.*/) ){ - try{ - data = ( xhr.responseXML && xhr.responseXML.documentElement ) ? + 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 ]; + $.parseXml(responseText); + } catch (e) { + data = xhr.responseText; + } + } else if (responseText.match(/\s*[\{\[].*/)) { + /*jshint evil:true*/ + data = eval('(' + responseText + ')'); + } else { + data = responseText; } + 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)); diff --git a/src/viewer.js b/src/viewer.js index d9b02708..f1880afc 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1463,7 +1463,7 @@ function onCanvasDrag( tracker, position, delta, shift ) { this.viewport.applyConstraints(); } } - this.raiseEvent( 'canvas-click', { + this.raiseEvent( 'canvas-drag', { tracker: tracker, position: position, delta: delta,