diff --git a/build.properties b/build.properties index e40d3257..51bfcb7d 100644 --- a/build.properties +++ b/build.properties @@ -6,7 +6,7 @@ PROJECT: openseadragon BUILD_MAJOR: 0 BUILD_MINOR: 8 -BUILD_ID: 24 +BUILD_ID: 26 BUILD: ${PROJECT}.${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_ID} VERSION: ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_ID} diff --git a/openseadragon.js b/openseadragon.js index a20ba81e..012437b3 100644 --- a/openseadragon.js +++ b/openseadragon.js @@ -1,5 +1,5 @@ /** - * @version OpenSeadragon 0.8.24 + * @version OpenSeadragon 0.8.26 * * @fileOverview *

@@ -389,27 +389,13 @@ OpenSeadragon = window.OpenSeadragon || (function(){ } return element; }, - - /** - * @function - * @name OpenSeadragon.getOffsetParent - * @param {Element} element - * @param {Boolean} [isFixed] - * @returns {Element} - */ - getOffsetParent: function( element, isFixed ) { - if ( isFixed && element != document.body ) { - return document.body; - } else { - return element.offsetParent; - } - }, /** + * Determines the position of the upper-left corner of the element. * @function * @name OpenSeadragon.getElementPosition - * @param {Element|String} element - * @returns {Point} + * @param {Element|String} element - the elemenet we want the position for. + * @returns {Point} - the position of the upper left corner of the element. */ getElementPosition: function( element ) { var result = new $.Point(), @@ -418,7 +404,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ element = $.getElement( element ); isFixed = $.getElementStyle( element ).position == "fixed"; - offsetParent = $.getOffsetParent( element, isFixed ); + offsetParent = getOffsetParent( element, isFixed ); while ( offsetParent ) { @@ -431,13 +417,14 @@ OpenSeadragon = window.OpenSeadragon || (function(){ element = offsetParent; isFixed = $.getElementStyle( element ).position == "fixed"; - offsetParent = $.getOffsetParent( element, isFixed ); + offsetParent = getOffsetParent( element, isFixed ); } return result; }, /** + * Determines the height and width of the given element. * @function * @name OpenSeadragon.getElementSize * @param {Element|String} element @@ -453,6 +440,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Returns the CSSStyle object for the given element. * @function * @name OpenSeadragon.getElementStyle * @param {Element|String} element @@ -471,6 +459,9 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Gets the latest event, really only useful internally since its + * specific to IE behavior. TODO: Deprecate this from the api and + * use it internally. * @function * @name OpenSeadragon.getEvent * @param {Event} [event] @@ -481,6 +472,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Gets the position of the mouse on the screen for a given event. * @function * @name OpenSeadragon.getMousePosition * @param {Event} [event] @@ -513,6 +505,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Determines the pages current scroll position. * @function * @name OpenSeadragon.getPageScroll * @returns {Point} @@ -537,6 +530,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Determines the size of the browsers window. * @function * @name OpenSeadragon.getWindowSize * @returns {Point} @@ -562,18 +556,10 @@ OpenSeadragon = window.OpenSeadragon || (function(){ return result; }, - /** - * @function - * @name OpenSeadragon.imageFormatSupported - * @param {String} [extension] - * @returns {Boolean} - */ - imageFormatSupported: function( extension ) { - extension = extension ? extension : ""; - return !!FILEFORMATS[ extension.toLowerCase() ]; - }, /** + * Wraps the given element in a nest of divs so that the element can + * be easily centered. * @function * @name OpenSeadragon.makeCenteredNode * @param {Element|String} element @@ -617,6 +603,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Creates an easily positionable element of the given type that therefor + * serves as an excellent container element. * @function * @name OpenSeadragon.makeNeutralElement * @param {String} tagName @@ -636,6 +624,9 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Ensures an image is loaded correctly to support alpha transparency. + * Generally only IE has issues doing this correctly for formats like + * png. * @function * @name OpenSeadragon.makeTransparentImage * @param {String} src @@ -676,6 +667,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Sets the opacity of the specified element. * @function * @name OpenSeadragon.setElementOpacity * @param {Element|String} element @@ -723,6 +715,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Adds an event listener for the given element, eventName and handler. * @function * @name OpenSeadragon.addEvent * @param {Element|String} element @@ -751,6 +744,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Remove a given event listener for the given element, event type and + * handler. * @function * @name OpenSeadragon.removeEvent * @param {Element|String} element @@ -779,6 +774,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Cancels the default browser behavior had the event propagated all + * the way up the DOM to the window object. * @function * @name OpenSeadragon.cancelEvent * @param {Event} [event] @@ -797,6 +794,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Stops the propagation of the event up the DOM. * @function * @name OpenSeadragon.stopEvent * @param {Event} [event] @@ -812,11 +810,18 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Similar to OpenSeadragon.delegate, but it does not immediately call + * the method on the object, returning a function which can be called + * repeatedly to delegate the method. It also allows additonal arguments + * to be passed during construction which will be added during each + * invocation, and each invocation can add additional arguments as well. + * * @function * @name OpenSeadragon.createCallback * @param {Object} object * @param {Function} method - * @param [args] any additional arguments are passed as arguments to the created callback + * @param [args] any additional arguments are passed as arguments to the + * created callback * @returns {Function} */ createCallback: function( object, method ) { @@ -841,6 +846,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Retreives the value of a url parameter from the window.location string. * @function * @name OpenSeadragon.getUrlParameter * @param {String} key @@ -852,10 +858,12 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Makes an AJAX request. * @function * @name OpenSeadragon.makeAjaxRequest - * @param {String} url - * @param {Function} [callback] + * @param {String} url - the url to request + * @param {Function} [callback] - a function to call when complete + * @throws {Error} */ makeAjaxRequest: function( url, callback ) { var async = typeof( callback ) == "function", @@ -909,7 +917,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ request.open( "GET", url, async ); request.send( null ); } catch (e) { - $.Debug.log( + $.console.log( "%s while making AJAX request: %s", e.name, e.message @@ -926,39 +934,275 @@ OpenSeadragon = window.OpenSeadragon || (function(){ return async ? null : request; }, + /** - * Parses an XML string into a DOM Document. + * Loads a Deep Zoom Image description from a url or XML string and + * provides a callback hook for the resulting Document * @function - * @name OpenSeadragon.parseXml - * @param {String} string - * @returns {Document} + * @name OpenSeadragon.createFromDZI + * @param {String} xmlUrl + * @param {String} xmlString + * @param {Function} callback */ - parseXml: function( string ) { - //TODO: yet another example where we can determine the correct - // implementation once at start-up instead of everytime we use - // the function. - var xmlDoc = null, - parser; + createFromDZI: function( dzi, callback ) { + var async = typeof ( callback ) == "function", + xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, + xmlString = xmlUrl ? null : dzi, + error = null, + urlParts, + filename, + lastDot, + tilesUrl; - if ( window.ActiveXObject ) { - xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); - xmlDoc.async = false; - xmlDoc.loadXML( string ); + if( xmlUrl ){ + urlParts = xmlUrl.split( '/' ); + filename = urlParts[ urlParts.length - 1 ]; + lastDot = filename.lastIndexOf( '.' ); - } else if ( window.DOMParser ) { + if ( lastDot > -1 ) { + urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); + } - parser = new DOMParser(); - xmlDoc = parser.parseFromString( string, "text/xml" ); - - } else { - throw new Error( "Browser doesn't support XML DOM." ); + tilesUrl = urlParts.join( '/' ) + "_files/"; } - return xmlDoc; + function finish( func, obj ) { + try { + return func( obj, tilesUrl ); + } catch ( e ) { + if ( async ) { + return null; + } else { + throw e; + } + } + } + + if ( async ) { + if ( xmlString ) { + window.setTimeout( function() { + var source = finish( processDZIXml, parseXml( xmlString ) ); + // call after finish sets error + callback( source, error ); + }, 1); + } else { + $.makeAjaxRequest( xmlUrl, function( xhr ) { + var source = finish( processDZIResponse, xhr ); + // call after finish sets error + callback( source, error ); + }); + } + + return null; + } + + if ( xmlString ) { + return finish( + processDZIXml, + parseXml( xmlString ) + ); + } else { + return finish( + processDZIResponse, + $.makeAjaxRequest( xmlUrl ) + ); + } } + }); + /** + * @private + * @inner + * @function + * @param {Element} element + * @param {Boolean} [isFixed] + * @returns {Element} + */ + function getOffsetParent( element, isFixed ) { + if ( isFixed && element != document.body ) { + return document.body; + } else { + return element.offsetParent; + } + }; + + /** + * @private + * @inner + * @function + * @param {XMLHttpRequest} xhr + * @param {String} tilesUrl + */ + function processDZIResponse( xhr, tilesUrl ) { + var status, + statusText, + doc = null; + + 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 ( xhr.responseXML && xhr.responseXML.documentElement ) { + doc = xhr.responseXML; + } else if ( xhr.responseText ) { + doc = parseXml( xhr.responseText ); + } + + return processDZIXml( doc, tilesUrl ); + }; + + /** + * @private + * @inner + * @function + * @param {Document} xmlDoc + * @param {String} tilesUrl + */ + function processDZIXml( xmlDoc, tilesUrl ) { + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName; + + if ( rootName == "Image" ) { + try { + return processDZI( root, tilesUrl ); + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.Dzi") ); + } + } else if ( rootName == "Collection" ) { + throw new Error( $.getString( "Errors.Dzc" ) ); + } else if ( rootName == "Error" ) { + return processDZIError( root ); + } + + throw new Error( $.getString( "Errors.Dzi" ) ); + }; + + /** + * @private + * @inner + * @function + * @param {Element} imageNode + * @param {String} tilesUrl + */ + function processDZI( imageNode, tilesUrl ) { + var fileFormat = imageNode.getAttribute( "Format" ), + sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], + dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), + width = parseInt( sizeNode.getAttribute( "Width" ) ), + height = parseInt( sizeNode.getAttribute( "Height" ) ), + tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ), + tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ), + dispRects = [], + dispRectNode, + rectNode, + i; + + if ( !imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + } + + for ( i = 0; i < dispRectNodes.length; i++ ) { + dispRectNode = dispRectNodes[ i ]; + rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; + + dispRects.push( new $.DisplayRect( + parseInt( rectNode.getAttribute( "X" ) ), + parseInt( rectNode.getAttribute( "Y" ) ), + parseInt( rectNode.getAttribute( "Width" ) ), + parseInt( rectNode.getAttribute( "Height" ) ), + 0, // ignore MinLevel attribute, bug in Deep Zoom Composer + parseInt( dispRectNode.getAttribute( "MaxLevel" ) ) + )); + } + return new $.DziTileSource( + width, + height, + tileSize, + tileOverlap, + tilesUrl, + fileFormat, + dispRects + ); + }; + + /** + * @private + * @inner + * @function + * @param {Document} errorNode + * @throws {Error} + */ + function processDZIError( errorNode ) { + var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], + message = messageNode.firstChild.nodeValue; + + throw new Error(message); + }; + + /** + * Reports whether the image format is supported for tiling in this + * version. + * @private + * @inner + * @function + * @param {String} [extension] + * @returns {Boolean} + */ + function imageFormatSupported( extension ) { + extension = extension ? extension : ""; + return !!FILEFORMATS[ extension.toLowerCase() ]; + }; + + /** + * Parses an XML string into a DOM Document. + * @private + * @inner + * @function + * @name OpenSeadragon.parseXml + * @param {String} string + * @returns {Document} + */ + function parseXml( string ) { + //TODO: yet another example where we can determine the correct + // implementation once at start-up instead of everytime we use + // the function. + var xmlDoc = null, + parser; + + if ( window.ActiveXObject ) { + + xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); + xmlDoc.async = false; + xmlDoc.loadXML( string ); + + } else if ( window.DOMParser ) { + + parser = new DOMParser(); + xmlDoc = parser.parseFromString( string, "text/xml" ); + + } else { + throw new Error( "Browser doesn't support XML DOM." ); + } + + return xmlDoc; + }; }( OpenSeadragon )); @@ -1592,25 +1836,26 @@ $.Control.prototype = { (function( $ ){ /** - * @class * - * The main point of entry into creating a zoomable image on the page. + * The main point of entry into creating a zoomable image on the page. * - * We have provided an idiomatic javascript constructor which takes - * a single object, but still support the legacy positional arguments. + * We have provided an idiomatic javascript constructor which takes + * a single object, but still support the legacy positional arguments. * - * The options below are given in order that they appeared in the constructor - * as arguments and we translate a positional call into an idiomatic call. - * - * options:{ - * element: String id of Element to attach to, - * xmlPath: String xpath ( TODO: not sure! ), - * prefixUrl: String url used to prepend to paths, eg button images, - * controls: Array of Seadragon.Controls, - * overlays: Array of Seadragon.Overlays, - * overlayControls: An Array of ( TODO: not sure! ) - * } + * The options below are given in order that they appeared in the constructor + * as arguments and we translate a positional call into an idiomatic call. * + * @class + * @extends OpenSeadragon.EventHandler + * @param {Object} options + * @param {String} options.element Id of Element to attach to, + * @param {String} options.xmlPath Xpath ( TODO: not sure! ), + * @param {String} options.prefixUrl Url used to prepend to paths, eg button + * images, etc. + * @param {Seadragon.Controls[]} options.controls Array of Seadragon.Controls, + * @param {Seadragon.Overlays[]} options.overlays Array of Seadragon.Overlays, + * @param {Seadragon.Controls[]} options.overlayControls An Array of ( TODO: + * not sure! ) * **/ $.Viewer = function( options ) { @@ -1897,6 +2142,10 @@ $.Viewer = function( options ) { $.extend( $.Viewer.prototype, $.EventHandler.prototype, { + /** + * @function + * @name OpenSeadragon.Viewer.prototype.addControl + */ addControl: function ( elmt, anchor ) { var elmt = $.getElement( elmt ), div = null; @@ -1935,21 +2184,36 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { elmt.style.display = "inline-block"; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isOpen + */ isOpen: function () { return !!this.source; }, - openDzi: function ( xmlUrl, xmlString ) { + /** + * If the string is xml is simply parsed and opened, otherwise the string + * is treated as an URL and an xml document is requested via ajax, parsed + * and then opened in the viewer. + * @function + * @name OpenSeadragon.Viewer.prototype.openDzi + * @param {String} dzi and xml string or the url to a DZI xml document. + */ + openDzi: function ( dzi ) { var _this = this; - $.DziTileSourceHelper.createFromXml( - xmlUrl, - xmlString, + $.createFromDZI( + dzi, function( source ){ _this.open( source ); } ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.openTileSource + */ openTileSource: function ( tileSource ) { var _this = this; window.setTimeout( function () { @@ -1957,6 +2221,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { }, 1 ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.open + */ open: function( source ) { var _this = this, overlay, @@ -2039,6 +2307,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { this.raiseEvent( "open" ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.close + */ close: function () { this.source = null; this.viewport = null; @@ -2047,6 +2319,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { this.canvas.innerHTML = ""; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.removeControl + */ removeControl: function ( elmt ) { var elmt = $.getElement( elmt ), @@ -2058,12 +2334,20 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.clearControls + */ clearControls: function () { while ( this.controls.length > 0 ) { this.controls.pop().destroy(); } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isDashboardEnabled + */ isDashboardEnabled: function () { var i; @@ -2076,18 +2360,34 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { return false; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isFullPage + */ isFullPage: function () { return this.container.parentNode == document.body; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled + */ isMouseNavEnabled: function () { return this.innerTracker.isTracking(); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isVisible + */ isVisible: function () { return this.container.style.visibility != "hidden"; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setDashboardEnabled + */ setDashboardEnabled: function( enabled ) { var i; for ( i = this.controls.length - 1; i >= 0; i-- ) { @@ -2095,6 +2395,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setFullPage + */ setFullPage: function( fullPage ) { var body = document.body, @@ -2183,10 +2487,18 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled + */ setMouseNavEnabled: function( enabled ){ this.innerTracker.setTracking( enabled ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setVisible + */ setVisible: function( visible ){ this.container.style.visibility = visible ? "" : "hidden"; } @@ -2591,7 +2903,14 @@ $.extend( $, { (function( $ ){ /** + * A Point is really used as a 2-dimensional vector, equally useful for + * representing a point on a plane, or the height and width of a plane + * not requiring any other frame of reference. * @class + * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0. + * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0. + * @property {Number} [x] The vector component 'x'. + * @property {Number} [y] The vector component 'y'. */ $.Point = function( x, y ) { this.x = typeof ( x ) == "number" ? x : 0; @@ -2600,6 +2919,13 @@ $.Point = function( x, y ) { $.Point.prototype = { + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ plus: function( point ) { return new $.Point( this.x + point.x, @@ -2607,6 +2933,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ minus: function( point ) { return new $.Point( this.x - point.x, @@ -2614,6 +2947,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ times: function( factor ) { return new $.Point( this.x * factor, @@ -2621,6 +2961,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ divide: function( factor ) { return new $.Point( this.x / factor, @@ -2628,10 +2975,24 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ negate: function() { return new $.Point( -this.x, -this.y ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ distanceTo: function( point ) { return Math.sqrt( Math.pow( this.x - point.x, 2 ) + @@ -2639,10 +3000,24 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ apply: function( func ) { return new $.Point( func( this.x ), func( this.y ) ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ equals: function( point ) { return ( point instanceof $.Point @@ -2653,6 +3028,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ toString: function() { return "(" + this.x + "," + this.y + ")"; } @@ -2665,6 +3047,18 @@ $.Point.prototype = { /** * @class + * @param {Number} width + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {Number} minLevel + * @param {Number} maxLevel + * @property {Number} aspectRatio + * @property {Number} dimensions + * @property {Number} tileSize + * @property {Number} tileOverlap + * @property {Number} minLevel + * @property {Number} maxLevel */ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { this.aspectRatio = width / height; @@ -2681,10 +3075,18 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve $.TileSource.prototype = { + /** + * @function + * @param {Number} level + */ getLevelScale: function( level ) { return 1 / ( 1 << ( this.maxLevel - level ) ); }, + /** + * @function + * @param {Number} level + */ getNumTiles: function( level ) { var scale = this.getLevelScale( level ), x = Math.ceil( scale * this.dimensions.x / this.tileSize ), @@ -2693,6 +3095,10 @@ $.TileSource.prototype = { 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, @@ -2701,6 +3107,11 @@ $.TileSource.prototype = { return new $.Point(rx, ry); }, + /** + * @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 ), @@ -2709,6 +3120,12 @@ $.TileSource.prototype = { 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, @@ -2723,10 +3140,27 @@ $.TileSource.prototype = { return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); }, + /** + * 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 && @@ -2744,7 +3178,18 @@ $.TileSource.prototype = { /** * @class - */ + * @extends OpenSeadragon.TileSource + * @param {Number} width + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + * @param {String} fileFormat + * @param {OpenSeadragon.DisplayRect[]} displayRects + * @property {String} tilesUrl + * @property {String} fileFormat + * @property {OpenSeadragon.DisplayRect[]} displayRects + */ $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects ) { var i, rect, @@ -2772,11 +3217,25 @@ $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, file }; $.extend( $.DziTileSource.prototype, $.TileSource.prototype, { - + + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ getTileUrl: function( level, x, y ) { return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' ); }, + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.tileExists + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ tileExists: function( level, x, y ) { var rects = this._levelRects[ level ], rect, @@ -2818,185 +3277,6 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, { } }); -/** - * @static - */ -$.DziTileSourceHelper = { - - createFromXml: function( xmlUrl, xmlString, callback ) { - var async = typeof (callback) == "function", - error = null, - urlParts, - filename, - lastDot, - tilesUrl, - handler; - - if ( !xmlUrl ) { - this.error = $.getString( "Errors.Empty" ); - if ( async ) { - window.setTimeout( function() { - callback( null, error ); - }, 1 ); - return null; - } - throw new Error( error ); - } - - urlParts = xmlUrl.split( '/' ); - filename = urlParts[ urlParts.length - 1 ]; - lastDot = filename.lastIndexOf( '.' ); - - if ( lastDot > -1 ) { - urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); - } - - tilesUrl = urlParts.join( '/' ) + "_files/"; - - function finish( func, obj ) { - try { - return func( obj, tilesUrl ); - } catch ( e ) { - if ( async ) { - return null; - } else { - throw e; - } - } - } - - if ( async ) { - if ( xmlString ) { - handler = $.delegate( this, this.processXml ); - window.setTimeout( function() { - var source = finish( handler, $.parseXml( xmlString ) ); - // call after finish sets error - callback( source, error ); - }, 1); - } else { - handler = $.delegate( this, this.processResponse ); - $.makeAjaxRequest( xmlUrl, function( xhr ) { - var source = finish( handler, xhr ); - // call after finish sets error - callback( source, error ); - }); - } - - return null; - } - - if ( xmlString ) { - return finish( - $.delegate( this, this.processXml ), - $.parseXml( xmlString ) - ); - } else { - return finish( - $.delegate( this, this.processResponse ), - $.makeAjaxRequest( xmlUrl ) - ); - } - }, - processResponse: function( xhr, tilesUrl ) { - var status, - statusText, - doc = null; - - 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 ( xhr.responseXML && xhr.responseXML.documentElement ) { - doc = xhr.responseXML; - } else if ( xhr.responseText ) { - doc = $.parseXml( xhr.responseText ); - } - - return this.processXml( doc, tilesUrl ); - }, - - processXml: function( xmlDoc, tilesUrl ) { - - if ( !xmlDoc || !xmlDoc.documentElement ) { - throw new Error( $.getString( "Errors.Xml" ) ); - } - - var root = xmlDoc.documentElement, - rootName = root.tagName; - - if ( rootName == "Image" ) { - try { - return this.processDzi( root, tilesUrl ); - } catch ( e ) { - throw (e instanceof Error) ? - e : - new Error( $.getString("Errors.Dzi") ); - } - } else if ( rootName == "Collection" ) { - throw new Error( $.getString( "Errors.Dzc" ) ); - } else if ( rootName == "Error" ) { - return this.processError( root ); - } - - throw new Error( $.getString( "Errors.Dzi" ) ); - }, - - processDzi: function( imageNode, tilesUrl ) { - var fileFormat = imageNode.getAttribute( "Format" ), - sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], - dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), - width = parseInt( sizeNode.getAttribute( "Width" ) ), - height = parseInt( sizeNode.getAttribute( "Height" ) ), - tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ), - tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ), - dispRects = [], - dispRectNode, - rectNode, - i; - - if ( !$.imageFormatSupported( fileFormat ) ) { - throw new Error( - $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) - ); - } - - for ( i = 0; i < dispRectNodes.length; i++ ) { - dispRectNode = dispRectNodes[ i ]; - rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; - - dispRects.push( new $.DisplayRect( - parseInt( rectNode.getAttribute( "X" ) ), - parseInt( rectNode.getAttribute( "Y" ) ), - parseInt( rectNode.getAttribute( "Width" ) ), - parseInt( rectNode.getAttribute( "Height" ) ), - 0, // ignore MinLevel attribute, bug in Deep Zoom Composer - parseInt( dispRectNode.getAttribute( "MaxLevel" ) ) - )); - } - return new $.DziTileSource( - width, - height, - tileSize, - tileOverlap, - tilesUrl, - fileFormat, - dispRects - ); - }, - - processError: function( errorNode ) { - var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], - message = messageNode.firstChild.nodeValue; - - throw new Error(message); - } -}; }( OpenSeadragon )); @@ -3020,12 +3300,34 @@ $.ButtonState = { * as fading the bottons out when the user has not interacted with them * for a specified period. * @class + * @extends OpenSeadragon.EventHandler * @param {Object} options - * @param {String} options.tooltip + * @param {String} options.tooltip Provides context help for the button we the + * user hovers over it. * @param {String} options.srcRest URL of image to use in 'rest' state * @param {String} options.srcGroup URL of image to use in 'up' state * @param {String} options.srcHover URL of image to use in 'hover' state * @param {String} options.srcDown URL of image to use in 'domn' state + * @param {Element} [options.element] Element to use as a container for the + * button. + * @property {String} tooltip Provides context help for the button we the + * user hovers over it. + * @property {String} srcRest URL of image to use in 'rest' state + * @property {String} srcGroup URL of image to use in 'up' state + * @property {String} srcHover URL of image to use in 'hover' state + * @property {String} srcDown URL of image to use in 'domn' state + * @property {Object} config Configurable settings for this button. + * @property {Element} [element] Element to use as a container for the + * button. + * @property {Number} fadeDelay How long to wait before fading + * @property {Number} fadeLength How long should it take to fade the button. + * @property {Number} fadeBeginTime When the button last began to fade. + * @property {Boolean} shouldFade Whether this button should fade after user + * stops interacting with the viewport. + this.fadeDelay = 0; // begin fading immediately + this.fadeLength = 2000; // fade over a period of 2 seconds + this.fadeBeginTime = null; + this.shouldFade = false; */ $.Button = function( options ) { @@ -3038,6 +3340,7 @@ $.Button = function( options ) { this.srcGroup = options.srcGroup; this.srcHover = options.srcHover; this.srcDown = options.srcDown; + //TODO: make button elements accessible by making them a-tags // maybe even consider basing them on the element and adding // methods jquery-style. @@ -3161,10 +3464,22 @@ $.Button = function( options ) { $.extend( $.Button.prototype, $.EventHandler.prototype, { + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupEnter + */ notifyGroupEnter: function() { inTo( this, $.ButtonState.GROUP ); }, + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupExit + */ notifyGroupExit: function() { outTo( this, $.ButtonState.REST ); } @@ -3258,18 +3573,25 @@ function outTo( button, newState ) { (function( $ ){ /** - * @class - * * Manages events on groups of buttons. - * - * options: { - * buttons: Array of buttons * required, - * group: Element to use as the container, - * config: Object with Viewer settings ( TODO: is this actually used anywhere? ) - * enter: Function callback for when the mouse enters group - * exit: Function callback for when mouse leaves the group - * release: Function callback for when mouse is released - * } + * @class + * @param {Object} options - a dictionary of settings applied against the entire + * group of buttons + * @param {Array} options.buttons Array of buttons + * @param {Element} [options.group] Element to use as the container, + * @param {Object} options.config Object with Viewer settings ( TODO: is + * this actually used anywhere? ) + * @param {Function} [options.enter] Function callback for when the mouse + * enters group + * @param {Function} [options.exit] Function callback for when mouse leaves + * the group + * @param {Function} [options.release] Function callback for when mouse is + * released + * @property {Array} buttons - An array containing the buttons themselves. + * @property {Element} element - The shared container for the buttons. + * @property {Object} config - Configurable settings for the group of buttons. + * @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross + * the group of buttons. **/ $.ButtonGroup = function( options ) { @@ -3325,10 +3647,22 @@ $.ButtonGroup = function( options ) { $.ButtonGroup.prototype = { + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateEnter + */ emulateEnter: function() { this.tracker.enter(); }, + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateExit + */ emulateExit: function() { this.tracker.exit(); } @@ -3340,7 +3674,20 @@ $.ButtonGroup.prototype = { (function( $ ){ /** + * A Rectangle really represents a 2x2 matrix where each row represents a + * 2 dimensional vector component, the first is (x,y) and the second is + * (width, height). The latter component implies the equation of a simple + * plane. + * * @class + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {Number} width The vector component 'width'. + * @property {Number} height The vector component 'height'. */ $.Rect = function( x, y, width, height ) { this.x = typeof ( x ) == "number" ? x : 0; @@ -3350,14 +3697,34 @@ $.Rect = function( x, y, width, height ) { }; $.Rect.prototype = { + + /** + * The aspect ratio is simply the ratio of width to height. + * @function + * @returns {Number} The ratio of width to height. + */ getAspectRatio: function() { return this.width / this.height; }, + /** + * Provides the coordinates of the upper-left corner of the rectanglea s a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of + * the rectangle. + */ getTopLeft: function() { return new $.Point( this.x, this.y ); }, + /** + * Provides the coordinates of the bottom-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of + * the rectangle. + */ getBottomRight: function() { return new $.Point( this.x + this.width, @@ -3365,6 +3732,12 @@ $.Rect.prototype = { ); }, + /** + * Computes the center of the rectangle. + * @function + * @returns {OpenSeadragon.Point} The center of the rectangle as represnted + * as represented by a 2-dimensional vector (x,y) + */ getCenter: function() { return new $.Point( this.x + this.width / 2.0, @@ -3372,19 +3745,36 @@ $.Rect.prototype = { ); }, + /** + * Returns the width and height component as a vector OpenSeadragon.Point + * @function + * @returns {OpenSeadragon.Point} The 2 dimensional vector represnting the + * the width and height of the rectangle. + */ getSize: function() { return new $.Point( this.width, this.height ); }, + /** + * Determines if two Rectanlges have equivalent components. + * @function + * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. + * @return {Boolean} 'true' if all components are equal, otherwise 'false'. + */ equals: function( other ) { - return - ( other instanceof $.Rect ) && + return ( other instanceof $.Rect ) && ( this.x === other.x ) && ( this.y === other.y ) && ( this.width === other.width ) && ( this.height === other.height ); }, + /** + * Provides a string representation of the retangle which is useful for + * debugging. + * @function + * @returns {String} A string representation of the rectangle. + */ toString: function() { return "[" + this.x + "," + @@ -3395,12 +3785,25 @@ $.Rect.prototype = { } }; + }( OpenSeadragon )); (function( $ ){ /** + * A display rectanlge is very similar to the OpenSeadragon.Rect but adds two + * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels + * for this rectangle. * @class + * @extends OpenSeadragon.Rect + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @param {Number} minLevel The lowest zoom level supported. + * @param {Number} maxLevel The highest zoom level supported. + * @property {Number} minLevel The lowest zoom level supported. + * @property {Number} maxLevel The highest zoom level supported. */ $.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) { $.Rect.apply( this, [ x, y, width, height ] ); @@ -3505,38 +3908,79 @@ function transform( stiffness, x ) { /** * @class + * @param {Number} level The zoom level this tile belongs to. + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates + * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? + * @param {String} url The URL of this tile's image. + * + * @property {Number} level The zoom level this tile belongs to. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates + * @property {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? + * @property {String} url The URL of this tile's image. + * @property {Boolean} loaded Is this tile loaded? + * @property {Boolean} loading Is this tile loading + * @property {Element} elmt The HTML element for this tile + * @property {Image} image The Image object for this tile + * @property {String} style The alias of this.elmt.style. + * @property {String} position This tile's position on screen, in pixels. + * @property {String} size This tile's size on screen, in pixels + * @property {String} blendStart The start time of this tile's blending + * @property {String} opacity The current opacity this tile should be. + * @property {String} distance The distance of this tile to the viewport center + * @property {String} visibility The visibility score of this tile. + * @property {Boolean} beingDrawn Whether this tile is currently being drawn + * @property {Number} lastTouchTime Timestamp the tile was last touched. */ $.Tile = function(level, x, y, bounds, exists, url) { this.level = level; this.x = x; this.y = y; - this.bounds = bounds; // where this tile fits, in normalized coordinates - this.exists = exists; // part of sparse image? tile hasn't failed to load? - this.loaded = false; // is this tile loaded? - this.loading = false; // or is this tile loading? + this.bounds = bounds; + this.exists = exists; + this.url = url; + this.loaded = false; + this.loading = false; - this.elmt = null; // the HTML element for this tile - this.image = null; // the Image object for this tile - this.url = url; // the URL of this tile's image + this.elmt = null; + this.image = null; - this.style = null; // alias of this.elmt.style - this.position = null; // this tile's position on screen, in pixels - this.size = null; // this tile's size on screen, in pixels - this.blendStart = null; // the start time of this tile's blending - this.opacity = null; // the current opacity this tile should be - this.distance = null; // the distance of this tile to the viewport center - this.visibility = null; // the visibility score of this tile + this.style = null; + this.position = null; + this.size = null; + this.blendStart = null; + this.opacity = null; + this.distance = null; + this.visibility = null; - this.beingDrawn = false; // whether this tile is currently being drawn - this.lastTouchTime = 0; // the time that tile was last touched + this.beingDrawn = false; + this.lastTouchTime = 0; }; $.Tile.prototype = { + /** + * Provides a string representation of this tiles level and (x,y) + * components. + * @function + * @returns {String} + */ toString: function() { return this.level + "/" + this.x + "_" + this.y; }, + /** + * Renders the tile in an html container. + * @function + * @param {Element} container + */ drawHTML: function( container ) { var position = this.position.apply( Math.floor ), @@ -3573,12 +4017,17 @@ $.Tile.prototype = { }, - drawCanvas: function(context) { + /** + * Renders the tile in a canvas-based context. + * @function + * @param {Canvas} context + */ + drawCanvas: function( context ) { var position = this.position, size = this.size; - if (!this.loaded) { + if ( !this.loaded ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -3587,9 +4036,13 @@ $.Tile.prototype = { } context.globalAlpha = this.opacity; - context.drawImage(this.image, position.x, position.y, size.x, size.y); + context.drawImage( this.image, position.x, position.y, size.x, size.y ); }, + /** + * Removes tile from it's contianer. + * @function + */ unload: function() { if ( this.elmt && this.elmt.parentNode ) { this.elmt.parentNode.removeChild( this.elmt ); @@ -3782,633 +4235,65 @@ var QUOTA = 100, /** * @class + * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @param {Element} element - Reference to Viewer 'canvas'. + * @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @property {Element} container -Reference to Viewer 'canvas'. + * @property {Element|Canvas} canvas + * @property {CanvasContext} context + * @property {Object} config - Reference to Viewer config. + * @property {Number} downloading - How many images are currently being loaded in parallel. + * @property {Number} normHeight - Ratio of zoomable image height to width. + * @property {Object} tilesMatrix A '3d' dictionary [level][x][y] --> Tile. + * @property {Array} tilesLoaded An unordered list of Tiles with loaded images. + * @property {Object} coverage '3d' dictionary [level][x][y] --> Boolean. + * @property {Array} overlays An unordered list of Overlays added. + * @property {Array} lastDrawn An unordered list of Tiles drawn last frame. + * @property {Number} lastResetTime + * @property {Boolean} midUpdate Is the drawer currently updating the viewport. + * @property {Boolean} updateAgain Does the drawer need to update the viewort again. + * @property {Element} elmt DEPRECATED Alias for container. */ -$.Drawer = function(source, viewport, elmt) { +$.Drawer = function( source, viewport, element ) { - this.container = $.getElement( elmt ); - this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); - this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; this.viewport = viewport; this.source = source; + this.container = $.getElement( element ); + this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); + this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; this.config = this.viewport.config; + this.normHeight = source.dimensions.y / source.dimensions.x; this.downloading = 0; - this.imageLoaderLimit = this.config.imageLoaderLimit; - - //this.profiler = new $.Profiler(); - - this.minLevel = source.minLevel; - this.maxLevel = source.maxLevel; - this.tileSize = source.tileSize; - this.tileOverlap = source.tileOverlap; - this.normHeight = source.dimensions.y / source.dimensions.x; - - // 1d dictionary [level] --> Point - this.cacheNumTiles = {}; - // 1d dictionary [level] --> Point - this.cachePixelRatios = {}; - // 3d dictionary [level][x][y] --> Tile this.tilesMatrix = {}; - // unordered list of Tiles with loaded images this.tilesLoaded = []; - // 3d dictionary [level][x][y] --> Boolean this.coverage = {}; - - // unordered list of Overlays added this.overlays = []; - // unordered list of Tiles drawn last frame this.lastDrawn = []; this.lastResetTime = 0; this.midUpdate = false; this.updateAgain = true; - - this.elmt = this.container; + this.elmt = this.container; + + this.canvas.style.width = "100%"; + this.canvas.style.height = "100%"; + this.canvas.style.position = "absolute"; - this.canvas.style.width = "100%"; - this.canvas.style.height = "100%"; - this.canvas.style.position = "absolute"; - // explicit left-align this.container.style.textAlign = "left"; - this.container.appendChild(this.canvas); + this.container.appendChild( this.canvas ); + + //this.profiler = new $.Profiler(); }; $.Drawer.prototype = { - getPixelRatio: function( level ) { - if ( !this.cachePixelRatios[ level ] ) { - this.cachePixelRatios[ level ] = this.source.getPixelRatio( level ); - } - - return this.cachePixelRatios[ level ]; - }, - - - _getTile: function( level, x, y, time, numTilesX, numTilesY ) { - var xMod, - yMod, - bounds, - exists, - url, - tile; - - if ( !this.tilesMatrix[ level ] ) { - this.tilesMatrix[ level ] = {}; - } - if ( !this.tilesMatrix[ level ][ x ] ) { - this.tilesMatrix[ level ][ x ] = {}; - } - - if ( !this.tilesMatrix[ level ][ x ][ y ] ) { - xMod = ( numTilesX + ( x % numTilesX ) ) % numTilesX; - yMod = ( numTilesY + ( y % numTilesY ) ) % numTilesY; - bounds = this.source.getTileBounds( level, xMod, yMod ); - exists = this.source.tileExists( level, xMod, yMod ); - url = this.source.getTileUrl( level, xMod, yMod ); - - bounds.x += 1.0 * ( x - xMod ) / numTilesX; - bounds.y += this.normHeight * ( y - yMod ) / numTilesY; - - this.tilesMatrix[ level ][ x ][ y ] = new $.Tile( - level, - x, - y, - bounds, - exists, - url - ); - } - - tile = this.tilesMatrix[ level ][ x ][ y ]; - tile.lastTouchTime = time; - - return tile; - }, - - _loadTile: function( tile, time ) { - tile.loading = this.loadImage( - tile.url, - $.createCallback( - null, - $.delegate( this, this._onTileLoad ), - tile, - time - ) - ); - }, - - _onTileLoad: function( tile, time, image ) { - var insertionIndex, - cutoff, - worstTile, - worstTime, - worstLevel, - worstTileIndex, - prevTile, - prevTime, - prevLevel, - i; - - tile.loading = false; - - if ( this.midUpdate ) { - $.Debug.warn( "Tile load callback in middle of drawing routine." ); - return; - } else if ( !image ) { - $.Debug.log( "Tile %s failed to load: %s", tile, tile.url ); - tile.exists = false; - return; - } else if ( time < this.lastResetTime ) { - $.Debug.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); - return; - } - - tile.loaded = true; - tile.image = image; - - insertionIndex = this.tilesLoaded.length; - - if ( this.tilesLoaded.length >= QUOTA ) { - cutoff = Math.ceil( Math.log( this.tileSize ) / Math.log( 2 ) ); - - worstTile = null; - worstTileIndex = -1; - - for ( i = this.tilesLoaded.length - 1; i >= 0; i-- ) { - prevTile = this.tilesLoaded[ i ]; - - if ( prevTile.level <= this.cutoff || prevTile.beingDrawn ) { - continue; - } else if ( !worstTile ) { - worstTile = prevTile; - worstTileIndex = i; - continue; - } - - prevTime = prevTile.lastTouchTime; - worstTime = worstTile.lastTouchTime; - prevLevel = prevTile.level; - worstLevel = worstTile.level; - - if ( prevTime < worstTime || - ( prevTime == worstTime && prevLevel > worstLevel ) ) { - worstTile = prevTile; - worstTileIndex = i; - } - } - - if ( worstTile && worstTileIndex >= 0 ) { - worstTile.unload(); - insertionIndex = worstTileIndex; - } - } - - this.tilesLoaded[ insertionIndex ] = tile; - this.updateAgain = true; - }, - - _clearTiles: function() { - this.tilesMatrix = {}; - this.tilesLoaded = []; - }, - - - - /** - * Returns true if the given tile provides coverage to lower-level tiles of - * lower resolution representing the same content. If neither x nor y is - * given, returns true if the entire visible level provides coverage. - * - * Note that out-of-bounds tiles provide coverage in this sense, since - * there's no content that they would need to cover. Tiles at non-existent - * levels that are within the image bounds, however, do not. - */ - _providesCoverage: function( level, x, y ) { - var rows, - cols, - i, j; - - if ( !this.coverage[ level ] ) { - return false; - } - - if ( x === undefined || y === undefined ) { - rows = this.coverage[ level ]; - for ( i in rows ) { - if ( rows.hasOwnProperty( i ) ) { - cols = rows[ i ]; - for ( j in cols ) { - if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { - return false; - } - } - } - } - - return true; - } - - return ( - this.coverage[ level ][ x] === undefined || - this.coverage[ level ][ x ][ y ] === undefined || - this.coverage[ level ][ x ][ y ] === true - ); - }, - - /** - * Returns true if the given tile is completely covered by higher-level - * tiles of higher resolution representing the same content. If neither x - * nor y is given, returns true if the entire visible level is covered. - */ - _isCovered: function( level, x, y ) { - if ( x === undefined || y === undefined ) { - return this._providesCoverage( level + 1 ); - } else { - return ( - this._providesCoverage( level + 1, 2 * x, 2 * y ) && - this._providesCoverage( level + 1, 2 * x, 2 * y + 1 ) && - this._providesCoverage( level + 1, 2 * x + 1, 2 * y ) && - this._providesCoverage( level + 1, 2 * x + 1, 2 * y + 1 ) - ); - } - }, - - /** - * Sets whether the given tile provides coverage or not. - */ - _setCoverage: function( level, x, y, covers ) { - if ( !this.coverage[ level ] ) { - $.Debug.warn( - "Setting coverage for a tile before its level's coverage has been reset: %s", - level - ); - return; - } - - if ( !this.coverage[ level ][ x ] ) { - this.coverage[ level ][ x ] = {}; - } - - this.coverage[ level ][ x ][ y ] = covers; - }, - - /** - * Resets coverage information for the given level. This should be called - * after every draw routine. Note that at the beginning of the next draw - * routine, coverage for every visible tile should be explicitly set. - */ - _resetCoverage: function( level ) { - this.coverage[ level ] = {}; - }, - - - _compareTiles: function( prevBest, tile ) { - if ( !prevBest ) { - return tile; - } - - if ( tile.visibility > prevBest.visibility ) { - return tile; - } else if ( tile.visibility == prevBest.visibility ) { - if ( tile.distance < prevBest.distance ) { - return tile; - } - } - - return prevBest; - }, - - - _getOverlayIndex: function( elmt ) { - var i; - for ( i = this.overlays.length - 1; i >= 0; i-- ) { - if ( this.overlays[ i ].elmt == elmt ) { - return i; - } - } - - return -1; - }, - - - _updateActual: function() { - this.updateAgain = false; - - var tile, - level, - viewportSize = this.viewport.getContainerSize(), - viewportWidth = viewportSize.x, - viewportHeight = viewportSize.y, - viewportBounds = this.viewport.getBounds( true ), - viewportTL = viewportBounds.getTopLeft(), - viewportBR = viewportBounds.getBottomRight(), - haveDrawn = false, - best = null, - currentTime = new Date().getTime(), - zeroRatioC = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( 0 ), - true - ).x, - lowestLevel = Math.max( - this.minLevel, - Math.floor( - Math.log( this.config.minZoomImageRatio ) / - Math.log( 2 ) - ) - ), - highestLevel = Math.min( - this.maxLevel, - Math.floor( - Math.log( zeroRatioC / MIN_PIXEL_RATIO ) / - Math.log( 2 ) - ) - ); - - //TODO - while ( this.lastDrawn.length > 0 ) { - tile = this.lastDrawn.pop(); - tile.beingDrawn = false; - } - - //TODO - this.canvas.innerHTML = ""; - if ( USE_CANVAS ) { - this.canvas.width = viewportWidth; - this.canvas.height = viewportHeight; - this.context.clearRect( 0, 0, viewportWidth, viewportHeight ); - } - - //TODO - if ( !this.config.wrapHorizontal && - ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { - return; - } else if - ( !this.config.wrapVertical && - ( viewportBR.y < 0 || viewportTL.y > this.normHeight ) ) { - return; - } - - //TODO - if ( !this.config.wrapHorizontal ) { - viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, 1 ); - } - if ( !this.config.wrapVertical ) { - viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, this.normHeight ); - } - - //TODO - lowestLevel = Math.min( lowestLevel, highestLevel ); - - //TODO - for ( level = highestLevel; level >= lowestLevel; level-- ) { - - //TODO - best = this._drawLevel( - level, - lowestLevel, - viewportTL, - viewportBR, - currentTime, - best - ); - - //TODO - if ( this._providesCoverage( level ) ) { - break; - } - } - - //TODO - this._drawTiles(); - this._drawOverlays(); - - //TODO - if ( best ) { - this._loadTile( best, currentTime ); - // because we haven't finished drawing, so - this.updateAgain = true; - } - }, - - _drawLevel: function( level, lowestLevel, viewportTL, viewportBR, currentTime, best ){ - var x, y, - levelOpacity, - levelVisibility, - drawTile, - tile, - tileTL, - tileBR, - numTiles, - numTilesX, - numTilesY, - renderPixelRatioC, - renderPixelRatioT, - levelOpacity, - levelVisibility, - haveDrawn = false, - drawLevel = false, - viewportCenter = this.viewport.pixelFromPoint( this.viewport.getCenter() ), - zeroRatioT = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( 0 ), - false - ).x, - optimalRatio = this.config.immediateRender ? - 1 : - zeroRatioT; - - //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( level ), - true - ).x; - - if ( ( !haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO ) || - ( level == lowestLevel ) ) { - drawLevel = true; - haveDrawn = true; - } else if ( !haveDrawn ) { - return best; - } - - //OK, a new drawing so do your calculations - tileTL = this.source.getTileAtPoint( level, viewportTL ); - tileBR = this.source.getTileAtPoint( level, viewportBR ); - numTiles = numberOfTiles( this, level ); - numTilesX = numTiles.x; - numTilesY = numTiles.y; - - renderPixelRatioT = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( level ), - false - ).x; - - levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); - levelVisibility = optimalRatio / Math.abs( - optimalRatio - renderPixelRatioT - ); - - this._resetCoverage( level ); - - if ( !this.config.wrapHorizontal ) { - tileBR.x = Math.min( tileBR.x, numTilesX - 1 ); - } - if ( !this.config.wrapVertical ) { - tileBR.y = Math.min( tileBR.y, numTilesY - 1 ); - } - - for ( x = tileTL.x; x <= tileBR.x; x++ ) { - for ( y = tileTL.y; y <= tileBR.y; y++ ) { - - tile = this._getTile( - level, - x, y, - currentTime, - numTilesX, - numTilesY - ); - - this._setCoverage( level, x, y, false ); - - if ( !tile.exists ) { - continue; - } - - drawTile = drawLevel; - if ( haveDrawn && !drawTile ) { - if ( this._isCovered( level, x, y ) ) { - this._setCoverage( level, x, y, true ); - } else { - drawTile = true; - } - } - - if ( !drawTile ) { - continue; - } - - this._positionTile( - tile, - viewportCenter, - levelVisibility - ); - - if ( tile.loaded ) { - - updateAgain = this._blendTile( - tile, - x, y, - level, - levelOpacity, - currentTime - ); - - } else if ( tile.Loading ) { - //TODO: .Loading is never defined... did they mean .loading? - // but they didnt do anything so what is this block if - // if it does nothing. - } else { - best = this._compareTiles( best, tile ); - } - } - } - return best; - }, - - _positionTile: function( tile, viewportCenter, levelVisibility ){ - var boundsTL = tile.bounds.getTopLeft(), - boundsSize = tile.bounds.getSize(), - positionC = this.viewport.pixelFromPoint( boundsTL, true ), - sizeC = this.viewport.deltaPixelsFromPoints( boundsSize, true ), - positionT = this.viewport.pixelFromPoint( boundsTL, false ), - sizeT = this.viewport.deltaPixelsFromPoints( boundsSize, false ), - tileCenter = positionT.plus( sizeT.divide( 2 ) ), - tileDistance = viewportCenter.distanceTo( tileCenter ); - - if ( !this.tileOverlap ) { - sizeC = sizeC.plus( new $.Point( 1, 1 ) ); - } - - tile.position = positionC; - tile.size = sizeC; - tile.distance = tileDistance; - tile.visibility = levelVisibility; - }, - - _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * this.config.blendTime, - deltaTime, - opacity; - - if ( !tile.blendStart ) { - tile.blendStart = currentTime; - } - - deltaTime = currentTime - tile.blendStart; - opacity = Math.min( 1, deltaTime / blendTimeMillis ); - - if ( this.config.alwaysBlend ) { - opacity *= levelOpacity; - } - - tile.opacity = opacity; - - this.lastDrawn.push( tile ); - - if ( opacity == 1 ) { - this._setCoverage( level, x, y, true ); - } else if ( deltaTime < blendTimeMillis ) { - return true; - } - - return false; - }, - - _drawTiles: function(){ - var i, - tile; - - for ( i = this.lastDrawn.length - 1; i >= 0; i-- ) { - tile = this.lastDrawn[ i ]; - - if ( USE_CANVAS ) { - tile.drawCanvas( this.context ); - } else { - tile.drawHTML( this.canvas ); - } - - tile.beingDrawn = true; - } - }, - - _drawOverlays: function(){ - var i, - length = this.overlays.length; - for ( i = 0; i < length; i++ ) { - this._drawOverlay( this.overlays[ i ] ); - } - }, - - _drawOverlay: function( overlay ){ - - var bounds = overlay.bounds; - - overlay.position = this.viewport.pixelFromPoint( - bounds.getTopLeft(), - true - ); - overlay.size = this.viewport.deltaPixelsFromPoints( - bounds.getSize(), - true - ); - overlay.drawHTML( this.container ); - }, - addOverlay: function( element, location, placement ) { element = $.getElement( element ); - if ( this._getOverlayIndex( element ) >= 0 ) { + if ( getOverlayIndex( this.overlays, element ) >= 0 ) { // they're trying to add a duplicate overlay return; } @@ -4421,7 +4306,7 @@ $.Drawer.prototype = { var i; element = $.getElement( element ); - i = this._getOverlayIndex( element ); + i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].update( location, placement ); @@ -4433,7 +4318,7 @@ $.Drawer.prototype = { var i; element = $.getElement( element ); - i = this._getOverlayIndex( element ); + i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].destroy(); @@ -4459,27 +4344,28 @@ $.Drawer.prototype = { }, reset: function() { - this._clearTiles(); - this.lastResetTime = new Date().getTime(); + clearTiles( this ); + this.lastResetTime = +new Date(); this.updateAgain = true; }, update: function() { //this.profiler.beginUpdate(); this.midUpdate = true; - this._updateActual(); + updateViewport( this ); this.midUpdate = false; //this.profiler.endUpdate(); }, - loadImage: function(src, callback) { + loadImage: function( src, callback ) { var _this = this, loading = false, image, jobid, complete; - if ( !this.imageLoaderLimit || this.downloading < this.imageLoaderLimit ) { + if ( !this.config.imageLoaderLimit || + this.downloading < this.config.imageLoaderLimit ) { this.downloading++; @@ -4491,7 +4377,7 @@ $.Drawer.prototype = { try { callback( image ); } catch ( e ) { - $.Debug.error( + $.console.error( "%s while executing %s callback: %s", e.name, src, @@ -4522,6 +4408,567 @@ $.Drawer.prototype = { } }; +/** + * @private + * @inner + * Pretty much every other line in this needs to be documented so its clear + * how each piece of this routine contributes to the drawing process. That's + * why there are so many TODO's inside this function. + */ +function updateViewport( drawer ) { + drawer.updateAgain = false; + + var tile, + level, + best = null, + haveDrawn = false, + currentTime = +new Date(), + viewportSize = drawer.viewport.getContainerSize(), + viewportBounds = drawer.viewport.getBounds( true ), + viewportTL = viewportBounds.getTopLeft(), + viewportBR = viewportBounds.getBottomRight(), + zeroRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( 0 ), + true + ).x, + lowestLevel = Math.max( + drawer.source.minLevel, + Math.floor( + Math.log( drawer.config.minZoomImageRatio ) / + Math.log( 2 ) + ) + ), + highestLevel = Math.min( + drawer.source.maxLevel, + Math.floor( + Math.log( zeroRatioC / MIN_PIXEL_RATIO ) / + Math.log( 2 ) + ) + ); + + //TODO + while ( drawer.lastDrawn.length > 0 ) { + tile = drawer.lastDrawn.pop(); + tile.beingDrawn = false; + } + + //TODO + drawer.canvas.innerHTML = ""; + if ( USE_CANVAS ) { + drawer.canvas.width = viewportSize.x; + drawer.canvas.height = viewportSize.y; + drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); + } + + //TODO + if ( !drawer.config.wrapHorizontal && + ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { + return; + } else if + ( !drawer.config.wrapVertical && + ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { + return; + } + + //TODO + if ( !drawer.config.wrapHorizontal ) { + viewportTL.x = Math.max( viewportTL.x, 0 ); + viewportBR.x = Math.min( viewportBR.x, 1 ); + } + if ( !drawer.config.wrapVertical ) { + viewportTL.y = Math.max( viewportTL.y, 0 ); + viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); + } + + //TODO + lowestLevel = Math.min( lowestLevel, highestLevel ); + + //TODO + for ( level = highestLevel; level >= lowestLevel; level-- ) { + + //TODO + best = updateLevel( + drawer, + level, + lowestLevel, + viewportTL, + viewportBR, + currentTime, + best + ); + + //TODO + if ( providesCoverage( drawer.coverage, level ) ) { + break; + } + } + + //TODO + drawTiles( drawer, drawer.lastDrawn ); + drawOverlays( drawer.viewport, drawer.overlays, drawer.container ); + + //TODO + if ( best ) { + loadTile( drawer, best, currentTime ); + // because we haven't finished drawing, so + drawer.updateAgain = true; + } +}; + + +function updateLevel( drawer, level, lowestLevel, viewportTL, viewportBR, currentTime, best ){ + var x, y, + tileTL, + tileBR, + numberOfTiles, + levelOpacity, + levelVisibility, + renderPixelRatioC, + renderPixelRatioT, + haveDrawn = false, + drawLevel = false, + viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ), + zeroRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( 0 ), + false + ).x, + optimalRatio = drawer.config.immediateRender ? + 1 : + zeroRatioT; + + //Avoid calculations for draw if we have already drawn this + renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + true + ).x; + + if ( ( !haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO ) || + ( level == lowestLevel ) ) { + drawLevel = true; + haveDrawn = true; + } else if ( !haveDrawn ) { + return best; + } + + //OK, a new drawing so do your calculations + tileTL = drawer.source.getTileAtPoint( level, viewportTL ); + tileBR = drawer.source.getTileAtPoint( level, viewportBR ); + numberOfTiles = drawer.source.getNumTiles( level ); + + renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + false + ).x; + + levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); + levelVisibility = optimalRatio / Math.abs( + optimalRatio - renderPixelRatioT + ); + + resetCoverage( drawer.coverage, level ); + + if ( !drawer.config.wrapHorizontal ) { + tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); + } + if ( !drawer.config.wrapVertical ) { + tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); + } + + for ( x = tileTL.x; x <= tileBR.x; x++ ) { + for ( y = tileTL.y; y <= tileBR.y; y++ ) { + + best = updateTile( + drawer, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + return best; +}; + +function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + drawer.source, + drawer.tilesMatrix, + currentTime, + numberOfTiles, + drawer.normHeight + ), + drawTile = drawLevel; + + setCoverage( drawer.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( drawer.coverage, level, x, y ) ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + drawer.source.tileOverlap, + drawer.viewport, + viewportCenter, + levelVisibility + ); + + if ( tile.loaded ) { + + drawer.updateAgain = blendTile( + drawer, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + } else if ( tile.Loading ) { + //TODO: .Loading is never defined... did they mean .loading? + // but they didnt do anything so what is this block if + // if it does nothing? + } else { + best = compareTiles( best, tile ); + } + + return best; +}; + +function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) { + var xMod, + yMod, + bounds, + exists, + url, + tile; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = tileSource.getTileBounds( level, xMod, yMod ); + exists = tileSource.tileExists( level, xMod, yMod ); + url = tileSource.getTileUrl( level, xMod, yMod ); + + bounds.x += 1.0 * ( x - xMod ) / numTiles.x; + bounds.y += normHeight * ( y - yMod ) / numTiles.y; + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +}; + + +function loadTile( drawer, tile, time ) { + tile.loading = drawer.loadImage( + tile.url, + function( image ){ + onTileLoad( drawer, tile, time, image ); + } + ); +}; + +function onTileLoad( drawer, tile, time, image ) { + var insertionIndex, + cutoff, + worstTile, + worstTime, + worstLevel, + worstTileIndex, + prevTile, + prevTime, + prevLevel, + i; + + tile.loading = false; + + if ( drawer.midUpdate ) { + $.console.warn( "Tile load callback in middle of drawing routine." ); + return; + } else if ( !image ) { + $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + tile.exists = false; + return; + } else if ( time < drawer.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + return; + } + + tile.loaded = true; + tile.image = image; + + insertionIndex = drawer.tilesLoaded.length; + + if ( drawer.tilesLoaded.length >= QUOTA ) { + cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) ); + + worstTile = null; + worstTileIndex = -1; + + for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) { + prevTile = drawer.tilesLoaded[ i ]; + + if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) { + continue; + } else if ( !worstTile ) { + worstTile = prevTile; + worstTileIndex = i; + continue; + } + + prevTime = prevTile.lastTouchTime; + worstTime = worstTile.lastTouchTime; + prevLevel = prevTile.level; + worstLevel = worstTile.level; + + if ( prevTime < worstTime || + ( prevTime == worstTime && prevLevel > worstLevel ) ) { + worstTile = prevTile; + worstTileIndex = i; + } + } + + if ( worstTile && worstTileIndex >= 0 ) { + worstTile.unload(); + insertionIndex = worstTileIndex; + } + } + + drawer.tilesLoaded[ insertionIndex ] = tile; + drawer.updateAgain = true; +}; + + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){ + var boundsTL = tile.bounds.getTopLeft(), + boundsSize = tile.bounds.getSize(), + positionC = viewport.pixelFromPoint( boundsTL, true ), + positionT = viewport.pixelFromPoint( boundsTL, false ), + sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), + sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileDistance = viewportCenter.distanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +}; + + +function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * drawer.config.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = Math.min( 1, deltaTime / blendTimeMillis ); + + if ( drawer.config.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + drawer.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +}; + + +function clearTiles( drawer ) { + drawer.tilesMatrix = {}; + drawer.tilesLoaded = []; +}; + +/** + * @private + * @inner + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + */ +function providesCoverage( coverage, level, x, y ) { + var rows, + cols, + i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); +}; + +/** + * @private + * @inner + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +}; + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; +}; + +/** + * @private + * @inner + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +}; + +/** + * @private + * @inner + * Determines the 'z-index' of the given overlay. Overlays are ordered in + * a z-index based on the order they are added to the Drawer. + */ +function getOverlayIndex( overlays, element ) { + var i; + for ( i = overlays.length - 1; i >= 0; i-- ) { + if ( overlays[ i ].elmt == element ) { + return i; + } + } + + return -1; +}; + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +}; + function finishLoadingImage( image, callback, successful, jobid ){ image.onload = null; @@ -4537,13 +4984,45 @@ function finishLoadingImage( image, callback, successful, jobid ){ }; -function numberOfTiles( drawer, level ){ - - if ( !drawer.cacheNumTiles[ level ] ) { - drawer.cacheNumTiles[ level ] = drawer.source.getNumTiles( level ); - } - return drawer.cacheNumTiles[ level ]; +function drawOverlays( viewport, overlays, container ){ + var i, + length = overlays.length; + for ( i = 0; i < length; i++ ) { + drawOverlay( viewport, overlays[ i ], container ); + } +}; + +function drawOverlay( viewport, overlay, container ){ + + overlay.position = viewport.pixelFromPoint( + overlay.bounds.getTopLeft(), + true + ); + overlay.size = viewport.deltaPixelsFromPoints( + overlay.bounds.getSize(), + true + ); + overlay.drawHTML( container ); +}; + +function drawTiles( drawer, lastDrawn ){ + var i, + tile; + + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { + tile = lastDrawn[ i ]; + + //TODO: get rid of this if by determining the tile draw method once up + // front and defining the appropriate 'draw' function + if ( USE_CANVAS ) { + tile.drawCanvas( drawer.context ); + } else { + tile.drawHTML( drawer.canvas ); + } + + tile.beingDrawn = true; + } }; }( OpenSeadragon )); diff --git a/src/button.js b/src/button.js index 680c3c31..fba4613d 100644 --- a/src/button.js +++ b/src/button.js @@ -18,12 +18,34 @@ $.ButtonState = { * as fading the bottons out when the user has not interacted with them * for a specified period. * @class + * @extends OpenSeadragon.EventHandler * @param {Object} options - * @param {String} options.tooltip + * @param {String} options.tooltip Provides context help for the button we the + * user hovers over it. * @param {String} options.srcRest URL of image to use in 'rest' state * @param {String} options.srcGroup URL of image to use in 'up' state * @param {String} options.srcHover URL of image to use in 'hover' state * @param {String} options.srcDown URL of image to use in 'domn' state + * @param {Element} [options.element] Element to use as a container for the + * button. + * @property {String} tooltip Provides context help for the button we the + * user hovers over it. + * @property {String} srcRest URL of image to use in 'rest' state + * @property {String} srcGroup URL of image to use in 'up' state + * @property {String} srcHover URL of image to use in 'hover' state + * @property {String} srcDown URL of image to use in 'domn' state + * @property {Object} config Configurable settings for this button. + * @property {Element} [element] Element to use as a container for the + * button. + * @property {Number} fadeDelay How long to wait before fading + * @property {Number} fadeLength How long should it take to fade the button. + * @property {Number} fadeBeginTime When the button last began to fade. + * @property {Boolean} shouldFade Whether this button should fade after user + * stops interacting with the viewport. + this.fadeDelay = 0; // begin fading immediately + this.fadeLength = 2000; // fade over a period of 2 seconds + this.fadeBeginTime = null; + this.shouldFade = false; */ $.Button = function( options ) { @@ -36,6 +58,7 @@ $.Button = function( options ) { this.srcGroup = options.srcGroup; this.srcHover = options.srcHover; this.srcDown = options.srcDown; + //TODO: make button elements accessible by making them a-tags // maybe even consider basing them on the element and adding // methods jquery-style. @@ -159,10 +182,22 @@ $.Button = function( options ) { $.extend( $.Button.prototype, $.EventHandler.prototype, { + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupEnter + */ notifyGroupEnter: function() { inTo( this, $.ButtonState.GROUP ); }, + /** + * TODO: Determine what this function is intended to do and if it's actually + * useful as an API point. + * @function + * @name OpenSeadragon.Button.prototype.notifyGroupExit + */ notifyGroupExit: function() { outTo( this, $.ButtonState.REST ); } diff --git a/src/buttongroup.js b/src/buttongroup.js index c1f1f774..401e5041 100644 --- a/src/buttongroup.js +++ b/src/buttongroup.js @@ -1,18 +1,25 @@ (function( $ ){ /** - * @class - * * Manages events on groups of buttons. - * - * options: { - * buttons: Array of buttons * required, - * group: Element to use as the container, - * config: Object with Viewer settings ( TODO: is this actually used anywhere? ) - * enter: Function callback for when the mouse enters group - * exit: Function callback for when mouse leaves the group - * release: Function callback for when mouse is released - * } + * @class + * @param {Object} options - a dictionary of settings applied against the entire + * group of buttons + * @param {Array} options.buttons Array of buttons + * @param {Element} [options.group] Element to use as the container, + * @param {Object} options.config Object with Viewer settings ( TODO: is + * this actually used anywhere? ) + * @param {Function} [options.enter] Function callback for when the mouse + * enters group + * @param {Function} [options.exit] Function callback for when mouse leaves + * the group + * @param {Function} [options.release] Function callback for when mouse is + * released + * @property {Array} buttons - An array containing the buttons themselves. + * @property {Element} element - The shared container for the buttons. + * @property {Object} config - Configurable settings for the group of buttons. + * @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross + * the group of buttons. **/ $.ButtonGroup = function( options ) { @@ -68,10 +75,22 @@ $.ButtonGroup = function( options ) { $.ButtonGroup.prototype = { + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateEnter + */ emulateEnter: function() { this.tracker.enter(); }, + /** + * TODO: Figure out why this is used on the public API and if a more useful + * api can be created. + * @function + * @name OpenSeadragon.ButtonGroup.prototype.emulateExit + */ emulateExit: function() { this.tracker.exit(); } diff --git a/src/displayrectangle.js b/src/displayrectangle.js index 5171028e..8e011377 100644 --- a/src/displayrectangle.js +++ b/src/displayrectangle.js @@ -2,7 +2,19 @@ (function( $ ){ /** + * A display rectanlge is very similar to the OpenSeadragon.Rect but adds two + * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels + * for this rectangle. * @class + * @extends OpenSeadragon.Rect + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @param {Number} minLevel The lowest zoom level supported. + * @param {Number} maxLevel The highest zoom level supported. + * @property {Number} minLevel The lowest zoom level supported. + * @property {Number} maxLevel The highest zoom level supported. */ $.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) { $.Rect.apply( this, [ x, y, width, height ] ); diff --git a/src/drawer.js b/src/drawer.js index 085b6a18..161502ac 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -23,633 +23,65 @@ var QUOTA = 100, /** * @class + * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @param {Element} element - Reference to Viewer 'canvas'. + * @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source. + * @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport. + * @property {Element} container -Reference to Viewer 'canvas'. + * @property {Element|Canvas} canvas + * @property {CanvasContext} context + * @property {Object} config - Reference to Viewer config. + * @property {Number} downloading - How many images are currently being loaded in parallel. + * @property {Number} normHeight - Ratio of zoomable image height to width. + * @property {Object} tilesMatrix A '3d' dictionary [level][x][y] --> Tile. + * @property {Array} tilesLoaded An unordered list of Tiles with loaded images. + * @property {Object} coverage '3d' dictionary [level][x][y] --> Boolean. + * @property {Array} overlays An unordered list of Overlays added. + * @property {Array} lastDrawn An unordered list of Tiles drawn last frame. + * @property {Number} lastResetTime + * @property {Boolean} midUpdate Is the drawer currently updating the viewport. + * @property {Boolean} updateAgain Does the drawer need to update the viewort again. + * @property {Element} elmt DEPRECATED Alias for container. */ -$.Drawer = function(source, viewport, elmt) { +$.Drawer = function( source, viewport, element ) { - this.container = $.getElement( elmt ); - this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); - this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; this.viewport = viewport; this.source = source; + this.container = $.getElement( element ); + this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); + this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; this.config = this.viewport.config; + this.normHeight = source.dimensions.y / source.dimensions.x; this.downloading = 0; - this.imageLoaderLimit = this.config.imageLoaderLimit; - - //this.profiler = new $.Profiler(); - - this.minLevel = source.minLevel; - this.maxLevel = source.maxLevel; - this.tileSize = source.tileSize; - this.tileOverlap = source.tileOverlap; - this.normHeight = source.dimensions.y / source.dimensions.x; - - // 1d dictionary [level] --> Point - this.cacheNumTiles = {}; - // 1d dictionary [level] --> Point - this.cachePixelRatios = {}; - // 3d dictionary [level][x][y] --> Tile this.tilesMatrix = {}; - // unordered list of Tiles with loaded images this.tilesLoaded = []; - // 3d dictionary [level][x][y] --> Boolean this.coverage = {}; - - // unordered list of Overlays added this.overlays = []; - // unordered list of Tiles drawn last frame this.lastDrawn = []; this.lastResetTime = 0; this.midUpdate = false; this.updateAgain = true; - - this.elmt = this.container; + this.elmt = this.container; + + this.canvas.style.width = "100%"; + this.canvas.style.height = "100%"; + this.canvas.style.position = "absolute"; - this.canvas.style.width = "100%"; - this.canvas.style.height = "100%"; - this.canvas.style.position = "absolute"; - // explicit left-align this.container.style.textAlign = "left"; - this.container.appendChild(this.canvas); + this.container.appendChild( this.canvas ); + + //this.profiler = new $.Profiler(); }; $.Drawer.prototype = { - getPixelRatio: function( level ) { - if ( !this.cachePixelRatios[ level ] ) { - this.cachePixelRatios[ level ] = this.source.getPixelRatio( level ); - } - - return this.cachePixelRatios[ level ]; - }, - - - _getTile: function( level, x, y, time, numTilesX, numTilesY ) { - var xMod, - yMod, - bounds, - exists, - url, - tile; - - if ( !this.tilesMatrix[ level ] ) { - this.tilesMatrix[ level ] = {}; - } - if ( !this.tilesMatrix[ level ][ x ] ) { - this.tilesMatrix[ level ][ x ] = {}; - } - - if ( !this.tilesMatrix[ level ][ x ][ y ] ) { - xMod = ( numTilesX + ( x % numTilesX ) ) % numTilesX; - yMod = ( numTilesY + ( y % numTilesY ) ) % numTilesY; - bounds = this.source.getTileBounds( level, xMod, yMod ); - exists = this.source.tileExists( level, xMod, yMod ); - url = this.source.getTileUrl( level, xMod, yMod ); - - bounds.x += 1.0 * ( x - xMod ) / numTilesX; - bounds.y += this.normHeight * ( y - yMod ) / numTilesY; - - this.tilesMatrix[ level ][ x ][ y ] = new $.Tile( - level, - x, - y, - bounds, - exists, - url - ); - } - - tile = this.tilesMatrix[ level ][ x ][ y ]; - tile.lastTouchTime = time; - - return tile; - }, - - _loadTile: function( tile, time ) { - tile.loading = this.loadImage( - tile.url, - $.createCallback( - null, - $.delegate( this, this._onTileLoad ), - tile, - time - ) - ); - }, - - _onTileLoad: function( tile, time, image ) { - var insertionIndex, - cutoff, - worstTile, - worstTime, - worstLevel, - worstTileIndex, - prevTile, - prevTime, - prevLevel, - i; - - tile.loading = false; - - if ( this.midUpdate ) { - $.Debug.warn( "Tile load callback in middle of drawing routine." ); - return; - } else if ( !image ) { - $.Debug.log( "Tile %s failed to load: %s", tile, tile.url ); - tile.exists = false; - return; - } else if ( time < this.lastResetTime ) { - $.Debug.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); - return; - } - - tile.loaded = true; - tile.image = image; - - insertionIndex = this.tilesLoaded.length; - - if ( this.tilesLoaded.length >= QUOTA ) { - cutoff = Math.ceil( Math.log( this.tileSize ) / Math.log( 2 ) ); - - worstTile = null; - worstTileIndex = -1; - - for ( i = this.tilesLoaded.length - 1; i >= 0; i-- ) { - prevTile = this.tilesLoaded[ i ]; - - if ( prevTile.level <= this.cutoff || prevTile.beingDrawn ) { - continue; - } else if ( !worstTile ) { - worstTile = prevTile; - worstTileIndex = i; - continue; - } - - prevTime = prevTile.lastTouchTime; - worstTime = worstTile.lastTouchTime; - prevLevel = prevTile.level; - worstLevel = worstTile.level; - - if ( prevTime < worstTime || - ( prevTime == worstTime && prevLevel > worstLevel ) ) { - worstTile = prevTile; - worstTileIndex = i; - } - } - - if ( worstTile && worstTileIndex >= 0 ) { - worstTile.unload(); - insertionIndex = worstTileIndex; - } - } - - this.tilesLoaded[ insertionIndex ] = tile; - this.updateAgain = true; - }, - - _clearTiles: function() { - this.tilesMatrix = {}; - this.tilesLoaded = []; - }, - - - - /** - * Returns true if the given tile provides coverage to lower-level tiles of - * lower resolution representing the same content. If neither x nor y is - * given, returns true if the entire visible level provides coverage. - * - * Note that out-of-bounds tiles provide coverage in this sense, since - * there's no content that they would need to cover. Tiles at non-existent - * levels that are within the image bounds, however, do not. - */ - _providesCoverage: function( level, x, y ) { - var rows, - cols, - i, j; - - if ( !this.coverage[ level ] ) { - return false; - } - - if ( x === undefined || y === undefined ) { - rows = this.coverage[ level ]; - for ( i in rows ) { - if ( rows.hasOwnProperty( i ) ) { - cols = rows[ i ]; - for ( j in cols ) { - if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { - return false; - } - } - } - } - - return true; - } - - return ( - this.coverage[ level ][ x] === undefined || - this.coverage[ level ][ x ][ y ] === undefined || - this.coverage[ level ][ x ][ y ] === true - ); - }, - - /** - * Returns true if the given tile is completely covered by higher-level - * tiles of higher resolution representing the same content. If neither x - * nor y is given, returns true if the entire visible level is covered. - */ - _isCovered: function( level, x, y ) { - if ( x === undefined || y === undefined ) { - return this._providesCoverage( level + 1 ); - } else { - return ( - this._providesCoverage( level + 1, 2 * x, 2 * y ) && - this._providesCoverage( level + 1, 2 * x, 2 * y + 1 ) && - this._providesCoverage( level + 1, 2 * x + 1, 2 * y ) && - this._providesCoverage( level + 1, 2 * x + 1, 2 * y + 1 ) - ); - } - }, - - /** - * Sets whether the given tile provides coverage or not. - */ - _setCoverage: function( level, x, y, covers ) { - if ( !this.coverage[ level ] ) { - $.Debug.warn( - "Setting coverage for a tile before its level's coverage has been reset: %s", - level - ); - return; - } - - if ( !this.coverage[ level ][ x ] ) { - this.coverage[ level ][ x ] = {}; - } - - this.coverage[ level ][ x ][ y ] = covers; - }, - - /** - * Resets coverage information for the given level. This should be called - * after every draw routine. Note that at the beginning of the next draw - * routine, coverage for every visible tile should be explicitly set. - */ - _resetCoverage: function( level ) { - this.coverage[ level ] = {}; - }, - - - _compareTiles: function( prevBest, tile ) { - if ( !prevBest ) { - return tile; - } - - if ( tile.visibility > prevBest.visibility ) { - return tile; - } else if ( tile.visibility == prevBest.visibility ) { - if ( tile.distance < prevBest.distance ) { - return tile; - } - } - - return prevBest; - }, - - - _getOverlayIndex: function( elmt ) { - var i; - for ( i = this.overlays.length - 1; i >= 0; i-- ) { - if ( this.overlays[ i ].elmt == elmt ) { - return i; - } - } - - return -1; - }, - - - _updateActual: function() { - this.updateAgain = false; - - var tile, - level, - viewportSize = this.viewport.getContainerSize(), - viewportWidth = viewportSize.x, - viewportHeight = viewportSize.y, - viewportBounds = this.viewport.getBounds( true ), - viewportTL = viewportBounds.getTopLeft(), - viewportBR = viewportBounds.getBottomRight(), - haveDrawn = false, - best = null, - currentTime = new Date().getTime(), - zeroRatioC = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( 0 ), - true - ).x, - lowestLevel = Math.max( - this.minLevel, - Math.floor( - Math.log( this.config.minZoomImageRatio ) / - Math.log( 2 ) - ) - ), - highestLevel = Math.min( - this.maxLevel, - Math.floor( - Math.log( zeroRatioC / MIN_PIXEL_RATIO ) / - Math.log( 2 ) - ) - ); - - //TODO - while ( this.lastDrawn.length > 0 ) { - tile = this.lastDrawn.pop(); - tile.beingDrawn = false; - } - - //TODO - this.canvas.innerHTML = ""; - if ( USE_CANVAS ) { - this.canvas.width = viewportWidth; - this.canvas.height = viewportHeight; - this.context.clearRect( 0, 0, viewportWidth, viewportHeight ); - } - - //TODO - if ( !this.config.wrapHorizontal && - ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { - return; - } else if - ( !this.config.wrapVertical && - ( viewportBR.y < 0 || viewportTL.y > this.normHeight ) ) { - return; - } - - //TODO - if ( !this.config.wrapHorizontal ) { - viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, 1 ); - } - if ( !this.config.wrapVertical ) { - viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, this.normHeight ); - } - - //TODO - lowestLevel = Math.min( lowestLevel, highestLevel ); - - //TODO - for ( level = highestLevel; level >= lowestLevel; level-- ) { - - //TODO - best = this._drawLevel( - level, - lowestLevel, - viewportTL, - viewportBR, - currentTime, - best - ); - - //TODO - if ( this._providesCoverage( level ) ) { - break; - } - } - - //TODO - this._drawTiles(); - this._drawOverlays(); - - //TODO - if ( best ) { - this._loadTile( best, currentTime ); - // because we haven't finished drawing, so - this.updateAgain = true; - } - }, - - _drawLevel: function( level, lowestLevel, viewportTL, viewportBR, currentTime, best ){ - var x, y, - levelOpacity, - levelVisibility, - drawTile, - tile, - tileTL, - tileBR, - numTiles, - numTilesX, - numTilesY, - renderPixelRatioC, - renderPixelRatioT, - levelOpacity, - levelVisibility, - haveDrawn = false, - drawLevel = false, - viewportCenter = this.viewport.pixelFromPoint( this.viewport.getCenter() ), - zeroRatioT = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( 0 ), - false - ).x, - optimalRatio = this.config.immediateRender ? - 1 : - zeroRatioT; - - //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( level ), - true - ).x; - - if ( ( !haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO ) || - ( level == lowestLevel ) ) { - drawLevel = true; - haveDrawn = true; - } else if ( !haveDrawn ) { - return best; - } - - //OK, a new drawing so do your calculations - tileTL = this.source.getTileAtPoint( level, viewportTL ); - tileBR = this.source.getTileAtPoint( level, viewportBR ); - numTiles = numberOfTiles( this, level ); - numTilesX = numTiles.x; - numTilesY = numTiles.y; - - renderPixelRatioT = this.viewport.deltaPixelsFromPoints( - this.source.getPixelRatio( level ), - false - ).x; - - levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); - levelVisibility = optimalRatio / Math.abs( - optimalRatio - renderPixelRatioT - ); - - this._resetCoverage( level ); - - if ( !this.config.wrapHorizontal ) { - tileBR.x = Math.min( tileBR.x, numTilesX - 1 ); - } - if ( !this.config.wrapVertical ) { - tileBR.y = Math.min( tileBR.y, numTilesY - 1 ); - } - - for ( x = tileTL.x; x <= tileBR.x; x++ ) { - for ( y = tileTL.y; y <= tileBR.y; y++ ) { - - tile = this._getTile( - level, - x, y, - currentTime, - numTilesX, - numTilesY - ); - - this._setCoverage( level, x, y, false ); - - if ( !tile.exists ) { - continue; - } - - drawTile = drawLevel; - if ( haveDrawn && !drawTile ) { - if ( this._isCovered( level, x, y ) ) { - this._setCoverage( level, x, y, true ); - } else { - drawTile = true; - } - } - - if ( !drawTile ) { - continue; - } - - this._positionTile( - tile, - viewportCenter, - levelVisibility - ); - - if ( tile.loaded ) { - - updateAgain = this._blendTile( - tile, - x, y, - level, - levelOpacity, - currentTime - ); - - } else if ( tile.Loading ) { - //TODO: .Loading is never defined... did they mean .loading? - // but they didnt do anything so what is this block if - // if it does nothing. - } else { - best = this._compareTiles( best, tile ); - } - } - } - return best; - }, - - _positionTile: function( tile, viewportCenter, levelVisibility ){ - var boundsTL = tile.bounds.getTopLeft(), - boundsSize = tile.bounds.getSize(), - positionC = this.viewport.pixelFromPoint( boundsTL, true ), - sizeC = this.viewport.deltaPixelsFromPoints( boundsSize, true ), - positionT = this.viewport.pixelFromPoint( boundsTL, false ), - sizeT = this.viewport.deltaPixelsFromPoints( boundsSize, false ), - tileCenter = positionT.plus( sizeT.divide( 2 ) ), - tileDistance = viewportCenter.distanceTo( tileCenter ); - - if ( !this.tileOverlap ) { - sizeC = sizeC.plus( new $.Point( 1, 1 ) ); - } - - tile.position = positionC; - tile.size = sizeC; - tile.distance = tileDistance; - tile.visibility = levelVisibility; - }, - - _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * this.config.blendTime, - deltaTime, - opacity; - - if ( !tile.blendStart ) { - tile.blendStart = currentTime; - } - - deltaTime = currentTime - tile.blendStart; - opacity = Math.min( 1, deltaTime / blendTimeMillis ); - - if ( this.config.alwaysBlend ) { - opacity *= levelOpacity; - } - - tile.opacity = opacity; - - this.lastDrawn.push( tile ); - - if ( opacity == 1 ) { - this._setCoverage( level, x, y, true ); - } else if ( deltaTime < blendTimeMillis ) { - return true; - } - - return false; - }, - - _drawTiles: function(){ - var i, - tile; - - for ( i = this.lastDrawn.length - 1; i >= 0; i-- ) { - tile = this.lastDrawn[ i ]; - - if ( USE_CANVAS ) { - tile.drawCanvas( this.context ); - } else { - tile.drawHTML( this.canvas ); - } - - tile.beingDrawn = true; - } - }, - - _drawOverlays: function(){ - var i, - length = this.overlays.length; - for ( i = 0; i < length; i++ ) { - this._drawOverlay( this.overlays[ i ] ); - } - }, - - _drawOverlay: function( overlay ){ - - var bounds = overlay.bounds; - - overlay.position = this.viewport.pixelFromPoint( - bounds.getTopLeft(), - true - ); - overlay.size = this.viewport.deltaPixelsFromPoints( - bounds.getSize(), - true - ); - overlay.drawHTML( this.container ); - }, - addOverlay: function( element, location, placement ) { element = $.getElement( element ); - if ( this._getOverlayIndex( element ) >= 0 ) { + if ( getOverlayIndex( this.overlays, element ) >= 0 ) { // they're trying to add a duplicate overlay return; } @@ -662,7 +94,7 @@ $.Drawer.prototype = { var i; element = $.getElement( element ); - i = this._getOverlayIndex( element ); + i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].update( location, placement ); @@ -674,7 +106,7 @@ $.Drawer.prototype = { var i; element = $.getElement( element ); - i = this._getOverlayIndex( element ); + i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].destroy(); @@ -700,27 +132,28 @@ $.Drawer.prototype = { }, reset: function() { - this._clearTiles(); - this.lastResetTime = new Date().getTime(); + clearTiles( this ); + this.lastResetTime = +new Date(); this.updateAgain = true; }, update: function() { //this.profiler.beginUpdate(); this.midUpdate = true; - this._updateActual(); + updateViewport( this ); this.midUpdate = false; //this.profiler.endUpdate(); }, - loadImage: function(src, callback) { + loadImage: function( src, callback ) { var _this = this, loading = false, image, jobid, complete; - if ( !this.imageLoaderLimit || this.downloading < this.imageLoaderLimit ) { + if ( !this.config.imageLoaderLimit || + this.downloading < this.config.imageLoaderLimit ) { this.downloading++; @@ -732,7 +165,7 @@ $.Drawer.prototype = { try { callback( image ); } catch ( e ) { - $.Debug.error( + $.console.error( "%s while executing %s callback: %s", e.name, src, @@ -763,6 +196,567 @@ $.Drawer.prototype = { } }; +/** + * @private + * @inner + * Pretty much every other line in this needs to be documented so its clear + * how each piece of this routine contributes to the drawing process. That's + * why there are so many TODO's inside this function. + */ +function updateViewport( drawer ) { + drawer.updateAgain = false; + + var tile, + level, + best = null, + haveDrawn = false, + currentTime = +new Date(), + viewportSize = drawer.viewport.getContainerSize(), + viewportBounds = drawer.viewport.getBounds( true ), + viewportTL = viewportBounds.getTopLeft(), + viewportBR = viewportBounds.getBottomRight(), + zeroRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( 0 ), + true + ).x, + lowestLevel = Math.max( + drawer.source.minLevel, + Math.floor( + Math.log( drawer.config.minZoomImageRatio ) / + Math.log( 2 ) + ) + ), + highestLevel = Math.min( + drawer.source.maxLevel, + Math.floor( + Math.log( zeroRatioC / MIN_PIXEL_RATIO ) / + Math.log( 2 ) + ) + ); + + //TODO + while ( drawer.lastDrawn.length > 0 ) { + tile = drawer.lastDrawn.pop(); + tile.beingDrawn = false; + } + + //TODO + drawer.canvas.innerHTML = ""; + if ( USE_CANVAS ) { + drawer.canvas.width = viewportSize.x; + drawer.canvas.height = viewportSize.y; + drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y ); + } + + //TODO + if ( !drawer.config.wrapHorizontal && + ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { + return; + } else if + ( !drawer.config.wrapVertical && + ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { + return; + } + + //TODO + if ( !drawer.config.wrapHorizontal ) { + viewportTL.x = Math.max( viewportTL.x, 0 ); + viewportBR.x = Math.min( viewportBR.x, 1 ); + } + if ( !drawer.config.wrapVertical ) { + viewportTL.y = Math.max( viewportTL.y, 0 ); + viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); + } + + //TODO + lowestLevel = Math.min( lowestLevel, highestLevel ); + + //TODO + for ( level = highestLevel; level >= lowestLevel; level-- ) { + + //TODO + best = updateLevel( + drawer, + level, + lowestLevel, + viewportTL, + viewportBR, + currentTime, + best + ); + + //TODO + if ( providesCoverage( drawer.coverage, level ) ) { + break; + } + } + + //TODO + drawTiles( drawer, drawer.lastDrawn ); + drawOverlays( drawer.viewport, drawer.overlays, drawer.container ); + + //TODO + if ( best ) { + loadTile( drawer, best, currentTime ); + // because we haven't finished drawing, so + drawer.updateAgain = true; + } +}; + + +function updateLevel( drawer, level, lowestLevel, viewportTL, viewportBR, currentTime, best ){ + var x, y, + tileTL, + tileBR, + numberOfTiles, + levelOpacity, + levelVisibility, + renderPixelRatioC, + renderPixelRatioT, + haveDrawn = false, + drawLevel = false, + viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ), + zeroRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( 0 ), + false + ).x, + optimalRatio = drawer.config.immediateRender ? + 1 : + zeroRatioT; + + //Avoid calculations for draw if we have already drawn this + renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + true + ).x; + + if ( ( !haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO ) || + ( level == lowestLevel ) ) { + drawLevel = true; + haveDrawn = true; + } else if ( !haveDrawn ) { + return best; + } + + //OK, a new drawing so do your calculations + tileTL = drawer.source.getTileAtPoint( level, viewportTL ); + tileBR = drawer.source.getTileAtPoint( level, viewportBR ); + numberOfTiles = drawer.source.getNumTiles( level ); + + renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( + drawer.source.getPixelRatio( level ), + false + ).x; + + levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); + levelVisibility = optimalRatio / Math.abs( + optimalRatio - renderPixelRatioT + ); + + resetCoverage( drawer.coverage, level ); + + if ( !drawer.config.wrapHorizontal ) { + tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); + } + if ( !drawer.config.wrapVertical ) { + tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); + } + + for ( x = tileTL.x; x <= tileBR.x; x++ ) { + for ( y = tileTL.y; y <= tileBR.y; y++ ) { + + best = updateTile( + drawer, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + return best; +}; + +function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + drawer.source, + drawer.tilesMatrix, + currentTime, + numberOfTiles, + drawer.normHeight + ), + drawTile = drawLevel; + + setCoverage( drawer.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( drawer.coverage, level, x, y ) ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + drawer.source.tileOverlap, + drawer.viewport, + viewportCenter, + levelVisibility + ); + + if ( tile.loaded ) { + + drawer.updateAgain = blendTile( + drawer, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + } else if ( tile.Loading ) { + //TODO: .Loading is never defined... did they mean .loading? + // but they didnt do anything so what is this block if + // if it does nothing? + } else { + best = compareTiles( best, tile ); + } + + return best; +}; + +function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) { + var xMod, + yMod, + bounds, + exists, + url, + tile; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + bounds = tileSource.getTileBounds( level, xMod, yMod ); + exists = tileSource.tileExists( level, xMod, yMod ); + url = tileSource.getTileUrl( level, xMod, yMod ); + + bounds.x += 1.0 * ( x - xMod ) / numTiles.x; + bounds.y += normHeight * ( y - yMod ) / numTiles.y; + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +}; + + +function loadTile( drawer, tile, time ) { + tile.loading = drawer.loadImage( + tile.url, + function( image ){ + onTileLoad( drawer, tile, time, image ); + } + ); +}; + +function onTileLoad( drawer, tile, time, image ) { + var insertionIndex, + cutoff, + worstTile, + worstTime, + worstLevel, + worstTileIndex, + prevTile, + prevTime, + prevLevel, + i; + + tile.loading = false; + + if ( drawer.midUpdate ) { + $.console.warn( "Tile load callback in middle of drawing routine." ); + return; + } else if ( !image ) { + $.console.log( "Tile %s failed to load: %s", tile, tile.url ); + tile.exists = false; + return; + } else if ( time < drawer.lastResetTime ) { + $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); + return; + } + + tile.loaded = true; + tile.image = image; + + insertionIndex = drawer.tilesLoaded.length; + + if ( drawer.tilesLoaded.length >= QUOTA ) { + cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) ); + + worstTile = null; + worstTileIndex = -1; + + for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) { + prevTile = drawer.tilesLoaded[ i ]; + + if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) { + continue; + } else if ( !worstTile ) { + worstTile = prevTile; + worstTileIndex = i; + continue; + } + + prevTime = prevTile.lastTouchTime; + worstTime = worstTile.lastTouchTime; + prevLevel = prevTile.level; + worstLevel = worstTile.level; + + if ( prevTime < worstTime || + ( prevTime == worstTime && prevLevel > worstLevel ) ) { + worstTile = prevTile; + worstTileIndex = i; + } + } + + if ( worstTile && worstTileIndex >= 0 ) { + worstTile.unload(); + insertionIndex = worstTileIndex; + } + } + + drawer.tilesLoaded[ insertionIndex ] = tile; + drawer.updateAgain = true; +}; + + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){ + var boundsTL = tile.bounds.getTopLeft(), + boundsSize = tile.bounds.getSize(), + positionC = viewport.pixelFromPoint( boundsTL, true ), + positionT = viewport.pixelFromPoint( boundsTL, false ), + sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), + sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), + tileCenter = positionT.plus( sizeT.divide( 2 ) ), + tileDistance = viewportCenter.distanceTo( tileCenter ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +}; + + +function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * drawer.config.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = Math.min( 1, deltaTime / blendTimeMillis ); + + if ( drawer.config.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + drawer.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( drawer.coverage, level, x, y, true ); + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +}; + + +function clearTiles( drawer ) { + drawer.tilesMatrix = {}; + drawer.tilesLoaded = []; +}; + +/** + * @private + * @inner + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + */ +function providesCoverage( coverage, level, x, y ) { + var rows, + cols, + i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); +}; + +/** + * @private + * @inner + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +}; + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; +}; + +/** + * @private + * @inner + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +}; + +/** + * @private + * @inner + * Determines the 'z-index' of the given overlay. Overlays are ordered in + * a z-index based on the order they are added to the Drawer. + */ +function getOverlayIndex( overlays, element ) { + var i; + for ( i = overlays.length - 1; i >= 0; i-- ) { + if ( overlays[ i ].elmt == element ) { + return i; + } + } + + return -1; +}; + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +}; + function finishLoadingImage( image, callback, successful, jobid ){ image.onload = null; @@ -778,13 +772,45 @@ function finishLoadingImage( image, callback, successful, jobid ){ }; -function numberOfTiles( drawer, level ){ - - if ( !drawer.cacheNumTiles[ level ] ) { - drawer.cacheNumTiles[ level ] = drawer.source.getNumTiles( level ); - } - return drawer.cacheNumTiles[ level ]; +function drawOverlays( viewport, overlays, container ){ + var i, + length = overlays.length; + for ( i = 0; i < length; i++ ) { + drawOverlay( viewport, overlays[ i ], container ); + } +}; + +function drawOverlay( viewport, overlay, container ){ + + overlay.position = viewport.pixelFromPoint( + overlay.bounds.getTopLeft(), + true + ); + overlay.size = viewport.deltaPixelsFromPoints( + overlay.bounds.getSize(), + true + ); + overlay.drawHTML( container ); +}; + +function drawTiles( drawer, lastDrawn ){ + var i, + tile; + + for ( i = lastDrawn.length - 1; i >= 0; i-- ) { + tile = lastDrawn[ i ]; + + //TODO: get rid of this if by determining the tile draw method once up + // front and defining the appropriate 'draw' function + if ( USE_CANVAS ) { + tile.drawCanvas( drawer.context ); + } else { + tile.drawHTML( drawer.canvas ); + } + + tile.beingDrawn = true; + } }; }( OpenSeadragon )); diff --git a/src/dzitilesource.js b/src/dzitilesource.js index 209211ee..30866fcd 100644 --- a/src/dzitilesource.js +++ b/src/dzitilesource.js @@ -3,7 +3,18 @@ /** * @class - */ + * @extends OpenSeadragon.TileSource + * @param {Number} width + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {String} tilesUrl + * @param {String} fileFormat + * @param {OpenSeadragon.DisplayRect[]} displayRects + * @property {String} tilesUrl + * @property {String} fileFormat + * @property {OpenSeadragon.DisplayRect[]} displayRects + */ $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects ) { var i, rect, @@ -31,11 +42,25 @@ $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, file }; $.extend( $.DziTileSource.prototype, $.TileSource.prototype, { - + + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.getTileUrl + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ getTileUrl: function( level, x, y ) { return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' ); }, + /** + * @function + * @name OpenSeadragon.DziTileSource.prototype.tileExists + * @param {Number} level + * @param {Number} x + * @param {Number} y + */ tileExists: function( level, x, y ) { var rects = this._levelRects[ level ], rect, @@ -77,185 +102,6 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, { } }); -/** - * @static - */ -$.DziTileSourceHelper = { - - createFromXml: function( xmlUrl, xmlString, callback ) { - var async = typeof (callback) == "function", - error = null, - urlParts, - filename, - lastDot, - tilesUrl, - handler; - - if ( !xmlUrl ) { - this.error = $.getString( "Errors.Empty" ); - if ( async ) { - window.setTimeout( function() { - callback( null, error ); - }, 1 ); - return null; - } - throw new Error( error ); - } - - urlParts = xmlUrl.split( '/' ); - filename = urlParts[ urlParts.length - 1 ]; - lastDot = filename.lastIndexOf( '.' ); - - if ( lastDot > -1 ) { - urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); - } - - tilesUrl = urlParts.join( '/' ) + "_files/"; - - function finish( func, obj ) { - try { - return func( obj, tilesUrl ); - } catch ( e ) { - if ( async ) { - return null; - } else { - throw e; - } - } - } - - if ( async ) { - if ( xmlString ) { - handler = $.delegate( this, this.processXml ); - window.setTimeout( function() { - var source = finish( handler, $.parseXml( xmlString ) ); - // call after finish sets error - callback( source, error ); - }, 1); - } else { - handler = $.delegate( this, this.processResponse ); - $.makeAjaxRequest( xmlUrl, function( xhr ) { - var source = finish( handler, xhr ); - // call after finish sets error - callback( source, error ); - }); - } - - return null; - } - - if ( xmlString ) { - return finish( - $.delegate( this, this.processXml ), - $.parseXml( xmlString ) - ); - } else { - return finish( - $.delegate( this, this.processResponse ), - $.makeAjaxRequest( xmlUrl ) - ); - } - }, - processResponse: function( xhr, tilesUrl ) { - var status, - statusText, - doc = null; - - 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 ( xhr.responseXML && xhr.responseXML.documentElement ) { - doc = xhr.responseXML; - } else if ( xhr.responseText ) { - doc = $.parseXml( xhr.responseText ); - } - - return this.processXml( doc, tilesUrl ); - }, - - processXml: function( xmlDoc, tilesUrl ) { - - if ( !xmlDoc || !xmlDoc.documentElement ) { - throw new Error( $.getString( "Errors.Xml" ) ); - } - - var root = xmlDoc.documentElement, - rootName = root.tagName; - - if ( rootName == "Image" ) { - try { - return this.processDzi( root, tilesUrl ); - } catch ( e ) { - throw (e instanceof Error) ? - e : - new Error( $.getString("Errors.Dzi") ); - } - } else if ( rootName == "Collection" ) { - throw new Error( $.getString( "Errors.Dzc" ) ); - } else if ( rootName == "Error" ) { - return this.processError( root ); - } - - throw new Error( $.getString( "Errors.Dzi" ) ); - }, - - processDzi: function( imageNode, tilesUrl ) { - var fileFormat = imageNode.getAttribute( "Format" ), - sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], - dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), - width = parseInt( sizeNode.getAttribute( "Width" ) ), - height = parseInt( sizeNode.getAttribute( "Height" ) ), - tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ), - tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ), - dispRects = [], - dispRectNode, - rectNode, - i; - - if ( !$.imageFormatSupported( fileFormat ) ) { - throw new Error( - $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) - ); - } - - for ( i = 0; i < dispRectNodes.length; i++ ) { - dispRectNode = dispRectNodes[ i ]; - rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; - - dispRects.push( new $.DisplayRect( - parseInt( rectNode.getAttribute( "X" ) ), - parseInt( rectNode.getAttribute( "Y" ) ), - parseInt( rectNode.getAttribute( "Width" ) ), - parseInt( rectNode.getAttribute( "Height" ) ), - 0, // ignore MinLevel attribute, bug in Deep Zoom Composer - parseInt( dispRectNode.getAttribute( "MaxLevel" ) ) - )); - } - return new $.DziTileSource( - width, - height, - tileSize, - tileOverlap, - tilesUrl, - fileFormat, - dispRects - ); - }, - - processError: function( errorNode ) { - var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], - message = messageNode.firstChild.nodeValue; - - throw new Error(message); - } -}; }( OpenSeadragon )); diff --git a/src/openseadragon.js b/src/openseadragon.js index 1c710964..b24124e2 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -389,27 +389,13 @@ OpenSeadragon = window.OpenSeadragon || (function(){ } return element; }, - - /** - * @function - * @name OpenSeadragon.getOffsetParent - * @param {Element} element - * @param {Boolean} [isFixed] - * @returns {Element} - */ - getOffsetParent: function( element, isFixed ) { - if ( isFixed && element != document.body ) { - return document.body; - } else { - return element.offsetParent; - } - }, /** + * Determines the position of the upper-left corner of the element. * @function * @name OpenSeadragon.getElementPosition - * @param {Element|String} element - * @returns {Point} + * @param {Element|String} element - the elemenet we want the position for. + * @returns {Point} - the position of the upper left corner of the element. */ getElementPosition: function( element ) { var result = new $.Point(), @@ -418,7 +404,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ element = $.getElement( element ); isFixed = $.getElementStyle( element ).position == "fixed"; - offsetParent = $.getOffsetParent( element, isFixed ); + offsetParent = getOffsetParent( element, isFixed ); while ( offsetParent ) { @@ -431,13 +417,14 @@ OpenSeadragon = window.OpenSeadragon || (function(){ element = offsetParent; isFixed = $.getElementStyle( element ).position == "fixed"; - offsetParent = $.getOffsetParent( element, isFixed ); + offsetParent = getOffsetParent( element, isFixed ); } return result; }, /** + * Determines the height and width of the given element. * @function * @name OpenSeadragon.getElementSize * @param {Element|String} element @@ -453,6 +440,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Returns the CSSStyle object for the given element. * @function * @name OpenSeadragon.getElementStyle * @param {Element|String} element @@ -471,6 +459,9 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Gets the latest event, really only useful internally since its + * specific to IE behavior. TODO: Deprecate this from the api and + * use it internally. * @function * @name OpenSeadragon.getEvent * @param {Event} [event] @@ -481,6 +472,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Gets the position of the mouse on the screen for a given event. * @function * @name OpenSeadragon.getMousePosition * @param {Event} [event] @@ -513,6 +505,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Determines the pages current scroll position. * @function * @name OpenSeadragon.getPageScroll * @returns {Point} @@ -537,6 +530,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Determines the size of the browsers window. * @function * @name OpenSeadragon.getWindowSize * @returns {Point} @@ -562,18 +556,10 @@ OpenSeadragon = window.OpenSeadragon || (function(){ return result; }, - /** - * @function - * @name OpenSeadragon.imageFormatSupported - * @param {String} [extension] - * @returns {Boolean} - */ - imageFormatSupported: function( extension ) { - extension = extension ? extension : ""; - return !!FILEFORMATS[ extension.toLowerCase() ]; - }, /** + * Wraps the given element in a nest of divs so that the element can + * be easily centered. * @function * @name OpenSeadragon.makeCenteredNode * @param {Element|String} element @@ -617,6 +603,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Creates an easily positionable element of the given type that therefor + * serves as an excellent container element. * @function * @name OpenSeadragon.makeNeutralElement * @param {String} tagName @@ -636,6 +624,9 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Ensures an image is loaded correctly to support alpha transparency. + * Generally only IE has issues doing this correctly for formats like + * png. * @function * @name OpenSeadragon.makeTransparentImage * @param {String} src @@ -676,6 +667,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Sets the opacity of the specified element. * @function * @name OpenSeadragon.setElementOpacity * @param {Element|String} element @@ -723,6 +715,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Adds an event listener for the given element, eventName and handler. * @function * @name OpenSeadragon.addEvent * @param {Element|String} element @@ -751,6 +744,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Remove a given event listener for the given element, event type and + * handler. * @function * @name OpenSeadragon.removeEvent * @param {Element|String} element @@ -779,6 +774,8 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Cancels the default browser behavior had the event propagated all + * the way up the DOM to the window object. * @function * @name OpenSeadragon.cancelEvent * @param {Event} [event] @@ -797,6 +794,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Stops the propagation of the event up the DOM. * @function * @name OpenSeadragon.stopEvent * @param {Event} [event] @@ -812,11 +810,18 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Similar to OpenSeadragon.delegate, but it does not immediately call + * the method on the object, returning a function which can be called + * repeatedly to delegate the method. It also allows additonal arguments + * to be passed during construction which will be added during each + * invocation, and each invocation can add additional arguments as well. + * * @function * @name OpenSeadragon.createCallback * @param {Object} object * @param {Function} method - * @param [args] any additional arguments are passed as arguments to the created callback + * @param [args] any additional arguments are passed as arguments to the + * created callback * @returns {Function} */ createCallback: function( object, method ) { @@ -841,6 +846,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Retreives the value of a url parameter from the window.location string. * @function * @name OpenSeadragon.getUrlParameter * @param {String} key @@ -852,10 +858,12 @@ OpenSeadragon = window.OpenSeadragon || (function(){ }, /** + * Makes an AJAX request. * @function * @name OpenSeadragon.makeAjaxRequest - * @param {String} url - * @param {Function} [callback] + * @param {String} url - the url to request + * @param {Function} [callback] - a function to call when complete + * @throws {Error} */ makeAjaxRequest: function( url, callback ) { var async = typeof( callback ) == "function", @@ -909,7 +917,7 @@ OpenSeadragon = window.OpenSeadragon || (function(){ request.open( "GET", url, async ); request.send( null ); } catch (e) { - $.Debug.log( + $.console.log( "%s while making AJAX request: %s", e.name, e.message @@ -926,38 +934,274 @@ OpenSeadragon = window.OpenSeadragon || (function(){ return async ? null : request; }, + /** - * Parses an XML string into a DOM Document. + * Loads a Deep Zoom Image description from a url or XML string and + * provides a callback hook for the resulting Document * @function - * @name OpenSeadragon.parseXml - * @param {String} string - * @returns {Document} + * @name OpenSeadragon.createFromDZI + * @param {String} xmlUrl + * @param {String} xmlString + * @param {Function} callback */ - parseXml: function( string ) { - //TODO: yet another example where we can determine the correct - // implementation once at start-up instead of everytime we use - // the function. - var xmlDoc = null, - parser; + createFromDZI: function( dzi, callback ) { + var async = typeof ( callback ) == "function", + xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, + xmlString = xmlUrl ? null : dzi, + error = null, + urlParts, + filename, + lastDot, + tilesUrl; - if ( window.ActiveXObject ) { - xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); - xmlDoc.async = false; - xmlDoc.loadXML( string ); + if( xmlUrl ){ + urlParts = xmlUrl.split( '/' ); + filename = urlParts[ urlParts.length - 1 ]; + lastDot = filename.lastIndexOf( '.' ); - } else if ( window.DOMParser ) { + if ( lastDot > -1 ) { + urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot ); + } - parser = new DOMParser(); - xmlDoc = parser.parseFromString( string, "text/xml" ); - - } else { - throw new Error( "Browser doesn't support XML DOM." ); + tilesUrl = urlParts.join( '/' ) + "_files/"; } - return xmlDoc; + function finish( func, obj ) { + try { + return func( obj, tilesUrl ); + } catch ( e ) { + if ( async ) { + return null; + } else { + throw e; + } + } + } + + if ( async ) { + if ( xmlString ) { + window.setTimeout( function() { + var source = finish( processDZIXml, parseXml( xmlString ) ); + // call after finish sets error + callback( source, error ); + }, 1); + } else { + $.makeAjaxRequest( xmlUrl, function( xhr ) { + var source = finish( processDZIResponse, xhr ); + // call after finish sets error + callback( source, error ); + }); + } + + return null; + } + + if ( xmlString ) { + return finish( + processDZIXml, + parseXml( xmlString ) + ); + } else { + return finish( + processDZIResponse, + $.makeAjaxRequest( xmlUrl ) + ); + } } + }); + /** + * @private + * @inner + * @function + * @param {Element} element + * @param {Boolean} [isFixed] + * @returns {Element} + */ + function getOffsetParent( element, isFixed ) { + if ( isFixed && element != document.body ) { + return document.body; + } else { + return element.offsetParent; + } + }; + + /** + * @private + * @inner + * @function + * @param {XMLHttpRequest} xhr + * @param {String} tilesUrl + */ + function processDZIResponse( xhr, tilesUrl ) { + var status, + statusText, + doc = null; + + 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 ( xhr.responseXML && xhr.responseXML.documentElement ) { + doc = xhr.responseXML; + } else if ( xhr.responseText ) { + doc = parseXml( xhr.responseText ); + } + + return processDZIXml( doc, tilesUrl ); + }; + + /** + * @private + * @inner + * @function + * @param {Document} xmlDoc + * @param {String} tilesUrl + */ + function processDZIXml( xmlDoc, tilesUrl ) { + + if ( !xmlDoc || !xmlDoc.documentElement ) { + throw new Error( $.getString( "Errors.Xml" ) ); + } + + var root = xmlDoc.documentElement, + rootName = root.tagName; + + if ( rootName == "Image" ) { + try { + return processDZI( root, tilesUrl ); + } catch ( e ) { + throw (e instanceof Error) ? + e : + new Error( $.getString("Errors.Dzi") ); + } + } else if ( rootName == "Collection" ) { + throw new Error( $.getString( "Errors.Dzc" ) ); + } else if ( rootName == "Error" ) { + return processDZIError( root ); + } + + throw new Error( $.getString( "Errors.Dzi" ) ); + }; + + /** + * @private + * @inner + * @function + * @param {Element} imageNode + * @param {String} tilesUrl + */ + function processDZI( imageNode, tilesUrl ) { + var fileFormat = imageNode.getAttribute( "Format" ), + sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ], + dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ), + width = parseInt( sizeNode.getAttribute( "Width" ) ), + height = parseInt( sizeNode.getAttribute( "Height" ) ), + tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ), + tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ), + dispRects = [], + dispRectNode, + rectNode, + i; + + if ( !imageFormatSupported( fileFormat ) ) { + throw new Error( + $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() ) + ); + } + + for ( i = 0; i < dispRectNodes.length; i++ ) { + dispRectNode = dispRectNodes[ i ]; + rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ]; + + dispRects.push( new $.DisplayRect( + parseInt( rectNode.getAttribute( "X" ) ), + parseInt( rectNode.getAttribute( "Y" ) ), + parseInt( rectNode.getAttribute( "Width" ) ), + parseInt( rectNode.getAttribute( "Height" ) ), + 0, // ignore MinLevel attribute, bug in Deep Zoom Composer + parseInt( dispRectNode.getAttribute( "MaxLevel" ) ) + )); + } + return new $.DziTileSource( + width, + height, + tileSize, + tileOverlap, + tilesUrl, + fileFormat, + dispRects + ); + }; + + /** + * @private + * @inner + * @function + * @param {Document} errorNode + * @throws {Error} + */ + function processDZIError( errorNode ) { + var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ], + message = messageNode.firstChild.nodeValue; + + throw new Error(message); + }; + + /** + * Reports whether the image format is supported for tiling in this + * version. + * @private + * @inner + * @function + * @param {String} [extension] + * @returns {Boolean} + */ + function imageFormatSupported( extension ) { + extension = extension ? extension : ""; + return !!FILEFORMATS[ extension.toLowerCase() ]; + }; + + /** + * Parses an XML string into a DOM Document. + * @private + * @inner + * @function + * @name OpenSeadragon.parseXml + * @param {String} string + * @returns {Document} + */ + function parseXml( string ) { + //TODO: yet another example where we can determine the correct + // implementation once at start-up instead of everytime we use + // the function. + var xmlDoc = null, + parser; + + if ( window.ActiveXObject ) { + + xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); + xmlDoc.async = false; + xmlDoc.loadXML( string ); + + } else if ( window.DOMParser ) { + + parser = new DOMParser(); + xmlDoc = parser.parseFromString( string, "text/xml" ); + + } else { + throw new Error( "Browser doesn't support XML DOM." ); + } + + return xmlDoc; + }; }( OpenSeadragon )); diff --git a/src/point.js b/src/point.js index dbea4b98..6f3e5ccd 100644 --- a/src/point.js +++ b/src/point.js @@ -2,7 +2,14 @@ (function( $ ){ /** + * A Point is really used as a 2-dimensional vector, equally useful for + * representing a point on a plane, or the height and width of a plane + * not requiring any other frame of reference. * @class + * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0. + * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0. + * @property {Number} [x] The vector component 'x'. + * @property {Number} [y] The vector component 'y'. */ $.Point = function( x, y ) { this.x = typeof ( x ) == "number" ? x : 0; @@ -11,6 +18,13 @@ $.Point = function( x, y ) { $.Point.prototype = { + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ plus: function( point ) { return new $.Point( this.x + point.x, @@ -18,6 +32,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ minus: function( point ) { return new $.Point( this.x - point.x, @@ -25,6 +46,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ times: function( factor ) { return new $.Point( this.x * factor, @@ -32,6 +60,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ divide: function( factor ) { return new $.Point( this.x / factor, @@ -39,10 +74,24 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ negate: function() { return new $.Point( -this.x, -this.y ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ distanceTo: function( point ) { return Math.sqrt( Math.pow( this.x - point.x, 2 ) + @@ -50,10 +99,24 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ apply: function( func ) { return new $.Point( func( this.x ), func( this.y ) ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ equals: function( point ) { return ( point instanceof $.Point @@ -64,6 +127,13 @@ $.Point.prototype = { ); }, + /** + * Add another Point to this point and return a new Point. + * @function + * @param {OpenSeadragon.Point} point The point to add vector components. + * @returns {OpenSeadragon.Point} A new point representing the sum of the + * vector components + */ toString: function() { return "(" + this.x + "," + this.y + ")"; } diff --git a/src/rectangle.js b/src/rectangle.js index af3c942c..e4b07866 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -2,7 +2,20 @@ (function( $ ){ /** + * A Rectangle really represents a 2x2 matrix where each row represents a + * 2 dimensional vector component, the first is (x,y) and the second is + * (width, height). The latter component implies the equation of a simple + * plane. + * * @class + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {Number} width The vector component 'height'. + * @param {Number} height The vector component 'width'. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {Number} width The vector component 'width'. + * @property {Number} height The vector component 'height'. */ $.Rect = function( x, y, width, height ) { this.x = typeof ( x ) == "number" ? x : 0; @@ -12,14 +25,34 @@ $.Rect = function( x, y, width, height ) { }; $.Rect.prototype = { + + /** + * The aspect ratio is simply the ratio of width to height. + * @function + * @returns {Number} The ratio of width to height. + */ getAspectRatio: function() { return this.width / this.height; }, + /** + * Provides the coordinates of the upper-left corner of the rectanglea s a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of + * the rectangle. + */ getTopLeft: function() { return new $.Point( this.x, this.y ); }, + /** + * Provides the coordinates of the bottom-right corner of the rectangle as a + * point. + * @function + * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of + * the rectangle. + */ getBottomRight: function() { return new $.Point( this.x + this.width, @@ -27,6 +60,12 @@ $.Rect.prototype = { ); }, + /** + * Computes the center of the rectangle. + * @function + * @returns {OpenSeadragon.Point} The center of the rectangle as represnted + * as represented by a 2-dimensional vector (x,y) + */ getCenter: function() { return new $.Point( this.x + this.width / 2.0, @@ -34,19 +73,36 @@ $.Rect.prototype = { ); }, + /** + * Returns the width and height component as a vector OpenSeadragon.Point + * @function + * @returns {OpenSeadragon.Point} The 2 dimensional vector represnting the + * the width and height of the rectangle. + */ getSize: function() { return new $.Point( this.width, this.height ); }, + /** + * Determines if two Rectanlges have equivalent components. + * @function + * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. + * @return {Boolean} 'true' if all components are equal, otherwise 'false'. + */ equals: function( other ) { - return - ( other instanceof $.Rect ) && + return ( other instanceof $.Rect ) && ( this.x === other.x ) && ( this.y === other.y ) && ( this.width === other.width ) && ( this.height === other.height ); }, + /** + * Provides a string representation of the retangle which is useful for + * debugging. + * @function + * @returns {String} A string representation of the rectangle. + */ toString: function() { return "[" + this.x + "," + @@ -57,4 +113,5 @@ $.Rect.prototype = { } }; + }( OpenSeadragon )); diff --git a/src/tile.js b/src/tile.js index f9a7cb69..c7a75442 100644 --- a/src/tile.js +++ b/src/tile.js @@ -3,38 +3,79 @@ /** * @class + * @param {Number} level The zoom level this tile belongs to. + * @param {Number} x The vector component 'x'. + * @param {Number} y The vector component 'y'. + * @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates + * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? + * @param {String} url The URL of this tile's image. + * + * @property {Number} level The zoom level this tile belongs to. + * @property {Number} x The vector component 'x'. + * @property {Number} y The vector component 'y'. + * @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized + * coordinates + * @property {Boolean} exists Is this tile a part of a sparse image? ( Also has + * this tile failed to load? + * @property {String} url The URL of this tile's image. + * @property {Boolean} loaded Is this tile loaded? + * @property {Boolean} loading Is this tile loading + * @property {Element} elmt The HTML element for this tile + * @property {Image} image The Image object for this tile + * @property {String} style The alias of this.elmt.style. + * @property {String} position This tile's position on screen, in pixels. + * @property {String} size This tile's size on screen, in pixels + * @property {String} blendStart The start time of this tile's blending + * @property {String} opacity The current opacity this tile should be. + * @property {String} distance The distance of this tile to the viewport center + * @property {String} visibility The visibility score of this tile. + * @property {Boolean} beingDrawn Whether this tile is currently being drawn + * @property {Number} lastTouchTime Timestamp the tile was last touched. */ $.Tile = function(level, x, y, bounds, exists, url) { this.level = level; this.x = x; this.y = y; - this.bounds = bounds; // where this tile fits, in normalized coordinates - this.exists = exists; // part of sparse image? tile hasn't failed to load? - this.loaded = false; // is this tile loaded? - this.loading = false; // or is this tile loading? + this.bounds = bounds; + this.exists = exists; + this.url = url; + this.loaded = false; + this.loading = false; - this.elmt = null; // the HTML element for this tile - this.image = null; // the Image object for this tile - this.url = url; // the URL of this tile's image + this.elmt = null; + this.image = null; - this.style = null; // alias of this.elmt.style - this.position = null; // this tile's position on screen, in pixels - this.size = null; // this tile's size on screen, in pixels - this.blendStart = null; // the start time of this tile's blending - this.opacity = null; // the current opacity this tile should be - this.distance = null; // the distance of this tile to the viewport center - this.visibility = null; // the visibility score of this tile + this.style = null; + this.position = null; + this.size = null; + this.blendStart = null; + this.opacity = null; + this.distance = null; + this.visibility = null; - this.beingDrawn = false; // whether this tile is currently being drawn - this.lastTouchTime = 0; // the time that tile was last touched + this.beingDrawn = false; + this.lastTouchTime = 0; }; $.Tile.prototype = { + /** + * Provides a string representation of this tiles level and (x,y) + * components. + * @function + * @returns {String} + */ toString: function() { return this.level + "/" + this.x + "_" + this.y; }, + /** + * Renders the tile in an html container. + * @function + * @param {Element} container + */ drawHTML: function( container ) { var position = this.position.apply( Math.floor ), @@ -71,12 +112,17 @@ $.Tile.prototype = { }, - drawCanvas: function(context) { + /** + * Renders the tile in a canvas-based context. + * @function + * @param {Canvas} context + */ + drawCanvas: function( context ) { var position = this.position, size = this.size; - if (!this.loaded) { + if ( !this.loaded ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -85,9 +131,13 @@ $.Tile.prototype = { } context.globalAlpha = this.opacity; - context.drawImage(this.image, position.x, position.y, size.x, size.y); + context.drawImage( this.image, position.x, position.y, size.x, size.y ); }, + /** + * Removes tile from it's contianer. + * @function + */ unload: function() { if ( this.elmt && this.elmt.parentNode ) { this.elmt.parentNode.removeChild( this.elmt ); diff --git a/src/tilesource.js b/src/tilesource.js index be667dc1..9f6e1223 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -4,6 +4,18 @@ /** * @class + * @param {Number} width + * @param {Number} height + * @param {Number} tileSize + * @param {Number} tileOverlap + * @param {Number} minLevel + * @param {Number} maxLevel + * @property {Number} aspectRatio + * @property {Number} dimensions + * @property {Number} tileSize + * @property {Number} tileOverlap + * @property {Number} minLevel + * @property {Number} maxLevel */ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) { this.aspectRatio = width / height; @@ -20,10 +32,18 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve $.TileSource.prototype = { + /** + * @function + * @param {Number} level + */ getLevelScale: function( level ) { return 1 / ( 1 << ( this.maxLevel - level ) ); }, + /** + * @function + * @param {Number} level + */ getNumTiles: function( level ) { var scale = this.getLevelScale( level ), x = Math.ceil( scale * this.dimensions.x / this.tileSize ), @@ -32,6 +52,10 @@ $.TileSource.prototype = { 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, @@ -40,6 +64,11 @@ $.TileSource.prototype = { return new $.Point(rx, ry); }, + /** + * @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 ), @@ -48,6 +77,12 @@ $.TileSource.prototype = { 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, @@ -62,10 +97,27 @@ $.TileSource.prototype = { return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); }, + /** + * 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 && diff --git a/src/viewer.js b/src/viewer.js index 42ee974d..d3e125b0 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1,25 +1,26 @@ (function( $ ){ /** - * @class * - * The main point of entry into creating a zoomable image on the page. + * The main point of entry into creating a zoomable image on the page. * - * We have provided an idiomatic javascript constructor which takes - * a single object, but still support the legacy positional arguments. + * We have provided an idiomatic javascript constructor which takes + * a single object, but still support the legacy positional arguments. * - * The options below are given in order that they appeared in the constructor - * as arguments and we translate a positional call into an idiomatic call. - * - * options:{ - * element: String id of Element to attach to, - * xmlPath: String xpath ( TODO: not sure! ), - * prefixUrl: String url used to prepend to paths, eg button images, - * controls: Array of Seadragon.Controls, - * overlays: Array of Seadragon.Overlays, - * overlayControls: An Array of ( TODO: not sure! ) - * } + * The options below are given in order that they appeared in the constructor + * as arguments and we translate a positional call into an idiomatic call. * + * @class + * @extends OpenSeadragon.EventHandler + * @param {Object} options + * @param {String} options.element Id of Element to attach to, + * @param {String} options.xmlPath Xpath ( TODO: not sure! ), + * @param {String} options.prefixUrl Url used to prepend to paths, eg button + * images, etc. + * @param {Seadragon.Controls[]} options.controls Array of Seadragon.Controls, + * @param {Seadragon.Overlays[]} options.overlays Array of Seadragon.Overlays, + * @param {Seadragon.Controls[]} options.overlayControls An Array of ( TODO: + * not sure! ) * **/ $.Viewer = function( options ) { @@ -306,6 +307,10 @@ $.Viewer = function( options ) { $.extend( $.Viewer.prototype, $.EventHandler.prototype, { + /** + * @function + * @name OpenSeadragon.Viewer.prototype.addControl + */ addControl: function ( elmt, anchor ) { var elmt = $.getElement( elmt ), div = null; @@ -344,21 +349,36 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { elmt.style.display = "inline-block"; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isOpen + */ isOpen: function () { return !!this.source; }, - openDzi: function ( xmlUrl, xmlString ) { + /** + * If the string is xml is simply parsed and opened, otherwise the string + * is treated as an URL and an xml document is requested via ajax, parsed + * and then opened in the viewer. + * @function + * @name OpenSeadragon.Viewer.prototype.openDzi + * @param {String} dzi and xml string or the url to a DZI xml document. + */ + openDzi: function ( dzi ) { var _this = this; - $.DziTileSourceHelper.createFromXml( - xmlUrl, - xmlString, + $.createFromDZI( + dzi, function( source ){ _this.open( source ); } ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.openTileSource + */ openTileSource: function ( tileSource ) { var _this = this; window.setTimeout( function () { @@ -366,6 +386,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { }, 1 ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.open + */ open: function( source ) { var _this = this, overlay, @@ -448,6 +472,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { this.raiseEvent( "open" ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.close + */ close: function () { this.source = null; this.viewport = null; @@ -456,6 +484,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { this.canvas.innerHTML = ""; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.removeControl + */ removeControl: function ( elmt ) { var elmt = $.getElement( elmt ), @@ -467,12 +499,20 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.clearControls + */ clearControls: function () { while ( this.controls.length > 0 ) { this.controls.pop().destroy(); } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isDashboardEnabled + */ isDashboardEnabled: function () { var i; @@ -485,18 +525,34 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { return false; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isFullPage + */ isFullPage: function () { return this.container.parentNode == document.body; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled + */ isMouseNavEnabled: function () { return this.innerTracker.isTracking(); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.isVisible + */ isVisible: function () { return this.container.style.visibility != "hidden"; }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setDashboardEnabled + */ setDashboardEnabled: function( enabled ) { var i; for ( i = this.controls.length - 1; i >= 0; i-- ) { @@ -504,6 +560,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setFullPage + */ setFullPage: function( fullPage ) { var body = document.body, @@ -592,10 +652,18 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, { } }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled + */ setMouseNavEnabled: function( enabled ){ this.innerTracker.setTracking( enabled ); }, + /** + * @function + * @name OpenSeadragon.Viewer.prototype.setVisible + */ setVisible: function( visible ){ this.container.style.visibility = visible ? "" : "hidden"; }