/** * @version OpenSeadragon 0.9.40 * * @fileOverview *

* * OpenSeadragon - Javascript Deep Zooming * *

*

* OpenSeadragon is provides an html interface for creating * deep zoom user interfaces. The simplest examples include deep * zoom for large resolution images, and complex examples include * zoomable map interfaces driven by SVG files. *

* * @author
(c) 2011 Christopher Thatcher * @author
(c) 2010 OpenSeadragon Team * @author
(c) 2010 CodePlex Foundation * *

* Original license preserved below:
*

 * ----------------------------------------------------------------------------
 * 
 *  License: New BSD License (BSD)
 *  Copyright (c) 2010, OpenSeadragon
 *  All rights reserved.
 * 
 *  Redistribution and use in source and binary forms, with or without 
 *  modification, are permitted provided that the following conditions are met:
 *  
 *  * Redistributions of source code must retain the above copyright notice, this 
 *    list of conditions and the following disclaimer.
 *  
 *  * Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 * 
 *  * Neither the name of OpenSeadragon nor the names of its contributors may be 
 *    used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 *  POSSIBILITY OF SUCH DAMAGE.
 * 
 * ----------------------------------------------------------------------------
 * 
*

**/ /** * The root namespace for OpenSeadragon, this function also serves as a single * point of instantiation for an {@link OpenSeadragon.Viewer}, including all * combinations of out-of-the-box configurable features. All utility methods * and classes are defined on or below this namespace. * * @namespace * @function * @name OpenSeadragon * @exports $ as OpenSeadragon * * @param {Object} options All required and optional settings for instantiating * a new instance of an OpenSeadragon image viewer. * * @param {String} options.xmlPath * DEPRECATED. A relative path to load a DZI file from the server. * Prefer the newer options.tileSources. * * @param {Array|String|Function|Object[]|Array[]|String[]|Function[]} options.tileSources * As an Array, the tileSource can hold either be all Objects or mixed * types of Arrays of Objects, String, Function. When a value is a String, * the tileSource is used to create a {@link OpenSeadragon.DziTileSource}. * When a value is a Function, the function is used to create a new * {@link OpenSeadragon.TileSource} whose abstract method * getUrl( level, x, y ) is implemented by the function. Finally, when it * is an Array of objects, it is used to create a * {@link OpenSeadragon.LegacyTileSource}. * * @param {Boolean} [options.debugMode=true] * Currently does nothing. TODO: provide an in-screen panel providing event * detail feedback. * * @param {Number} [options.animationTime=1.5] * Specifies the animation duration per each {@link OpenSeadragon.Spring} * which occur when the image is dragged or zoomed. * * @param {Number} [options.blendTime=0.5] * Specifies the duration of animation as higher or lower level tiles are * replacing the existing tile. * * @param {Boolean} [options.alwaysBlend=false] * Forces the tile to always blend. By default the tiles skip blending * when the blendTime is surpassed and the current animation frame would * not complete the blend. * * @param {Boolean} [options.autoHideControls=true] * If the user stops interacting with the viewport, fade the navigation * controls. Useful for presentation since the controls are by default * floated on top of the image the user is viewing. * * @param {Boolean} [options.immediateRender=false] * Render the best closest level first, ignoring the lowering levels which * provide the effect of very blurry to sharp. It is recommended to change * setting to true for mobile devices. * * @param {Boolean} [options.wrapHorizontal=false] * Set to true to force the image to wrap horizontally within the viewport. * Useful for maps or images representing the surface of a sphere or cylinder. * * @param {Boolean} [options.wrapVertical=false] * Set to true to force the image to wrap vertically within the viewport. * Useful for maps or images representing the surface of a sphere or cylinder. * * @param {Number} [options.minZoomImageRatio=0.8] * The minimum percentage ( expressed as a number between 0 and 1 ) of * the viewport height or width at which the zoom out will be constrained. * Setting it to 0, for example will allow you to zoom out infinitly. * * @param {Number} [options.maxZoomPixelRatio=2] * The maximum ratio to allow a zoom-in to affect the highest level pixel * ratio. This can be set to Infinity to allow 'infinite' zooming into the * image though it is less effective visually if the HTML5 Canvas is not * availble on the viewing device. * * @param {Number} [options.visibilityRatio=0.5] * The percentage ( as a number from 0 to 1 ) of the source image which * must be kept within the viewport. If the image is dragged beyond that * limit, it will 'bounce' back until the minimum visibility ration is * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to * true will provide the effect of an infinitely scrolling viewport. * * @param {Number} [options.springStiffness=5.0] * * @param {Number} [options.imageLoaderLimit=0] * The maximum number of image requests to make concurrently. By default * it is set to 0 allowing the browser to make the maximum number of * image requests in parallel as allowed by the browsers policy. * * @param {Number} [options.clickTimeThreshold=200] * If multiple mouse clicks occurs within less than this number of * milliseconds, treat them as a single click. * * @param {Number} [options.clickDistThreshold=5] * If a mouse or touch drag occurs and the distance to the starting drag * point is less than this many pixels, ignore the drag event. * * @param {Number} [options.zoomPerClick=2.0] * The "zoom distance" per mouse click or touch tap. * * @param {Number} [options.zoomPerScroll=1.2] * The "zoom distance" per mouse scroll or touch pinch. * * @param {Number} [options.zoomPerSecond=2.0] * The number of seconds to animate a single zoom event over. * * @param {Boolean} [options.showNavigationControl=true] * Set to false to prevent the appearance of the default navigation controls. * * @param {Number} [options.controlsFadeDelay=2000] * The number of milliseconds to wait once the user has stopped interacting * with the interface before begining to fade the controls. Assumes * showNavigationControl and autoHideControls are both true. * * @param {Number} [options.controlsFadeLength=1500] * The number of milliseconds to animate the controls fading out. * * @param {Number} [options.maxImageCacheCount=100] * The max number of images we should keep in memory (per drawer). * * @param {Number} [options.minPixelRatio=0.5] * The higher the minPixelRatio, the lower the quality of the image that * is considered sufficient to stop rendering a given zoom level. For * example, if you are targeting mobile devices with less bandwith you may * try setting this to 1.5 or higher. * * @param {Boolean} [options.mouseNavEnabled=true] * Is the user able to interact with the image via mouse or touch. Default * interactions include draging the image in a plane, and zooming in toward * and away from the image. * * @param {Boolean} [options.preserveViewport=false] * If the viewer has been configured with a sequence of tile sources, then * normally navigating to through each image resets the viewport to 'home' * position. If preserveViewport is set to true, then the viewport position * is preserved when navigating between images in the sequence. * * @param {String} [options.prefixUrl=''] * Appends the prefixUrl to navImages paths, which is very useful * since the default paths are rarely useful for production * environments. * * @param {Object} [options.navImages=] * An object with a property for each button or other built-in navigation * control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'. * Each of those in turn provides an image path for each state of the botton * or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the * image paths, by default assume there is a folder on the servers root path * called '/images', eg '/images/zoomin_rest.png'. If you need to adjust * these paths, prefer setting the option.prefixUrl rather than overriding * every image path directly through this setting. * * @returns {OpenSeadragon.Viewer} */ OpenSeadragon = window.OpenSeadragon || function( options ){ return new OpenSeadragon.Viewer( options ); }; (function( $ ){ /** * Taken from jquery 1.6.1 * [[Class]] -> type pairs * @private */ var class2type = { '[object Boolean]': 'boolean', '[object Number]': 'number', '[object String]': 'string', '[object Function]': 'function', '[object Array]': 'array', '[object Date]': 'date', '[object RegExp]': 'regexp', '[object Object]': 'object' }, // Save a reference to some core methods toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, push = Array.prototype.push, slice = Array.prototype.slice, trim = String.prototype.trim, indexOf = Array.prototype.indexOf; /** * Taken from jQuery 1.6.1 * @name $.isFunction * @function * @see jQuery */ $.isFunction = function( obj ) { return $.type(obj) === "function"; }; /** * Taken from jQuery 1.6.1 * @name $.isArray * @function * @see jQuery */ $.isArray = Array.isArray || function( obj ) { return $.type(obj) === "array"; }; /** * A crude way of determining if an object is a window. * Taken from jQuery 1.6.1 * @name $.isWindow * @function * @see jQuery */ $.isWindow = function( obj ) { return obj && typeof obj === "object" && "setInterval" in obj; }; /** * Taken from jQuery 1.6.1 * @name $.type * @function * @see jQuery */ $.type = function( obj ) { return obj == null ? String( obj ) : class2type[ toString.call(obj) ] || "object"; }; /** * Taken from jQuery 1.6.1 * @name $.isPlainObject * @function * @see jQuery */ $.isPlainObject = function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) { return false; } // Not own constructor property must be Object if ( obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for ( key in obj ) {} return key === undefined || hasOwn.call( obj, key ); }; /** * Taken from jQuery 1.6.1 * @name $.isEmptyObject * @function * @see jQuery */ $.isEmptyObject = function( obj ) { for ( var name in obj ) { return false; } return true; }; }( OpenSeadragon )); /** * This closure defines all static methods available to the OpenSeadragon * namespace. Many, if not most, are taked directly from jQuery for use * to simplify and reduce common programming patterns. More static methods * from jQuery may eventually make their way into this though we are * attempting to avoid substaintial plagarism or the more explicit dependency * on jQuery only because OpenSeadragon is a broadly useful code base and * would be made less broad by requiring jQuery fully. * * Some static methods have also been refactored from the original OpenSeadragon * project. */ (function( $ ){ /** * Taken from jQuery 1.6.1 * @see jQuery */ $.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[ 0 ] || {}, length = arguments.length, deep = false, i = 1; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[ 1 ] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( length === i ) { target = this; --i; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && OpenSeadragon.isArray( src ) ? src : []; } else { clone = src && OpenSeadragon.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them target[ name ] = OpenSeadragon.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; $.extend( $, { /** * These are the default values for the optional settings documented * in the {@link OpenSeadragon} constructor detail. * @name $.DEFAULT_SETTINGS * @static */ DEFAULT_SETTINGS: { //DATA SOURCE DETAILS xmlPath: null, tileSources: null, tileHost: null, //INTERFACE FEATURES debugMode: true, animationTime: 1.5, blendTime: 0.5, alwaysBlend: false, autoHideControls: true, immediateRender: false, wrapHorizontal: false, wrapVertical: false, panHorizontal: true, panVertical: true, visibilityRatio: 0.5, springStiffness: 5.0, clickTimeThreshold: 200, clickDistThreshold: 5, zoomPerClick: 2.0, zoomPerScroll: 1.2, zoomPerSecond: 2.0, showNavigationControl: true, controlsFadeDelay: 2000, controlsFadeLength: 1500, mouseNavEnabled: true, showNavigator: false, navigatorElement: null, navigatorHeight: null, navigatorWidth: null, navigatorPosition: null, navigatorSizeRatio: 0.25, preserveViewport: false, //PERFORMANCE SETTINGS minPixelRatio: 0.5, imageLoaderLimit: 0, maxImageCacheCount: 200, minZoomImageRatio: 0.9, maxZoomPixelRatio: 2, //INTERFACE RESOURCE SETTINGS prefixUrl: null, navImages: { zoomIn: { REST: '/images/zoomin_rest.png', GROUP: '/images/zoomin_grouphover.png', HOVER: '/images/zoomin_hover.png', DOWN: '/images/zoomin_pressed.png' }, zoomOut: { REST: '/images/zoomout_rest.png', GROUP: '/images/zoomout_grouphover.png', HOVER: '/images/zoomout_hover.png', DOWN: '/images/zoomout_pressed.png' }, home: { REST: '/images/home_rest.png', GROUP: '/images/home_grouphover.png', HOVER: '/images/home_hover.png', DOWN: '/images/home_pressed.png' }, fullpage: { REST: '/images/fullpage_rest.png', GROUP: '/images/fullpage_grouphover.png', HOVER: '/images/fullpage_hover.png', DOWN: '/images/fullpage_pressed.png' }, previous: { REST: '/images/previous_rest.png', GROUP: '/images/previous_grouphover.png', HOVER: '/images/previous_hover.png', DOWN: '/images/previous_pressed.png' }, next: { REST: '/images/next_rest.png', GROUP: '/images/next_grouphover.png', HOVER: '/images/next_hover.png', DOWN: '/images/next_pressed.png' } } }, /** * TODO: get rid of this. I can't see how it's required at all. Looks * like an early legacy code artifact. * @static * @ignore */ SIGNAL: "----seadragon----", /** * Invokes the the method as if it where a method belonging to the object. * @name $.delegate * @function * @param {Object} object * @param {Function} method */ delegate: function( object, method ) { return function() { if ( arguments === undefined ) arguments = []; return method.apply( object, arguments ); }; }, /** * An enumeration of Browser vendors including UNKNOWN, IE, FIREFOX, * SAFARI, CHROME, and OPERA. * @name $.BROWSERS * @static */ BROWSERS: { UNKNOWN: 0, IE: 1, FIREFOX: 2, SAFARI: 3, CHROME: 4, OPERA: 5 }, /** * Returns a DOM Element for the given id or element. * @function * @name OpenSeadragon.getElement * @param {String|Element} element Accepts an id or element. * @returns {Element} The element with the given id, null, or the element itself. */ getElement: function( element ) { if ( typeof ( element ) == "string" ) { element = document.getElementById( element ); } return element; }, /** * Determines the position of the upper-left corner of the element. * @function * @name OpenSeadragon.getElementPosition * @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(), isFixed, offsetParent; element = $.getElement( element ); isFixed = $.getElementStyle( element ).position == "fixed"; offsetParent = getOffsetParent( element, isFixed ); while ( offsetParent ) { result.x += element.offsetLeft; result.y += element.offsetTop; if ( isFixed ) { result = result.plus( $.getPageScroll() ); } element = offsetParent; isFixed = $.getElementStyle( element ).position == "fixed"; offsetParent = getOffsetParent( element, isFixed ); } return result; }, /** * Determines the height and width of the given element. * @function * @name OpenSeadragon.getElementSize * @param {Element|String} element * @returns {Point} */ getElementSize: function( element ) { element = $.getElement( element ); return new $.Point( element.clientWidth, element.clientHeight ); }, /** * Returns the CSSStyle object for the given element. * @function * @name OpenSeadragon.getElementStyle * @param {Element|String} element * @returns {CSSStyle} */ getElementStyle: function( element ) { element = $.getElement( element ); if ( element.currentStyle ) { return element.currentStyle; } else if ( window.getComputedStyle ) { return window.getComputedStyle( element, "" ); } else { throw new Error( "Unknown element style, no known technique." ); } }, /** * 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] * @returns {Event} */ getEvent: function( event ) { return event ? event : window.event; }, /** * Gets the position of the mouse on the screen for a given event. * @function * @name OpenSeadragon.getMousePosition * @param {Event} [event] * @returns {Point} */ getMousePosition: function( event ) { var result = new $.Point(); event = $.getEvent( event ); if ( typeof( event.pageX ) == "number" ) { result.x = event.pageX; result.y = event.pageY; } else if ( typeof( event.clientX ) == "number" ) { result.x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; result.y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; } else { throw new Error( "Unknown event mouse position, no known technique." ); } return result; }, /** * Determines the pages current scroll position. * @function * @name OpenSeadragon.getPageScroll * @returns {Point} */ getPageScroll: function() { var result = new $.Point(), docElement = document.documentElement || {}, body = document.body || {}; if ( typeof( window.pageXOffset ) == "number" ) { result.x = window.pageXOffset; result.y = window.pageYOffset; } else if ( body.scrollLeft || body.scrollTop ) { result.x = body.scrollLeft; result.y = body.scrollTop; } else if ( docElement.scrollLeft || docElement.scrollTop ) { result.x = docElement.scrollLeft; result.y = docElement.scrollTop; } return result; }, /** * Determines the size of the browsers window. * @function * @name OpenSeadragon.getWindowSize * @returns {Point} */ getWindowSize: function() { var result = new $.Point(), docElement = document.documentElement || {}, body = document.body || {}; if ( typeof( window.innerWidth ) == 'number' ) { result.x = window.innerWidth; result.y = window.innerHeight; } else if ( docElement.clientWidth || docElement.clientHeight ) { result.x = docElement.clientWidth; result.y = docElement.clientHeight; } else if ( body.clientWidth || body.clientHeight ) { result.x = body.clientWidth; result.y = body.clientHeight; } else { throw new Error("Unknown window size, no known technique."); } return result; }, /** * 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 * @returns {Element} */ makeCenteredNode: function( element ) { var div = $.makeNeutralElement( "div" ), html = [], innerDiv, innerDivs; element = $.getElement( element ); //TODO: I dont understand the use of # inside the style attributes // below. Invetigate the results of the constructed html in // the browser and clean up the mark-up to make this clearer. html.push('
'); html.push('
'); html.push('
'); div.innerHTML = html.join( '' ); div = div.firstChild; innerDiv = div; innerDivs = div.getElementsByTagName( "div" ); while ( innerDivs.length > 0 ) { innerDiv = innerDivs[ 0 ]; innerDivs = innerDiv.getElementsByTagName( "div" ); } innerDiv.appendChild( element ); return div; }, /** * Creates an easily positionable element of the given type that therefor * serves as an excellent container element. * @function * @name OpenSeadragon.makeNeutralElement * @param {String} tagName * @returns {Element} */ makeNeutralElement: function( tagName ) { var element = document.createElement( tagName ), style = element.style; style.background = "transparent none"; style.border = "none"; style.margin = "0px"; style.padding = "0px"; style.position = "static"; return element; }, /** * 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 * @returns {Element} */ makeTransparentImage: function( src ) { var img = $.makeNeutralElement( "img" ), element = null; if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) { element = $.makeNeutralElement("span"); element.style.display = "inline-block"; img.onload = function() { element.style.width = element.style.width || img.width + "px"; element.style.height = element.style.height || img.height + "px"; img.onload = null; img = null; // to prevent memory leaks in IE }; img.src = src; element.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "', sizingMethod='scale')"; } else { element = img; element.src = src; } return element; }, /** * Sets the opacity of the specified element. * @function * @name OpenSeadragon.setElementOpacity * @param {Element|String} element * @param {Number} opacity * @param {Boolean} [usesAlpha] */ setElementOpacity: function( element, opacity, usesAlpha ) { var previousFilter, ieOpacity, ieFilter; element = $.getElement( element ); if ( usesAlpha && !$.Browser.alpha ) { opacity = Math.round( opacity ); } if ( opacity < 1 ) { element.style.opacity = opacity; } else { element.style.opacity = ""; } if ( opacity == 1 ) { prevFilter = element.style.filter || ""; element.style.filter = prevFilter.replace(/alpha\(.*?\)/g, ""); return; } ieOpacity = Math.round( 100 * opacity ); ieFilter = " alpha(opacity=" + ieOpacity + ") "; //TODO: find out why this uses a try/catch instead of a predetermined // routine or at least an if/elseif/else try { if ( element.filters && element.filters.alpha ) { element.filters.alpha.opacity = ieOpacity; } else { element.style.filter += ieFilter; } } catch ( e ) { element.style.filter += ieFilter; } }, /** * Adds an event listener for the given element, eventName and handler. * @function * @name OpenSeadragon.addEvent * @param {Element|String} element * @param {String} eventName * @param {Function} handler * @param {Boolean} [useCapture] * @throws {Error} */ addEvent: function( element, eventName, handler, useCapture ) { element = $.getElement( element ); //TODO: Why do this if/else on every method call instead of just // defining this function once based on the same logic if ( element.addEventListener ) { element.addEventListener( eventName, handler, useCapture ); } else if ( element.attachEvent ) { element.attachEvent( "on" + eventName, handler ); if ( useCapture && element.setCapture ) { element.setCapture(); } } else { throw new Error( "Unable to attach event handler, no known technique." ); } }, /** * Remove a given event listener for the given element, event type and * handler. * @function * @name OpenSeadragon.removeEvent * @param {Element|String} element * @param {String} eventName * @param {Function} handler * @param {Boolean} [useCapture] * @throws {Error} */ removeEvent: function( element, eventName, handler, useCapture ) { element = $.getElement( element ); //TODO: Why do this if/else on every method call instead of just // defining this function once based on the same logic if ( element.removeEventListener ) { element.removeEventListener( eventName, handler, useCapture ); } else if ( element.detachEvent ) { element.detachEvent("on" + eventName, handler); if ( useCapture && element.releaseCapture ) { element.releaseCapture(); } } else { throw new Error( "Unable to detach event handler, no known technique." ); } }, /** * 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] */ cancelEvent: function( event ) { event = $.getEvent( event ); if ( event.preventDefault ) { // W3C for preventing default event.preventDefault(); } // legacy for preventing default event.cancel = true; // IE for preventing default event.returnValue = false; }, /** * Stops the propagation of the event up the DOM. * @function * @name OpenSeadragon.stopEvent * @param {Event} [event] */ stopEvent: function( event ) { event = $.getEvent( event ); if ( event.stopPropagation ) { event.stopPropagation(); // W3C for stopping propagation } event.cancelBubble = true; // IE for stopping propagation }, /** * 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 * @returns {Function} */ createCallback: function( object, method ) { //TODO: This pattern is painful to use and debug. It's much cleaner // to use pinning plus anonymous functions. Get rid of this // pattern! var initialArgs = [], i; for ( i = 2; i < arguments.length; i++ ) { initialArgs.push( arguments[ i ] ); } return function() { var args = initialArgs.concat( [] ), i; for ( i = 0; i < arguments.length; i++ ) { args.push( arguments[ i ] ); } return method.apply( object, args ); }; }, /** * Retreives the value of a url parameter from the window.location string. * @function * @name OpenSeadragon.getUrlParameter * @param {String} key * @returns {String} The value of the url parameter or null if no param matches. */ getUrlParameter: function( key ) { var value = URLPARAMS[ key ]; return value ? value : null; }, /** * Makes an AJAX request. * @function * @name OpenSeadragon.makeAjaxRequest * @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", request = null, actual, i; if ( async ) { actual = callback; callback = function() { window.setTimeout( $.createCallback( null, actual, request ), 1 ); }; } if ( window.ActiveXObject ) { //TODO: very bad...Why check every time using try/catch when // we could determine once at startup which activeX object // was supported. This will have significant impact on // performance for IE Browsers for ( i = 0; i < ACTIVEX.length; i++ ) { try { request = new ActiveXObject( ACTIVEX[ i ] ); break; } catch (e) { continue; } } } else if ( window.XMLHttpRequest ) { request = new XMLHttpRequest(); } if ( !request ) { throw new Error( "Browser doesn't support XMLHttpRequest." ); } if ( async ) { /** @ignore */ request.onreadystatechange = function() { if ( request.readyState == 4) { request.onreadystatechange = new function() { }; callback(); } }; } try { request.open( "GET", url, async ); request.send( null ); } catch (e) { $.console.log( "%s while making AJAX request: %s", e.name, e.message ); request.onreadystatechange = null; request = null; if ( async ) { callback(); } } return async ? null : request; }, /** * Taken from jQuery 1.6.1 * @param {Object} options * @param {String} options.url * @param {Function} options.callback * @param {String} [options.param='callback'] The name of the url parameter * to request the jsonp provider with. * @param {String} [options.callbackName=] The name of the callback to * request the jsonp provider with. */ jsonp: function( options ){ var script, url = options.url, head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement, jsonpCallback = options.callbackName || 'openseadragon' + (+new Date()), previous = window[ jsonpCallback ], replace = "$1" + jsonpCallback + "$2", callbackParam = options.param || 'callback', callback = options.callback; url = url.replace( /(\=)\?(&|$)|\?\?/i, replace ); // Add callback manually url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback; // Install callback window[ jsonpCallback ] = function( response ) { if ( !previous ){ delete window[ jsonpCallback ]; } else { window[ jsonpCallback ] = previous; } if( callback && $.isFunction( callback ) ){ callback( response ); } }; script = document.createElement( "script" ); script.async = "async"; if ( options.scriptCharset ) { script.charset = options.scriptCharset; } script.src = url; // Attach handlers for all browsers script.onload = script.onreadystatechange = function( _, isAbort ) { if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; // Remove the script if ( head && script.parentNode ) { head.removeChild( script ); } // Dereference the script script = undefined; } }; // Use insertBefore instead of appendChild to circumvent an IE6 bug. // This arises when a base node is used (#2709 and #4378). head.insertBefore( script, head.firstChild ); }, /** * Loads a Deep Zoom Image description from a url or XML string and * provides a callback hook for the resulting Document * @function * @name OpenSeadragon.createFromDZI * @param {String} xmlUrl * @param {String} xmlString * @param {Function} callback */ createFromDZI: function( dzi, callback, tileHost ) { var async = typeof ( callback ) == "function", xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, xmlString = xmlUrl ? null : dzi, error = null, urlParts, filename, lastDot, tilesUrl; if( tileHost ){ tilesUrl = tileHost + "/_files/"; } else if( xmlUrl ) { 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 ) { 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 ) ); } } }); /** * The current browser vendor, version, and related information regarding * detected features. Features include
* 'alpha' - Does the browser support image alpha * transparency.
* @name $.Browser * @static */ $.Browser = { vendor: $.BROWSERS.UNKNOWN, version: 0, alpha: true }; var ACTIVEX = [ "Msxml2.XMLHTTP", "Msxml3.XMLHTTP", "Microsoft.XMLHTTP" ], FILEFORMATS = { "bmp": false, "jpeg": true, "jpg": true, "png": true, "tif": false, "wdp": false }, URLPARAMS = {}; (function() { //A small auto-executing routine to determine the browser vendor, //version and supporting feature sets. var app = navigator.appName, ver = navigator.appVersion, ua = navigator.userAgent; //console.error( 'appName: ' + navigator.appName ); //console.error( 'appVersion: ' + navigator.appVersion ); //console.error( 'userAgent: ' + navigator.userAgent ); switch( navigator.appName ){ case "Microsoft Internet Explorer": if( !!window.attachEvent && !!window.ActiveXObject ) { $.Browser.vendor = $.BROWSERS.IE; $.Browser.version = parseFloat( ua.substring( ua.indexOf( "MSIE" ) + 5, ua.indexOf( ";", ua.indexOf( "MSIE" ) ) ) ); } break; case "Netscape": if( !!window.addEventListener ){ if ( ua.indexOf( "Firefox" ) >= 0 ) { $.Browser.vendor = $.BROWSERS.FIREFOX; $.Browser.version = parseFloat( ua.substring( ua.indexOf( "Firefox" ) + 8 ) ); } else if ( ua.indexOf( "Safari" ) >= 0 ) { $.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ? $.BROWSERS.CHROME : $.BROWSERS.SAFARI; $.Browser.version = parseFloat( ua.substring( ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1, ua.indexOf( "Safari" ) ) ); } } break; case "Opera": $.Browser.vendor = $.BROWSERS.OPERA; $.Browser.version = parseFloat( ver ); break; } // ignore '?' portion of query string var query = window.location.search.substring( 1 ), parts = query.split('&'), part, sep, i; for ( i = 0; i < parts.length; i++ ) { part = parts[ i ]; sep = part.indexOf( '=' ); if ( sep > 0 ) { URLPARAMS[ part.substring( 0, sep ) ] = decodeURIComponent( part.substring( sep + 1 ) ); } } //determine if this browser supports image alpha transparency $.Browser.alpha = !( $.Browser.vendor == $.BROWSERS.IE || ( $.Browser.vendor == $.BROWSERS.CHROME && $.Browser.version < 2 ) ); })(); //TODO: $.console is often used inside a try/catch block which generally // prevents allowings errors to occur with detection until a debugger // is attached. Although I've been guilty of the same anti-pattern // I eventually was convinced that errors should naturally propogate in // all but the most special cases. /** * A convenient alias for console when available, and a simple null * function when console is unavailable. * @static * @private */ var nullfunction = function( msg ){ //document.location.hash = msg; }; $.console = window.console || { log: nullfunction, debug: nullfunction, info: nullfunction, warn: nullfunction, error: nullfunction }; /** * @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 )); (function($){ /** * For use by classes which want to support custom, non-browser events. * @class */ $.EventHandler = function() { this.events = {}; }; $.EventHandler.prototype = { /** * Add an event handler for a given event. * @function * @param {String} eventName - Name of event to register. * @param {Function} handler - Function to call when event is triggered. */ addHandler: function( eventName, handler ) { var events = this.events[ eventName ]; if( !events ){ this.events[ eventName ] = events = []; } if( handler && $.isFunction( handler ) ){ events[ events.length ] = handler; } }, /** * Remove a specific event handler for a given event. * @function * @param {String} eventName - Name of event for which the handler is to be removed. * @param {Function} handler - Function to be removed. */ removeHandler: function( eventName, handler ) { //Start Thatcher - unneccessary indirection. Also, because events were // - not actually being removed, we need to add the code // - to do the removal ourselves. TODO var events = this.events[ eventName ]; if ( !events ){ return; } //End Thatcher }, /** * Retrive the list of all handlers registered for a given event. * @function * @param {String} eventName - Name of event to get handlers for. */ getHandler: function( eventName ) { var events = this.events[ eventName ]; if ( !events || !events.length ){ return null; } events = events.length === 1 ? [ events[ 0 ] ] : Array.apply( null, events ); return function( source, args ) { var i, length = events.length; for ( i = 0; i < length; i++ ) { if( events[ i ] ){ events[ i ]( source, args ); } } }; }, /** * Trigger an event, optionally passing additional information. * @function * @param {String} eventName - Name of event to register. * @param {Function} handler - Function to call when event is triggered. */ raiseEvent: function( eventName, eventArgs ) { var handler = this.getHandler( eventName ); if ( handler ) { if ( !eventArgs ) { eventArgs = new Object(); } handler( this, eventArgs ); } } }; }( OpenSeadragon )); (function( $ ){ // is any button currently being pressed while mouse events occur var IS_BUTTON_DOWN = false, // is any tracker currently capturing? IS_CAPTURING = false, // dictionary from hash to MouseTracker ACTIVE = {}, // list of trackers interested in capture CAPTURING = [], // dictionary from hash to private properties THIS = {}; /** * The MouseTracker allows other classes to set handlers for common mouse * events on a specific element like, 'enter', 'exit', 'press', 'release', * 'scroll', 'click', and 'drag'. * @class * @param {Object} options * Allows configurable properties to be entirely specified by passing * an options object to the constructor. The constructor also supports * the original positional arguments 'elements', 'clickTimeThreshold', * and 'clickDistThreshold' in that order. * @param {Element|String} options.element * A reference to an element or an element id for which the mouse * events will be monitored. * @param {Number} options.clickTimeThreshold * The number of milliseconds within which mutliple mouse clicks * will be treated as a single event. * @param {Number} options.clickDistThreshold * The distance between mouse click within multiple mouse clicks * will be treated as a single event. * @param {Function} options.enterHandler * An optional handler for mouse enter. * @param {Function} options.exitHandler * An optional handler for mouse exit. * @param {Function} options.pressHandler * An optional handler for mouse press. * @param {Function} options.releaseHandler * An optional handler for mouse release. * @param {Function} options.scrollHandler * An optional handler for mouse scroll. * @param {Function} options.clickHandler * An optional handler for mouse click. * @param {Function} options.dragHandler * An optional handler for mouse drag. * @property {Number} hash * An unique hash for this tracker. * @property {Element} element * The element for which mouse event are being monitored. * @property {Number} clickTimeThreshold * The number of milliseconds within which mutliple mouse clicks * will be treated as a single event. * @property {Number} clickDistThreshold * The distance between mouse click within multiple mouse clicks * will be treated as a single event. */ $.MouseTracker = function ( options ) { var args = arguments; if( !$.isPlainObject( options ) ){ options = { element: args[ 0 ], clickTimeThreshold: args[ 1 ], clickDistThreshold: args[ 2 ] }; } this.hash = Math.random(); this.element = $.getElement( options.element ); this.clickTimeThreshold = options.clickTimeThreshold; this.clickDistThreshold = options.clickDistThreshold; this.enterHandler = options.enterHandler || null; this.exitHandler = options.exitHandler || null; this.pressHandler = options.pressHandler || null; this.releaseHandler = options.releaseHandler || null; this.scrollHandler = options.scrollHandler || null; this.clickHandler = options.clickHandler || null; this.dragHandler = options.dragHandler || null; this.keyHandler = options.keyHandler || null; this.focusHandler = options.focusHandler || null; this.blurHandler = options.blurHandler || null; //Store private properties in a scope sealed hash map var _this = this; /** * @private * @property {Boolean} tracking * Are we currently tracking mouse events. * @property {Boolean} capturing * Are we curruently capturing mouse events. * @property {Boolean} buttonDown * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @property {Boolean} insideElement * Are we currently inside the screen area of the tracked element. * @property {OpenSeadragon.Point} lastPoint * Position of last mouse down/move * @property {Number} lastMouseDownTime * Time of last mouse down. * @property {OpenSeadragon.Point} lastMouseDownPoint * Position of last mouse down */ THIS[ this.hash ] = { "mouseover": function( event ){ onMouseOver( _this, event ); }, "mouseout": function( event ){ onMouseOut( _this, event ); }, "mousedown": function( event ){ onMouseDown( _this, event ); }, "mouseup": function( event ){ onMouseUp( _this, event ); }, "click": function( event ){ onMouseClick( _this, event ); }, "DOMMouseScroll": function( event ){ onMouseWheelSpin( _this, event ); }, "mousewheel": function( event ){ onMouseWheelSpin( _this, event ); }, "mouseupie": function( event ){ onMouseUpIE( _this, event ); }, "mousemoveie": function( event ){ onMouseMoveIE( _this, event ); }, "mouseupwindow": function( event ){ onMouseUpWindow( _this, event ); }, "mousemove": function( event ){ onMouseMove( _this, event ); }, "touchstart": function( event ){ onTouchStart( _this, event ); }, "touchmove": function( event ){ onTouchMove( _this, event ); }, "touchend": function( event ){ onTouchEnd( _this, event ); }, "keypress": function( event ){ onKeyPress( _this, event ); }, "focus": function( event ){ onFocus( _this, event ); }, "blur": function( event ){ onBlur( _this, event ); }, tracking: false, capturing: false, buttonDown: false, insideElement: false, lastPoint: null, lastMouseDownTime: null, lastMouseDownPoint: null, lastPinchDelta: 0 }; }; $.MouseTracker.prototype = { /** * Are we currently tracking events on this element. * @deprecated Just use this.tracking * @function * @returns {Boolean} Are we currently tracking events on this element. */ isTracking: function () { return THIS[ this.hash ].tracking; }, /** * Enable or disable whether or not we are tracking events on this element. * @function * @param {Boolean} track True to start tracking, false to stop tracking. * @returns {OpenSeadragon.MouseTracker} Chainable. */ setTracking: function ( track ) { if ( track ) { startTracking( this ); } else { stopTracking( this ); } //chain return this; }, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {Boolean} buttonDown * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} buttonDownAny * Was the button down anywhere in the screen during the event. */ enterHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {Boolean} buttonDown * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} buttonDownAny * Was the button down anywhere in the screen during the event. */ exitHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. */ pressHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {Boolean} buttonDown * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} insideElementRelease * Was the mouse still inside the tracked element when the button * was released. */ releaseHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {Number} scroll * The scroll delta for the event. * @param {Boolean} shift * Was the shift key being pressed during this event? */ scrollHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {Boolean} quick * True only if the clickDistThreshold and clickDeltaThreshold are * both pased. Useful for ignoring events. * @param {Boolean} shift * Was the shift key being pressed during this event? */ clickHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {OpenSeadragon.Point} position * The poistion of the event on the screen. * @param {OpenSeadragon.Point} delta * The x,y components of the difference between start drag and * end drag. Usefule for ignoring or weighting the events. * @param {Boolean} shift * Was the shift key being pressed during this event? */ dragHandler: function(){}, /** * Implement or assign implmentation to these handlers during or after * calling the constructor. * @function * @param {OpenSeadragon.MouseTracker} tracker * A reference to the tracker instance. * @param {Number} keyCode * The key code that was pressed. * @param {Boolean} shift * Was the shift key being pressed during this event? */ keyHandler: function(){}, focusHandler: function(){}, blurHandler: function(){} }; /** * Starts tracking mouse events on this element. * @private * @inner */ function startTracking( tracker ) { var events = [ "mouseover", "mouseout", "mousedown", "mouseup", "click", "DOMMouseScroll", "mousewheel", "touchstart", "touchmove", "touchend", "keypress", "focus", "blur" ], delegate = THIS[ tracker.hash ], event, i; if ( !delegate.tracking ) { for( i = 0; i < events.length; i++ ){ event = events[ i ]; $.addEvent( tracker.element, event, delegate[ event ], false ); } delegate.tracking = true; ACTIVE[ tracker.hash ] = tracker; } }; /** * Stops tracking mouse events on this element. * @private * @inner */ function stopTracking( tracker ) { var events = [ "mouseover", "mouseout", "mousedown", "mouseup", "click", "DOMMouseScroll", "mousewheel", "touchstart", "touchmove", "touchend", "keypress", "focus", "blur" ], delegate = THIS[ tracker.hash ], event, i; if ( delegate.tracking ) { for( i = 0; i < events.length; i++ ){ event = events[ i ]; $.removeEvent( tracker.element, event, delegate[ event ], false ); } releaseMouse( tracker ); delegate.tracking = false; delete ACTIVE[ tracker.hash ]; } }; /** * @private * @inner */ function hasMouse( tracker ) { return THIS[ tracker.hash ].insideElement; }; /** * Begin capturing mouse events on this element. * @private * @inner */ function captureMouse( tracker ) { var delegate = THIS[ tracker.hash ]; if ( !delegate.capturing ) { if ( $.Browser.vendor == $.BROWSERS.IE ) { $.removeEvent( tracker.element, "mouseup", delegate[ "mouseup" ], false ); $.addEvent( tracker.element, "mouseup", delegate[ "mouseupie" ], true ); $.addEvent( tracker.element, "mousemove", delegate[ "mousemoveie" ], true ); } else { $.addEvent( window, "mouseup", delegate[ "mouseupwindow" ], true ); $.addEvent( window, "mousemove", delegate[ "mousemove" ], true ); } delegate.capturing = true; } }; /** * Stop capturing mouse events on this element. * @private * @inner */ function releaseMouse( tracker ) { var delegate = THIS[ tracker.hash ]; if ( delegate.capturing ) { if ( $.Browser.vendor == $.BROWSERS.IE ) { $.removeEvent( tracker.element, "mousemove", delegate[ "mousemoveie" ], true ); $.removeEvent( tracker.element, "mouseup", delegate[ "mouseupie" ], true ); $.addEvent( tracker.element, "mouseup", delegate[ "mouseup" ], false ); } else { $.removeEvent( window, "mousemove", delegate[ "mousemove" ], true ); $.removeEvent( window, "mouseup", delegate[ "mouseupwindow" ], true ); } delegate.capturing = false; } }; /** * @private * @inner */ function triggerOthers( tracker, handler, event ) { var otherHash; for ( otherHash in ACTIVE ) { if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { handler( ACTIVE[ otherHash ], event ); } } }; /** * @private * @inner */ function onFocus( tracker, event ){ //console.log( "focus %s", event ); var propagate; if ( tracker.focusHandler ) { try { propagate = tracker.focusHandler( tracker, event ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing key handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onBlur( tracker, event ){ //console.log( "blur %s", event ); var propagate; if ( tracker.blurHandler ) { try { propagate = tracker.blurHandler( tracker, event ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing key handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onKeyPress( tracker, event ){ //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); var propagate; if ( tracker.keyHandler ) { try { propagate = tracker.keyHandler( tracker, event.keyCode ? event.keyCode : event.charCode, event.shiftKey ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing key handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onMouseOver( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && !isChild( event.srcElement, tracker.element ) ) { triggerOthers( tracker, onMouseOver, event ); } var to = event.target ? event.target : event.srcElement, from = event.relatedTarget ? event.relatedTarget : event.fromElement; if ( !isChild( tracker.element, to ) || isChild( tracker.element, from ) ) { return; } delegate.insideElement = true; if ( tracker.enterHandler ) { try { propagate = tracker.enterHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing enter handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onMouseOut( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && !isChild( event.srcElement, tracker.element ) ) { triggerOthers( tracker, onMouseOut, event ); } var from = event.target ? event.target : event.srcElement, to = event.relatedTarget ? event.relatedTarget : event.toElement; if ( !isChild( tracker.element, from ) || isChild( tracker.element, to ) ) { return; } delegate.insideElement = false; if ( tracker.exitHandler ) { try { propagate = tracker.exitHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing exit handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onMouseDown( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], propagate; if ( event.button == 2 ) { return; } delegate.buttonDown = true; delegate.lastPoint = getMouseAbsolute( event ); delegate.lastMouseDownPoint = delegate.lastPoint; delegate.lastMouseDownTime = +new Date(); if ( tracker.pressHandler ) { try { propagate = tracker.pressHandler( tracker, getMouseRelative( event, tracker.element ) ); if( propagate === false ){ $.cancelEvent( event ); } } catch (e) { $.console.error( "%s while executing press handler: %s", e.name, e.message, e ); } } if ( tracker.pressHandler || tracker.dragHandler ) { $.cancelEvent( event ); } if ( !( $.Browser.vendor == $.BROWSERS.IE ) || !IS_CAPTURING ) { captureMouse( tracker ); IS_CAPTURING = true; // reset to empty & add us CAPTURING = [ tracker ]; } else if ( $.Browser.vendor == $.BROWSERS.IE ) { // add us to the list CAPTURING.push( tracker ); } }; /** * @private * @inner */ function onTouchStart( tracker, event ) { var touchA, touchB; if( event.touches.length == 1 && event.targetTouches.length == 1 && event.changedTouches.length == 1 ){ THIS[ tracker.hash ].lastTouch = event.touches[ 0 ]; onMouseOver( tracker, event.changedTouches[ 0 ] ); onMouseDown( tracker, event.touches[ 0 ] ); } if( event.touches.length == 2 ){ touchA = getMouseAbsolute( event.touches[ 0 ] ); touchB = getMouseAbsolute( event.touches[ 1 ] ); THIS[ tracker.hash ].lastPinchDelta = Math.abs( touchA.x - touchB.x ) + Math.abs( touchA.y - touchB.y ); //$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta); } event.preventDefault(); }; /** * @private * @inner */ function onMouseUp( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], //were we inside the tracked element when we were pressed insideElementPress = delegate.buttonDown, //are we still inside the tracked element when we released insideElementRelease = delegate.insideElement, propagate; if ( event.button == 2 ) { return; } delegate.buttonDown = false; if ( tracker.releaseHandler ) { try { propagate = tracker.releaseHandler( tracker, getMouseRelative( event, tracker.element ), insideElementPress, insideElementRelease ); if( propagate === false ){ $.cancelEvent( event ); } } catch (e) { $.console.error( "%s while executing release handler: %s", e.name, e.message, e ); } } if ( insideElementPress && insideElementRelease ) { handleMouseClick( tracker, event ); } }; /** * @private * @inner */ function onTouchEnd( tracker, event ) { if( event.touches.length == 0 && event.targetTouches.length == 0 && event.changedTouches.length == 1 ){ THIS[ tracker.hash ].lastTouch = null; onMouseUp( tracker, event.changedTouches[ 0 ] ); onMouseOut( tracker, event.changedTouches[ 0 ] ); } if( event.touches.length + event.changedTouches.length == 2 ){ THIS[ tracker.hash ].lastPinchDelta = null; //$.console.debug("pinch end"); } event.preventDefault(); }; /** * Only triggered once by the deepest element that initially received * the mouse down event. We want to make sure THIS event doesn't bubble. * Instead, we want to trigger the elements that initially received the * mouse down event (including this one) only if the mouse is no longer * inside them. Then, we want to release capture, and emulate a regular * mouseup on the event that this event was meant for. * @private * @inner */ function onMouseUpIE( tracker, event ) { var event = $.getEvent( event ), othertracker, i; if ( event.button == 2 ) { return; } for ( i = 0; i < CAPTURING.length; i++ ) { othertracker = CAPTURING[ i ]; if ( !hasMouse( othertracker ) ) { onMouseUp( othertracker, event ); } } releaseMouse( tracker ); IS_CAPTURING = false; event.srcElement.fireEvent( "on" + event.type, document.createEventObject( event ) ); $.stopEvent( event ); }; /** * Only triggered in W3C browsers by elements within which the mouse was * initially pressed, since they are now listening to the window for * mouseup during the capture phase. We shouldn't handle the mouseup * here if the mouse is still inside this element, since the regular * mouseup handler will still fire. * @private * @inner */ function onMouseUpWindow( tracker, event ) { if ( ! THIS[ tracker.hash ].insideElement ) { onMouseUp( tracker, event ); } releaseMouse( tracker ); }; /** * @private * @inner */ function onMouseClick( tracker, event ) { if ( tracker.clickHandler ) { $.cancelEvent( event ); } }; /** * @private * @inner */ function onMouseWheelSpin( tracker, event ) { var nDelta = 0, propagate; if ( !event ) { // For IE, access the global (window) event object event = window.event; } if ( event.wheelDelta ) { // IE and Opera nDelta = event.wheelDelta; if ( window.opera ) { // Opera has the values reversed nDelta = -nDelta; } } else if (event.detail) { // Mozilla FireFox nDelta = -event.detail; } //The nDelta variable is gated to provide smooth z-index scrolling //since the mouse wheel allows for substantial deltas meant for rapid //y-index scrolling. nDelta = nDelta > 0 ? 1 : -1; if ( tracker.scrollHandler ) { try { propagate = tracker.scrollHandler( tracker, getMouseRelative( event, tracker.element ), nDelta, event.shiftKey ); if( propagate === false ){ $.cancelEvent( event ); } } catch (e) { $.console.error( "%s while executing scroll handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function handleMouseClick( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], propagate; if ( event.button == 2 ) { return; } var time = +new Date() - delegate.lastMouseDownTime, point = getMouseAbsolute( event ), distance = delegate.lastMouseDownPoint.distanceTo( point ), quick = time <= tracker.clickTimeThreshold && distance <= tracker.clickDistThreshold; if ( tracker.clickHandler ) { try { propagate = tracker.clickHandler( tracker, getMouseRelative( event, tracker.element ), quick, event.shiftKey ); if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { $.console.error( "%s while executing click handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onMouseMove( tracker, event ) { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], point = getMouseAbsolute( event ), delta = point.minus( delegate.lastPoint ), propagate; delegate.lastPoint = point; if ( tracker.dragHandler ) { try { propagate = tracker.dragHandler( tracker, getMouseRelative( event, tracker.element ), delta, event.shiftKey ); if( propagate === false ){ $.cancelEvent( event ); } } catch (e) { $.console.error( "%s while executing drag handler: %s", e.name, e.message, e ); } } }; /** * @private * @inner */ function onTouchMove( tracker, event ) { var touchA, touchB, pinchDelta; if( event.touches.length === 1 && event.targetTouches.length === 1 && event.changedTouches.length === 1 && THIS[ tracker.hash ].lastTouch === event.touches[ 0 ]){ onMouseMove( tracker, event.touches[ 0 ] ); } else if ( event.touches.length === 2 ){ touchA = getMouseAbsolute( event.touches[ 0 ] ); touchB = getMouseAbsolute( event.touches[ 1 ] ); pinchDelta = Math.abs( touchA.x - touchB.x ) + Math.abs( touchA.y - touchB.y ); //TODO: make the 75px pinch threshold configurable if( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ){ //$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta); onMouseWheelSpin( tracker, { shift: false, pageX: ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2, pageY: ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2, detail:( THIS[ tracker.hash ].lastPinchDelta > pinchDelta ) ? 1 : -1 }); THIS[ tracker.hash ].lastPinchDelta = pinchDelta; } } event.preventDefault(); }; /** * Only triggered once by the deepest element that initially received * the mouse down event. Since no other element has captured the mouse, * we want to trigger the elements that initially received the mouse * down event (including this one). The the param tracker isn't used * but for consistency with the other event handlers we include it. * @private * @inner */ function onMouseMoveIE( tracker, event ) { var i; for ( i = 0; i < CAPTURING.length; i++ ) { onMouseMove( CAPTURING[ i ], event ); } $.stopEvent( event ); }; /** * @private * @inner */ function getMouseAbsolute( event ) { return $.getMousePosition( event ); }; /** * @private * @inner */ function getMouseRelative( event, element ) { var mouse = $.getMousePosition( event ), offset = $.getElementPosition( element ); return mouse.minus( offset ); }; /** * @private * @inner * Returns true if elementB is a child node of elementA, or if they're equal. */ function isChild( elementA, elementB ) { var body = document.body; while ( elementB && elementA != elementB && body != elementB ) { try { elementB = elementB.parentNode; } catch (e) { return false; } } return elementA == elementB; }; /** * @private * @inner */ function onGlobalMouseDown() { IS_BUTTON_DOWN = true; }; /** * @private * @inner */ function onGlobalMouseUp() { IS_BUTTON_DOWN = false; }; (function () { if ( $.Browser.vendor == $.BROWSERS.IE ) { $.addEvent( document, "mousedown", onGlobalMouseDown, false ); $.addEvent( document, "mouseup", onGlobalMouseUp, false ); } else { $.addEvent( window, "mousedown", onGlobalMouseDown, true ); $.addEvent( window, "mouseup", onGlobalMouseUp, true ); } })(); }( OpenSeadragon )); (function( $ ){ /** * An enumeration of supported locations where controls can be anchored, * including NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, and BOTTOM_LEFT. * The anchoring is always relative to the container * @static */ $.ControlAnchor = { NONE: 0, TOP_LEFT: 1, TOP_RIGHT: 2, BOTTOM_RIGHT: 3, BOTTOM_LEFT: 4 }; /** * A Control represents any interface element which is meant to allow the user * to interact with the zoomable interface. Any control can be anchored to any * element. * @class * @param {Element} element - the contol element to be anchored in the container. * @param {OpenSeadragon.ControlAnchor} anchor - the location to anchor at. * @param {Element} container - the element to control will be anchored too. * * @property {Element} element - the element providing the user interface with * some type of control. Eg a zoom-in button * @property {OpenSeadragon.ControlAnchor} anchor - the position of the control * relative to the container. * @property {Element} container - the element within with the control is * positioned. * @property {Element} wrapper - a nuetral element surrounding the control * element. */ $.Control = function ( element, anchor, container ) { this.element = element; this.anchor = anchor; this.container = container; this.wrapper = $.makeNeutralElement( "span" ); this.wrapper.style.display = "inline-block"; this.wrapper.appendChild( this.element ); if ( this.anchor == $.ControlAnchor.NONE ) { // IE6 fix this.wrapper.style.width = this.wrapper.style.height = "100%"; } if ( this.anchor == $.ControlAnchor.TOP_RIGHT || this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) { this.container.insertBefore( this.wrapper, this.container.firstChild ); } else { this.container.appendChild( this.wrapper ); } }; $.Control.prototype = { /** * Removes the control from the container. * @function */ destroy: function() { this.wrapper.removeChild( this.element ); this.container.removeChild( this.wrapper ); }, /** * Determines if the control is currently visible. * @function * @return {Boolean} true if currenly visible, false otherwise. */ isVisible: function() { return this.wrapper.style.display != "none"; }, /** * Toggles the visibility of the control. * @function * @param {Boolean} visible - true to make visible, false to hide. */ setVisible: function( visible ) { this.wrapper.style.display = visible ? "inline-block" : "none"; }, /** * Sets the opacity level for the control. * @function * @param {Number} opactiy - a value between 1 and 0 inclusively. */ setOpacity: function( opacity ) { if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) { $.setElementOpacity( this.element, opacity, true ); } else { $.setElementOpacity( this.wrapper, opacity, true ); } } }; }( OpenSeadragon )); (function( $ ){ //id hash for private properties; var THIS = {}; /** * @class */ $.ControlDock = function( options ){ var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'], layout, i; $.extend( true, this, { id: 'controldock-'+(+new Date())+'-'+Math.floor(Math.random()*1000000), container: $.makeNeutralElement('form'), controls: [] }, options ); if( this.element ){ this.element = $.getElement( this.element ); this.element.appendChild( this.container ); this.element.style.position = 'relative'; this.container.style.width = '100%'; this.container.style.height = '100%'; } for( i = 0; i < layouts.length; i++ ){ layout = layouts[ i ]; this.controls[ layout ] = $.makeNeutralElement( "div" ); this.controls[ layout ].style.position = 'absolute'; if ( layout.match( 'left' ) ){ this.controls[ layout ].style.left = '0px'; } if ( layout.match( 'right' ) ){ this.controls[ layout ].style.right = '0px'; } if ( layout.match( 'top' ) ){ this.controls[ layout ].style.top = '0px'; } if ( layout.match( 'bottom' ) ){ this.controls[ layout ].style.bottom = '0px'; } } this.container.appendChild( this.controls.topleft ); this.container.appendChild( this.controls.topright ); this.container.appendChild( this.controls.bottomright ); this.container.appendChild( this.controls.bottomleft ); }; $.ControlDock.prototype = { /** * @function */ addControl: function ( element, anchor ) { var element = $.getElement( element ), div = null; if ( getControlIndex( this, element ) >= 0 ) { return; // they're trying to add a duplicate control } switch ( anchor ) { case $.ControlAnchor.TOP_RIGHT: div = this.controls.topright; element.style.position = "relative"; element.style.marginRight = "4px"; element.style.marginTop = "4px"; break; case $.ControlAnchor.BOTTOM_RIGHT: div = this.controls.bottomright; element.style.position = "relative"; element.style.marginRight = "4px"; element.style.marginBottom = "4px"; break; case $.ControlAnchor.BOTTOM_LEFT: div = this.controls.bottomleft; element.style.position = "relative"; element.style.marginLeft = "4px"; element.style.marginBottom = "4px"; break; case $.ControlAnchor.TOP_LEFT: div = this.controls.topleft; element.style.position = "relative"; element.style.marginLeft = "4px"; element.style.marginTop = "4px"; break; case $.ControlAnchor.NONE: default: div = this.container; element.style.margin = "0px"; element.style.padding = "0px"; break; } this.controls.push( new $.Control( element, anchor, div ) ); element.style.display = "inline-block"; }, /** * @function * @return {OpenSeadragon.Viewer} Chainable. */ removeControl: function ( element ) { var element = $.getElement( element ), i = getControlIndex( this, element ); if ( i >= 0 ) { this.controls[ i ].destroy(); this.controls.splice( i, 1 ); } return this; }, /** * @function * @return {OpenSeadragon.Viewer} Chainable. */ clearControls: function () { while ( this.controls.length > 0 ) { this.controls.pop().destroy(); } return this; }, /** * @function * @return {Boolean} */ areControlsEnabled: function () { var i; for ( i = this.controls.length - 1; i >= 0; i-- ) { if ( this.controls[ i ].isVisible() ) { return true; } } return false; }, /** * @function * @return {OpenSeadragon.Viewer} Chainable. */ setControlsEnabled: function( enabled ) { var i; for ( i = this.controls.length - 1; i >= 0; i-- ) { this.controls[ i ].setVisible( enabled ); } return this; } }; /////////////////////////////////////////////////////////////////////////////// // Utility methods /////////////////////////////////////////////////////////////////////////////// function getControlIndex( dock, element ) { var controls = dock.controls, i; for ( i = controls.length - 1; i >= 0; i-- ) { if ( controls[ i ].element == element ) { return i; } } return -1; }; }( OpenSeadragon )); (function( $ ){ // dictionary from hash to private properties var THIS = {}, // We keep a list of viewers so we can 'wake-up' each viewer on // a page after toggling between fullpage modes VIEWERS = {}; /** * * 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. * * 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 * @extends OpenSeadragon.ControlDock * @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 ) { var args = arguments, _this = this, i; //backward compatibility for positional args while prefering more //idiomatic javascript options object as the only argument if( !$.isPlainObject( options ) ){ options = { id: args[ 0 ], xmlPath: args.length > 1 ? args[ 1 ] : undefined, prefixUrl: args.length > 2 ? args[ 2 ] : undefined, controls: args.length > 3 ? args[ 3 ] : undefined, overlays: args.length > 4 ? args[ 4 ] : undefined, overlayControls: args.length > 5 ? args[ 5 ] : undefined }; } //options.config and the general config argument are deprecated //in favor of the more direct specification of optional settings //being pass directly on the options object if ( options.config ){ $.extend( true, options, options.config ); delete options.config; } //Public properties //Allow the options object to override global defaults $.extend( true, this, { //internal state and dom identifiers id: options.id, hash: options.id, //dom nodes element: null, canvas: null, container: null, //TODO: not sure how to best describe these overlays: [], overlayControls:[], //private state properties previousBody: [], //This was originally initialized in the constructor and so could never //have anything in it. now it can because we allow it to be specified //in the options and is only empty by default if not specified. Also //this array was returned from get_controls which I find confusing //since this object has a controls property which is treated in other //functions like clearControls. I'm removing the accessors. customControls: [], //These are originally not part options but declared as members //in initialize. Its still considered idiomatic to put them here source: null, drawer: null, viewport: null, navigator: null, //UI image resources //TODO: rename navImages to uiImages navImages: null, //interface button controls buttons: null, //TODO: this is defunct so safely remove it profiler: null }, $.DEFAULT_SETTINGS, options ); //Private state properties THIS[ this.hash ] = { "fsBoundsDelta": new $.Point( 1, 1 ), "prevContainerSize": null, "lastOpenStartTime": 0, "lastOpenEndTime": 0, "animating": false, "forceRedraw": false, "mouseInside": false, "group": null, // whether we should be continuously zooming "zooming": false, // how much we should be continuously zooming by "zoomFactor": null, "lastZoomTime": null, // did we decide this viewer has a sequence of tile sources "sequenced": false, "sequence": 0 }; //Inherit some behaviors and properties $.EventHandler.call( this ); $.ControlDock.call( this, options ); //Deal with tile sources var initialTileSource, customTileSource; if ( this.xmlPath ){ //Deprecated option. Now it is preferred to use the tileSources option this.tileSources = [ this.xmlPath ]; } if ( this.tileSources ){ //tileSources is a complex option... //It can be a string, object, function, or an array of any of these. // - A String implies a DZI // - An Srray of Objects implies a simple image // - A Function implies a custom tile source callback // - An Array that is not an Array of simple Objects implies a sequence // of tile sources which can be any of the above if( $.isArray( this.tileSources ) ){ if( $.isPlainObject( this.tileSources[ 0 ] ) ){ //This is a non-sequenced legacy tile source initialTileSource = this.tileSources; } else { //Sequenced tile source initialTileSource = this.tileSources[ 0 ]; THIS[ this.hash ].sequenced = true; } } else { initialTileSource = this.tileSources; } this.openTileSource( initialTileSource ); } this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); (function( canvas ){ canvas.width = "100%"; canvas.height = "100%"; canvas.overflow = "hidden"; canvas.position = "absolute"; canvas.top = "0px"; canvas.left = "0px"; }( this.canvas.style )); (function( container ){ container.width = "100%"; container.height = "100%"; container.position = "relative"; container.left = "0px"; container.top = "0px"; container.textAlign = "left"; // needed to protect against }( this.container.style )); this.container.insertBefore( this.canvas, this.container.firstChild ); this.element.appendChild( this.container ); //Used for toggling between fullscreen and default container size //TODO: these can be closure private and shared across Viewer // instances. this.bodyWidth = document.body.style.width; this.bodyHeight = document.body.style.height; this.bodyOverflow = document.body.style.overflow; this.docOverflow = document.documentElement.style.overflow; this.innerTracker = new $.MouseTracker({ element: this.canvas, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, clickHandler: $.delegate( this, onCanvasClick ), dragHandler: $.delegate( this, onCanvasDrag ), releaseHandler: $.delegate( this, onCanvasRelease ), scrollHandler: $.delegate( this, onCanvasScroll ) }).setTracking( this.mouseNavEnabled ? true : false ); // default state this.outerTracker = new $.MouseTracker({ element: this.container, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, enterHandler: $.delegate( this, onContainerEnter ), exitHandler: $.delegate( this, onContainerExit ), releaseHandler: $.delegate( this, onContainerRelease ) }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking ////////////////////////////////////////////////////////////////////////// // Navigation Controls ////////////////////////////////////////////////////////////////////////// var beginZoomingInHandler = $.delegate( this, beginZoomingIn ), endZoomingHandler = $.delegate( this, endZooming ), doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ), beginZoomingOutHandler = $.delegate( this, beginZoomingOut ), doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ), onHomeHandler = $.delegate( this, onHome ), onFullPageHandler = $.delegate( this, onFullPage ), onFocusHandler = $.delegate( this, onFocus ), onBlurHandler = $.delegate( this, onBlur ), onNextHandler = $.delegate( this, onNext ), onPreviousHandler = $.delegate( this, onPrevious ), navImages = this.navImages, buttons = []; if( this.showNavigationControl ){ buttons.push( this.zoomInButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomIn" ), srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ), onPress: beginZoomingInHandler, onRelease: endZoomingHandler, onClick: doSingleZoomInHandler, onEnter: beginZoomingInHandler, onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); buttons.push( this.zoomOutButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomOut" ), srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ), onPress: beginZoomingOutHandler, onRelease: endZoomingHandler, onClick: doSingleZoomOutHandler, onEnter: beginZoomingOutHandler, onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); buttons.push( this.homeButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.Home" ), srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ), onRelease: onHomeHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); buttons.push( this.fullPageButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.FullPage" ), srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ), onRelease: onFullPageHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); if( THIS[ this.hash ].sequenced ){ buttons.push( this.previousButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.PreviousPage" ), srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), onRelease: onPreviousHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); buttons.push( this.nextButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.NextPage" ), srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), onRelease: onNextHandler, onFocus: onFocusHandler, onBlur: onBlurHandler })); this.previousButton.disable(); } this.buttons = new $.ButtonGroup({ buttons: buttons, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold }); this.navControl = this.buttons.element; this.navControl[ $.SIGNAL ] = true; // hack to get our controls to fade this.addHandler( 'open', $.delegate( this, lightUp ) ); if( this.toolbar ){ this.toolbar = new $.ControlDock({ element: this.toolbar }); this.toolbar.addControl( this.navControl, $.ControlAnchor.TOP_LEFT ); }else{ this.addControl( this.navControl, $.ControlAnchor.BOTTOM_RIGHT ); } } for ( i = 0; i < this.customControls.length; i++ ) { this.addControl( this.customControls[ i ].id, this.customControls[ i ].anchor ); } window.setTimeout( function(){ beginControlsAutoHide( _this ); }, 1 ); // initial fade out }; $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, { /** * @function * @name OpenSeadragon.Viewer.prototype.isOpen * @return {Boolean} */ isOpen: function () { return !!this.source; }, /** * 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. * @return {OpenSeadragon.Viewer} Chainable. */ openDzi: function ( dzi ) { var _this = this; $.createFromDZI( dzi, function( source ){ _this.open( source ); }, this.tileHost ); return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.openTileSource * @return {OpenSeadragon.Viewer} Chainable. */ openTileSource: function ( tileSource ) { var _this = this, customTileSource; setTimeout(function(){ if ( $.type( tileSource ) == 'string') { //Standard DZI format _this.openDzi( tileSource ); } else if ( $.isArray( tileSource ) ){ //Legacy image pyramid _this.open( new $.LegacyTileSource( tileSource ) ); } else if ( $.isPlainObject( tileSource ) && $.isFunction( tileSource.getTileUrl ) ){ //Custom tile source customTileSource = new $.TileSource( tileSource.width, tileSource.height, tileSource.tileSize, tileSource.tileOverlap, tileSource.minLevel, tileSource.maxLevel ); customTileSource.getTileUrl = tileSource.getTileUrl; _this.open( customTileSource ); } else { //can assume it's already a tile source implementation _this.open( tileSource ); } }, 1); return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.open * @return {OpenSeadragon.Viewer} Chainable. */ open: function( source ) { var _this = this, overlay, i; if ( this.source ) { this.close( ); } // to ignore earlier opens THIS[ this.hash ].lastOpenStartTime = +new Date(); window.setTimeout( function () { if ( THIS[ _this.hash ].lastOpenStartTime > THIS[ _this.hash ].lastOpenEndTime ) { THIS[ _this.hash ].setMessage( $.getString( "Messages.Loading" ) ); } }, 2000); THIS[ this.hash ].lastOpenEndTime = +new Date(); this.canvas.innerHTML = ""; THIS[ this.hash ].prevContainerSize = $.getElementSize( this.container ); if( source ){ this.source = source; } this.viewport = this.viewport ? this.viewport : new $.Viewport({ containerSize: THIS[ this.hash ].prevContainerSize, contentSize: this.source.dimensions, springStiffness: this.springStiffness, animationTime: this.animationTime, minZoomImageRatio: this.minZoomImageRatio, maxZoomPixelRatio: this.maxZoomPixelRatio, visibilityRatio: this.visibilityRatio, wrapHorizontal: this.wrapHorizontal, wrapVertical: this.wrapVertical }); if( this.preserveVewport ){ this.viewport.resetContentSize( this.source.dimensions ); } this.drawer = new $.Drawer({ source: this.source, viewport: this.viewport, element: this.canvas, overlays: this.overlays, maxImageCacheCount: this.maxImageCacheCount, imageLoaderLimit: this.imageLoaderLimit, minZoomImageRatio: this.minZoomImageRatio, wrapHorizontal: this.wrapHorizontal, wrapVertical: this.wrapVertical, immediateRender: this.immediateRender, blendTime: this.blendTime, alwaysBlend: this.alwaysBlend, minPixelRatio: this.minPixelRatio }); //Instantiate a navigator if configured if ( this.showNavigator && ! this.navigator ){ this.navigator = new $.Navigator({ id: this.navigatorElement, position: this.navigatorPosition, sizeRatio: this.navigatorSizeRatio, height: this.navigatorHeight, width: this.navigatorWidth, tileSources: this.tileSources, tileHost: this.tileHost, prefixUrl: this.prefixUrl, overlays: this.overlays, viewer: this }); } //this.profiler = new $.Profiler(); THIS[ this.hash ].animating = false; THIS[ this.hash ].forceRedraw = true; scheduleUpdate( this, updateMulti ); //Assuming you had programatically created a bunch of overlays //and added them via configuration for ( i = 0; i < this.overlayControls.length; i++ ) { overlay = this.overlayControls[ i ]; if ( overlay.point != null ) { this.drawer.addOverlay( overlay.id, new $.Point( overlay.point.X, overlay.point.Y ), $.OverlayPlacement.TOP_LEFT ); } else { this.drawer.addOverlay( overlay.id, new $.Rect( overlay.rect.Point.X, overlay.rect.Point.Y, overlay.rect.Width, overlay.rect.Height ), overlay.placement ); } } VIEWERS[ this.hash ] = this; this.raiseEvent( "open" ); if( this.navigator ){ this.navigator.open( source ); } return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.close * @return {OpenSeadragon.Viewer} Chainable. */ close: function ( ) { this.source = null; this.drawer = null; this.viewport = this.preserveViewport ? this.viewport : null; //this.profiler = null; this.canvas.innerHTML = ""; VIEWERS[ this.hash ] = null; delete VIEWERS[ this.hash ]; return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled * @return {Boolean} */ isMouseNavEnabled: function () { return this.innerTracker.isTracking(); }, /** * @function * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled * @return {OpenSeadragon.Viewer} Chainable. */ setMouseNavEnabled: function( enabled ){ this.innerTracker.setTracking( enabled ); return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.isDashboardEnabled * @return {Boolean} */ areControlsEnabled: function () { return this.controls.length && this.controls[ i ].isVisibile(); }, /** * @function * @name OpenSeadragon.Viewer.prototype.setDashboardEnabled * @return {OpenSeadragon.Viewer} Chainable. */ setControlsEnabled: function( enabled ) { if( enabled ){ abortControlsAutoHide( this ); } else { beginControlsAutoHide( this ); }; }, /** * @function * @name OpenSeadragon.Viewer.prototype.isFullPage * @return {Boolean} */ isFullPage: function () { return this.container.parentNode == document.body; }, /** * Toggle full page mode. * @function * @name OpenSeadragon.Viewer.prototype.setFullPage * @param {Boolean} fullPage * If true, enter full page mode. If false, exit full page mode. * @return {OpenSeadragon.Viewer} Chainable. */ setFullPage: function( fullPage ) { var body = document.body, bodyStyle = body.style, docStyle = document.documentElement.style, containerStyle = this.container.style, canvasStyle = this.canvas.style, oldBounds, newBounds, viewer, hash, nodes, i; //dont bother modifying the DOM if we are already in full page mode. if ( fullPage == this.isFullPage() ) { return; } if ( fullPage ) { this.bodyOverflow = bodyStyle.overflow; this.docOverflow = docStyle.overflow; bodyStyle.overflow = "hidden"; docStyle.overflow = "hidden"; this.bodyWidth = bodyStyle.width; this.bodyHeight = bodyStyle.height; bodyStyle.width = "100%"; bodyStyle.height = "100%"; canvasStyle.backgroundColor = "black"; canvasStyle.color = "white"; containerStyle.position = "fixed"; //when entering full screen on the ipad it wasnt sufficient to leave //the body intact as only only the top half of the screen would //respond to touch events on the canvas, while the bottom half treated //them as touch events on the document body. Thus we remove and store //the bodies elements and replace them when we leave full screen. this.previousBody = []; nodes = body.childNodes.length; for ( i = 0; i < nodes; i ++ ){ this.previousBody.push( body.childNodes[ 0 ] ); body.removeChild( body.childNodes[ 0 ] ); } //If we've got a toolbar, we need to enable the user to use css to //preserve it in fullpage mode if( this.toolbar && this.toolbar.element ){ //save a reference to the parent so we can put it back //in the long run we need a better strategy this.toolbar.parentNode = this.toolbar.element.parentNode; this.toolbar.nextSibling = this.toolbar.element.nextSibling; body.appendChild( this.toolbar.element ); //Make sure the user has some ability to style the toolbar based //on the mode this.toolbar.element.setAttribute( 'class', this.toolbar.element.className +" fullpage" ); this.toolbar.element.style.position = 'fixed'; this.container.style.top = $.getElementSize( this.toolbar.element ).y + 'px'; } body.appendChild( this.container ); THIS[ this.hash ].prevContainerSize = $.getWindowSize(); // mouse will be inside container now $.delegate( this, onContainerEnter )(); } else { bodyStyle.overflow = this.bodyOverflow; docStyle.overflow = this.docOverflow; bodyStyle.width = this.bodyWidth; bodyStyle.height = this.bodyHeight; canvasStyle.backgroundColor = ""; canvasStyle.color = ""; containerStyle.position = "relative"; containerStyle.zIndex = ""; //If we've got a toolbar, we need to enable the user to use css to //reset it to its original state if( this.toolbar && this.toolbar.element ){ body.removeChild( this.toolbar.element ); //Make sure the user has some ability to style the toolbar based //on the mode this.toolbar.element.setAttribute( 'class', this.toolbar.element.className.replace('fullpage','') ); this.toolbar.element.style.position = 'relative'; this.toolbar.parentNode.insertBefore( this.toolbar.element, this.toolbar.nextSibling ); delete this.toolbar.parentNode; delete this.toolbar.nextSibling; this.container.style.top = 'auto'; } body.removeChild( this.container ); nodes = this.previousBody.length; for ( i = 0; i < nodes; i++ ){ body.appendChild( this.previousBody.shift() ); } this.element.appendChild( this.container ); THIS[ this.hash ].prevContainerSize = $.getElementSize( this.element ); // mouse will likely be outside now $.delegate( this, onContainerExit )(); } if ( this.viewport ) { oldBounds = this.viewport.getBounds(); this.viewport.resize( THIS[ this.hash ].prevContainerSize ); newBounds = this.viewport.getBounds(); if ( fullPage ) { THIS[ this.hash ].fsBoundsDelta = new $.Point( newBounds.width / oldBounds.width, newBounds.height / oldBounds.height ); } else { this.viewport.update(); this.viewport.zoomBy( Math.max( THIS[ this.hash ].fsBoundsDelta.x, THIS[ this.hash ].fsBoundsDelta.y ), null, true ); //Ensures that if multiple viewers are on a page, the viewers that //were hidden during fullpage are 'reopened' for( hash in VIEWERS ){ viewer = VIEWERS[ hash ]; if( viewer !== this && viewer != this.navigator ){ viewer.open( viewer.source ); if( viewer.navigator ){ viewer.navigator.open( viewer.source ); } } } } THIS[ this.hash ].forceRedraw = true; this.raiseEvent( "resize", this ); updateOnce( this ); } return this; }, /** * @function * @name OpenSeadragon.Viewer.prototype.isVisible * @return {Boolean} */ isVisible: function () { return this.container.style.visibility != "hidden"; }, /** * @function * @name OpenSeadragon.Viewer.prototype.setVisible * @return {OpenSeadragon.Viewer} Chainable. */ setVisible: function( visible ){ this.container.style.visibility = visible ? "" : "hidden"; return this; } }); /////////////////////////////////////////////////////////////////////////////// // Schedulers provide the general engine for animation /////////////////////////////////////////////////////////////////////////////// function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ var currentTime, targetTime, deltaTime; if ( THIS[ viewer.hash ].animating ) { return window.setTimeout( function(){ updateFunc( viewer ); }, 1 ); } currentTime = +new Date(); prevUpdateTime = prevUpdateTime ? prevUpdateTime : currentTime; // 60 frames per second is ideal targetTime = prevUpdateTime + 1000 / 60; deltaTime = Math.max( 1, targetTime - currentTime ); return window.setTimeout( function(){ updateFunc( viewer ); }, deltaTime ); }; //provides a sequence in the fade animation function scheduleControlsFade( viewer ) { window.setTimeout( function(){ updateControlsFade( viewer ); }, 20); }; //initiates an animation to hide the controls function beginControlsAutoHide( viewer ) { if ( !viewer.autoHideControls ) { return; } viewer.controlsShouldFade = true; viewer.controlsFadeBeginTime = +new Date() + viewer.controlsFadeDelay; window.setTimeout( function(){ scheduleControlsFade( viewer ); }, viewer.controlsFadeDelay ); }; //determines if fade animation is done or continues the animation function updateControlsFade( viewer ) { var currentTime, deltaTime, opacity, i; if ( viewer.controlsShouldFade ) { currentTime = new Date().getTime(); deltaTime = currentTime - viewer.controlsFadeBeginTime; opacity = 1.0 - deltaTime / viewer.controlsFadeLength; opacity = Math.min( 1.0, opacity ); opacity = Math.max( 0.0, opacity ); for ( i = viewer.controls.length - 1; i >= 0; i--) { viewer.controls[ i ].setOpacity( opacity ); } if ( opacity > 0 ) { // fade again scheduleControlsFade( viewer ); } } }; //stop the fade animation on the controls and show them function abortControlsAutoHide( viewer ) { var i; viewer.controlsShouldFade = false; for ( i = viewer.controls.length - 1; i >= 0; i-- ) { viewer.controls[ i ].setOpacity( 1.0 ); } }; /////////////////////////////////////////////////////////////////////////////// // Default view event handlers. /////////////////////////////////////////////////////////////////////////////// function onFocus(){ abortControlsAutoHide( this ); }; function onBlur(){ beginControlsAutoHide( this ); }; function onCanvasClick( tracker, position, quick, shift ) { var zoomPreClick, factor; if ( this.viewport && quick ) { // ignore clicks where mouse moved zoomPerClick = this.zoomPerClick; factor = shift ? 1.0 / zoomPerClick : zoomPerClick; this.viewport.zoomBy( factor, this.viewport.pointFromPixel( position, true ) ); this.viewport.applyConstraints(); } }; function onCanvasDrag( tracker, position, delta, shift ) { if ( this.viewport ) { if( !this.panHorizontal ){ delta.x = 0; } if( !this.panVertical ){ delta.y = 0; } this.viewport.panBy( this.viewport.deltaPointsFromPixels( delta.negate() ) ); } }; function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { if ( insideElementPress && this.viewport ) { this.viewport.applyConstraints(); } }; function onCanvasScroll( tracker, position, scroll, shift ) { var factor; if ( this.viewport ) { factor = Math.pow( this.zoomPerScroll, scroll ); this.viewport.zoomBy( factor, this.viewport.pointFromPixel( position, true ) ); this.viewport.applyConstraints(); } //cancels event return false; }; function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) { if ( !buttonDownElement ) { THIS[ this.hash ].mouseInside = false; if ( !THIS[ this.hash ].animating ) { beginControlsAutoHide( this ); } } }; function onContainerRelease( tracker, position, insideElementPress, insideElementRelease ) { if ( !insideElementRelease ) { THIS[ this.hash ].mouseInside = false; if ( !THIS[ this.hash ].animating ) { beginControlsAutoHide( this ); } } }; function onContainerEnter( tracker, position, buttonDownElement, buttonDownAny ) { THIS[ this.hash ].mouseInside = true; abortControlsAutoHide( this ); }; /////////////////////////////////////////////////////////////////////////////// // Page update routines ( aka Views - for future reference ) /////////////////////////////////////////////////////////////////////////////// function updateMulti( viewer ) { var beginTime; if ( !viewer.source ) { return; } beginTime = +new Date(); updateOnce( viewer ); scheduleUpdate( viewer, arguments.callee, beginTime ); }; function updateOnce( viewer ) { var containerSize, animated; if ( !viewer.source ) { return; } //viewer.profiler.beginUpdate(); containerSize = $.getElementSize( viewer.container ); if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { // maintain image position viewer.viewport.resize( containerSize, true ); THIS[ viewer.hash ].prevContainerSize = containerSize; viewer.raiseEvent( "resize" ); } animated = viewer.viewport.update(); if ( !THIS[ viewer.hash ].animating && animated ) { viewer.raiseEvent( "animationstart" ); abortControlsAutoHide( viewer ); } if ( animated ) { viewer.drawer.update(); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); } viewer.raiseEvent( "animation" ); } else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) { viewer.drawer.update(); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); } THIS[ viewer.hash ].forceRedraw = false; } if ( THIS[ viewer.hash ].animating && !animated ) { viewer.raiseEvent( "animationfinish" ); if ( !THIS[ viewer.hash ].mouseInside ) { beginControlsAutoHide( viewer ); } } THIS[ viewer.hash ].animating = animated; //viewer.profiler.endUpdate(); }; /////////////////////////////////////////////////////////////////////////////// // Navigation Controls /////////////////////////////////////////////////////////////////////////////// function resolveUrl( prefix, url ) { return prefix ? prefix + url : url; }; function beginZoomingIn() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); }; function beginZoomingOut() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); }; function endZooming() { THIS[ this.hash ].zooming = false; }; function scheduleZoom( viewer ) { window.setTimeout( $.delegate( viewer, doZoom ), 10 ); }; function doZoom() { var currentTime, deltaTime, adjustFactor; if ( THIS[ this.hash ].zooming && this.viewport) { currentTime = +new Date(); deltaTime = currentTime - THIS[ this.hash ].lastZoomTime; adjustedFactor = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 ); this.viewport.zoomBy( adjustedFactor ); this.viewport.applyConstraints(); THIS[ this.hash ].lastZoomTime = currentTime; scheduleZoom( this ); } }; function doSingleZoomIn() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; this.viewport.zoomBy( this.zoomPerClick / 1.0 ); this.viewport.applyConstraints(); } }; function doSingleZoomOut() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; this.viewport.zoomBy( 1.0 / this.zoomPerClick ); this.viewport.applyConstraints(); } }; function lightUp() { this.buttons.emulateEnter(); this.buttons.emulateExit(); }; function onHome() { if ( this.viewport ) { this.viewport.goHome(); } }; function onFullPage() { this.setFullPage( !this.isFullPage() ); // correct for no mouseout event on change this.buttons.emulateExit(); this.fullPageButton.element.focus(); if ( this.viewport ) { this.viewport.applyConstraints(); } }; function onPrevious(){ var previous = THIS[ this.hash ].sequence - 1, preserveVewport = true; if( previous >= 0 ){ THIS[ this.hash ].sequence = previous; if( 0 === previous ){ //Disable previous button this.previousButton.disable(); } if( this.tileSources.length > 0 ){ //Enable next button this.nextButton.enable(); } this.openTileSource( this.tileSources[ previous ] ); } }; function onNext(){ var next = THIS[ this.hash ].sequence + 1, preserveVewport = true; if( this.tileSources.length > next ){ THIS[ this.hash ].sequence = next; if( ( this.tileSources.length - 1 ) === next ){ //Disable next button this.nextButton.disable(); } if( next > 0 ){ //Enable previous button this.previousButton.enable(); } this.openTileSource( this.tileSources[ next ] ); } }; }( OpenSeadragon )); (function( $ ){ /** * @param {Object} options * @param {String} options.viewerId */ $.Navigator = function( options ){ var _this = this, viewer = options.viewer, viewerSize = $.getElementSize( viewer.element ); //We may need to create a new element and id if they did not //provide the id for the existing element if( !options.id ){ options.id = 'navigator-' + (+new Date()); this.element = $.makeNeutralElement( "div" ); this.element.id = options.id; this.element.className = 'navigator'; } options = $.extend( true, { navigatorSizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio }, options, { element: this.element, //These need to be overridden to prevent recursion since //the navigator is a viewer and a viewer has a navigator showNavigator: false, mouseNavEnabled: false, showNavigationControl: false }); options.minPixelRatio = Math.min( options.navigatorSizeRatio * $.DEFAULT_SETTINGS.minPixelRatio, $.DEFAULT_SETTINGS.minPixelRatio ); (function( style ){ style.marginTop = '0px'; style.marginRight = '0px'; style.marginBottom = '0px'; style.marginLeft = '0px'; style.border = '2px solid #555'; style.background = '#000'; style.opacity = 0.8; style.overflow = 'hidden'; }( this.element.style )); this.displayRegion = $.makeNeutralElement( "textarea" ); this.displayRegion.id = this.element.id + '-displayregion'; this.displayRegion.className = 'displayregion'; (function( style ){ style.position = 'relative'; style.top = '0px'; style.left = '0px'; style.fontSize = '0px'; style.border = '2px solid #900'; //TODO: IE doesnt like this property being set try{ style.outline = '2px auto #900'; }catch(e){/*ignore*/} style.background = 'transparent'; style.float = 'left'; //Webkit style.cssFloat = 'left'; //Firefox style.styleFloat = 'left'; //IE style.zIndex = 999999999; style.cursor = 'default'; }( this.displayRegion.style )); this.element.innerTracker = new $.MouseTracker({ element: this.element, scrollHandler: function(){ //dont scroll the page up and down if the user is scrolling //in the navigator return false; } }).setTracking( true ); this.displayRegion.innerTracker = new $.MouseTracker({ element: this.displayRegion, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, clickHandler: $.delegate( this, onCanvasClick ), dragHandler: $.delegate( this, onCanvasDrag ), releaseHandler: $.delegate( this, onCanvasRelease ), scrollHandler: $.delegate( this, onCanvasScroll ), focusHandler: function(){ var point = $.getElementPosition( _this.viewer.element ); window.scrollTo( 0, point.y ); _this.viewer.setControlsEnabled( true ); (function( style ){ style.border = '2px solid #437AB2'; style.outline = '2px auto #437AB2'; }( this.element.style )); }, blurHandler: function(){ _this.viewer.setControlsEnabled( false ); (function( style ){ style.border = '2px solid #900'; style.outline = '2px auto #900'; }( this.element.style )); }, keyHandler: function(tracker, keyCode, shiftKey){ //console.log( keyCode ); switch( keyCode ){ case 61://=|+ _this.viewer.viewport.zoomBy(1.1); _this.viewer.viewport.applyConstraints(); return false; case 45://-|_ _this.viewer.viewport.zoomBy(0.9); _this.viewer.viewport.applyConstraints(); return false; case 48://0|) _this.viewer.viewport.goHome(); _this.viewer.viewport.applyConstraints(); return false; case 119://w case 87://W case 38://up arrow shiftKey ? _this.viewer.viewport.zoomBy(1.1): _this.viewer.viewport.panBy(new $.Point(0, -0.05)); _this.viewer.viewport.applyConstraints(); return false; case 115://s case 83://S case 40://down arrow shiftKey ? _this.viewer.viewport.zoomBy(0.9): _this.viewer.viewport.panBy(new $.Point(0, 0.05)); _this.viewer.viewport.applyConstraints(); return false; case 97://a case 37://left arrow _this.viewer.viewport.panBy(new $.Point(-0.05, 0)); _this.viewer.viewport.applyConstraints(); return false; case 100://d case 39://right arrow _this.viewer.viewport.panBy(new $.Point(0.05, 0)); _this.viewer.viewport.applyConstraints(); return false; default: //console.log( 'navigator keycode %s', keyCode ); return true; } } }).setTracking( true ); // default state /*this.displayRegion.outerTracker = new $.MouseTracker({ element: this.container, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, enterHandler: $.delegate( this, onContainerEnter ), exitHandler: $.delegate( this, onContainerExit ), releaseHandler: $.delegate( this, onContainerRelease ) }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/ this.element.appendChild( this.displayRegion ); viewer.addControl( this.element, $.ControlAnchor.TOP_RIGHT ); if( options.width && options.height ){ this.element.style.width = options.width + 'px'; this.element.style.height = options.height + 'px'; } else { this.element.style.width = ( viewerSize.x * options.navigatorSizeRatio ) + 'px'; this.element.style.height = ( viewerSize.y * options.navigatorSizeRatio ) + 'px'; } $.Viewer.apply( this, [ options ] ); }; $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, { update: function( viewport ){ var bounds, topleft, bottomright; if( viewport && this.viewport ){ bounds = viewport.getBounds( true ); topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ); bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() ); //update style for navigator-box (function(style){ style.top = topleft.y + 'px'; style.left = topleft.x + 'px'; style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; }( this.displayRegion.style )); } } }); function onCanvasClick( tracker, position, quick, shift ) { this.displayRegion.focus(); }; function onCanvasDrag( tracker, position, delta, shift ) { if ( this.viewer.viewport ) { if( !this.panHorizontal ){ delta.x = 0; } if( !this.panVertical ){ delta.y = 0; } this.viewer.viewport.panBy( this.viewport.deltaPointsFromPixels( delta ) ); } }; function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { if ( insideElementPress && this.viewer.viewport ) { this.viewer.viewport.applyConstraints(); } }; function onCanvasScroll( tracker, position, scroll, shift ) { var factor; if ( this.viewer.viewport ) { factor = Math.pow( this.zoomPerScroll, scroll ); this.viewer.viewport.zoomBy( factor, //this.viewport.pointFromPixel( position, true ) this.viewport.getCenter() ); this.viewer.viewport.applyConstraints(); } //cancels event return false; }; }( OpenSeadragon )); (function( $ ){ //TODO: I guess this is where the i18n needs to be reimplemented. I'll look // into existing patterns for i18n in javascript but i think that mimicking // pythons gettext might be a reasonable approach. var I18N = { Errors: { Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + "Please try using IE 7 or Firefox 3.\n", Dzc: "Sorry, we don't support Deep Zoom Collections!", Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", Empty: "You asked us to open nothing, so we did just that.", ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", Security: "It looks like a security restriction stopped us from " + "loading this Deep Zoom Image.", Status: "This space unintentionally left blank ({0} {1}).", Unknown: "Whoops, something inexplicably went wrong. Sorry!" }, Messages: { Loading: "Loading..." }, Tooltips: { FullPage: "Toggle full page", Home: "Go home", ZoomIn: "Zoom in", ZoomOut: "Zoom out", NextPage: "Next page", PreviousPage: "Previous page" } }; $.extend( $, { /** * @function * @name OpenSeadragon.getString * @param {String} property */ getString: function( prop ) { var props = prop.split('.'), string = null, args = arguments, i; for ( i = 0; i < props.length; i++ ) { // in case not a subproperty string = I18N[ props[ i ] ] || {}; } if ( typeof( string ) != "string" ) { string = ""; } return string.replace(/\{\d+\}/g, function(capture) { var i = parseInt( capture.match( /\d+/ ) ) + 1; return i < args.length ? args[ i ] : ""; }); }, /** * @function * @name OpenSeadragon.setString * @param {String} property * @param {*} value */ setString: function( prop, value ) { var props = prop.split('.'), container = $.Strings, i; for ( i = 0; i < props.length - 1; i++ ) { if ( !container[ props[ i ] ] ) { container[ props[ i ] ] = {}; } container = container[ props[ i ] ]; } container[ props[ i ] ] = value; } }); }( OpenSeadragon )); (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; this.y = typeof ( y ) == "number" ? y : 0; }; $.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, this.y + point.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 */ minus: function( point ) { return new $.Point( this.x - point.x, this.y - point.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 */ times: function( factor ) { return new $.Point( this.x * factor, this.y * factor ); }, /** * 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, this.y / factor ); }, /** * 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 ) + Math.pow( this.y - point.y, 2 ) ); }, /** * 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 ) && ( this.x === point.x ) && ( this.y === point.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 */ toString: function() { return "(" + this.x + "," + this.y + ")"; } }; }( OpenSeadragon )); (function( $ ){ /** * @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; this.dimensions = new $.Point( width, height ); this.tileSize = tileSize ? tileSize : 0; this.tileOverlap = tileOverlap ? tileOverlap : 0; this.minLevel = minLevel ? minLevel : 0; this.maxLevel = maxLevel ? maxLevel : Math.ceil( Math.log( Math.max( width, height ) ) / Math.log( 2 ) ); }; $.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 ), y = Math.ceil( scale * this.dimensions.y / this.tileSize ); return new $.Point( x, y ); }, /** * @function * @param {Number} level */ getPixelRatio: function( level ) { var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ), rx = 1.0 / imageSizeScaled.x, ry = 1.0 / imageSizeScaled.y; return new $.Point(rx, ry); }, /** * @function * @param {Number} level * @param {OpenSeadragon.Point} point */ getTileAtPoint: function( level, point ) { var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ), tx = Math.floor( pixel.x / this.tileSize ), ty = Math.floor( pixel.y / this.tileSize ); return new $.Point( tx, ty ); }, /** * @function * @param {Number} level * @param {Number} x * @param {Number} y */ getTileBounds: function( level, x, y ) { var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ), px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap, py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap, sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap, sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap, scale = 1.0 / dimensionsScaled.x; sx = Math.min( sx, dimensionsScaled.x - px ); sy = Math.min( sy, dimensionsScaled.y - py ); return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); }, /** * This method is not implemented by this class other than to throw an Error * announcing you have to implement it. Because of the variety of tile * server technologies, and various specifications for building image * pyramids, this method is here to allow easy integration. * @function * @param {Number} level * @param {Number} x * @param {Number} y * @throws {Error} */ getTileUrl: function( level, x, y ) { throw new Error( "Method not implemented." ); }, /** * @function * @param {Number} level * @param {Number} x * @param {Number} y */ tileExists: function( level, x, y ) { var numTiles = this.getNumTiles( level ); return level >= this.minLevel && level <= this.maxLevel && x >= 0 && y >= 0 && x < numTiles.x && y < numTiles.y; } }; }( OpenSeadragon )); (function( $ ){ /** * @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, level; $.TileSource.call( this, width, height, tileSize, tileOverlap, null, null ); this._levelRects = {}; this.tilesUrl = tilesUrl; this.fileFormat = fileFormat; this.displayRects = displayRects; if ( this.displayRects ) { for ( i = this.displayRects.length - 1; i >= 0; i-- ) { rect = this.displayRects[ i ]; for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) { if ( !this._levelRects[ level ] ) { this._levelRects[ level ] = []; } this._levelRects[ level ].push( rect ); } } } }; $.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, scale, xMin, yMin, xMax, yMax, i; if ( !rects || !rects.length ) { return true; } for ( i = rects.length - 1; i >= 0; i-- ) { rect = rects[ i ]; if ( level < rect.minLevel || level > rect.maxLevel ) { continue; } scale = this.getLevelScale( level ); xMin = rect.x * scale; yMin = rect.y * scale; xMax = xMin + rect.width * scale; yMax = yMin + rect.height * scale; xMin = Math.floor( xMin / this.tileSize ); yMin = Math.floor( yMin / this.tileSize ); xMax = Math.ceil( xMax / this.tileSize ); yMax = Math.ceil( yMax / this.tileSize ); if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) { return true; } } return false; } }); }( OpenSeadragon )); (function( $ ){ /** * The LegacyTileSource allows simple, traditional image pyramids to be loaded * into an OpenSeadragon Viewer. Basically, this translates to the historically * common practice of starting with a 'master' image, maybe a tiff for example, * and generating a set of 'service' images like one or more thumbnails, a medium * resolution image and a high resolution image in standard web formats like * png or jpg. * @class * @param {Array} files An array of file descriptions, each is an object with * a 'url', a 'width', and a 'height'. Overriding classes can expect more * properties but these properties are sufficient for this implementation. * Additionally, the files are required to be listed in order from * smallest to largest. * @property {Number} aspectRatio * @property {Number} dimensions * @property {Number} tileSize * @property {Number} tileOverlap * @property {Number} minLevel * @property {Number} maxLevel * @property {Array} files */ $.LegacyTileSource = function( files ) { var width = files[ files.length - 1 ].width, height = files[ files.length - 1 ].height; $.TileSource.apply( this, [ width, height, Math.max( height, width ), //tileSize 0, //overlap 0, //mimLevel files.length - 1 //maxLevel ] ); this.files = files; }; $.LegacyTileSource.prototype = { /** * @function * @param {Number} level */ getLevelScale: function( level ) { var levelScale = NaN; if ( level >= this.minLevel && level <= this.maxLevel ){ levelScale = this.files[ level ].width / this.files[ this.maxLevel ].width; } return levelScale; }, /** * @function * @param {Number} level */ getNumTiles: function( level ) { var scale = this.getLevelScale( level ); if ( scale ){ return new $.Point( 1, 1 ); } else { return new $.Point( 0, 0 ); } }, /** * @function * @param {Number} level */ getPixelRatio: function( level ) { var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ), rx = 1.0 / imageSizeScaled.x, ry = 1.0 / imageSizeScaled.y; return new $.Point(rx, ry); }, /** * @function * @param {Number} level * @param {OpenSeadragon.Point} point */ getTileAtPoint: function( level, point ) { return new $.Point( 0, 0 ); }, /** * @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.files[ level ].width, py = ( y === 0 ) ? 0 : this.files[ level ].height, sx = this.files[ level ].width, sy = this.files[ level ].height, scale = 1.0 / ( this.width >= this.height ? dimensionsScaled.y : dimensionsScaled.x ); sx = Math.min( sx, dimensionsScaled.x - px ); sy = Math.min( sy, dimensionsScaled.y - py ); return new $.Rect( px * scale, py * scale, sx * scale, sy * scale ); }, /** * 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 ) { var url = null; if( level >= this.minLevel && level <= this.maxLevel ){ url = this.files[ level ].url; } return url; }, /** * @function * @param {Number} level * @param {Number} x * @param {Number} y */ tileExists: function( level, x, y ) { var numTiles = this.getNumTiles( level ); return level >= this.minLevel && level <= this.maxLevel && x >= 0 && y >= 0 && x < numTiles.x && y < numTiles.y; } }; }( OpenSeadragon )); (function( $ ){ /** * An enumeration of button states including, REST, GROUP, HOVER, and DOWN * @static */ $.ButtonState = { REST: 0, GROUP: 1, HOVER: 2, DOWN: 3 }; /** * Manages events, hover states for individual buttons, tool-tips, as well * 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 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. DEPRECATED. * @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 ) { var _this = this; $.EventHandler.call( this ); $.extend( true, this, { tooltip: null, srcRest: null, srcGroup: null, srcHover: null, srcDown: null, clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold, // begin fading immediately fadeDelay: 0, // fade over a period of 2 seconds fadeLength: 2000, onPress: null, onRelease: null, onClick: null, onEnter: null, onExit: null, onFocus: null, onBlur: null }, options ); //TODO: make button elements accessible by making them a-tags // maybe even consider basing them on the element and adding // methods jquery-style. this.element = options.element || $.makeNeutralElement( "button" ); this.element.href = '#'; this.addHandler( "onPress", this.onPress ); this.addHandler( "onRelease", this.onRelease ); this.addHandler( "onClick", this.onClick ); this.addHandler( "onEnter", this.onEnter ); this.addHandler( "onExit", this.onExit ); this.addHandler( "onFocus", this.onFocus ); this.addHandler( "onBlur", this.onBlur ); this.currentState = $.ButtonState.GROUP; this.imgRest = $.makeTransparentImage( this.srcRest ); this.imgGroup = $.makeTransparentImage( this.srcGroup ); this.imgHover = $.makeTransparentImage( this.srcHover ); this.imgDown = $.makeTransparentImage( this.srcDown ); this.fadeBeginTime = null; this.shouldFade = false; this.element.style.display = "inline-block"; this.element.style.position = "relative"; this.element.title = this.tooltip; this.element.appendChild( this.imgRest ); this.element.appendChild( this.imgGroup ); this.element.appendChild( this.imgHover ); this.element.appendChild( this.imgDown ); this.imgGroup.style.position = this.imgHover.style.position = this.imgDown.style.position = "absolute"; this.imgGroup.style.top = this.imgHover.style.top = this.imgDown.style.top = "0px"; this.imgGroup.style.left = this.imgHover.style.left = this.imgDown.style.left = "0px"; this.imgHover.style.visibility = this.imgDown.style.visibility = "hidden"; if ( $.Browser.vendor == $.BROWSERS.FIREFOX && $.Browser.version < 3 ){ this.imgGroup.style.top = this.imgHover.style.top = this.imgDown.style.top = ""; } this.tracker = new $.MouseTracker({ element: this.element, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, enterHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { if ( buttonDownElement ) { inTo( _this, $.ButtonState.DOWN ); _this.raiseEvent( "onEnter", _this ); } else if ( !buttonDownAny ) { inTo( _this, $.ButtonState.HOVER ); } }, focusHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { this.enterHandler( tracker, position, buttonDownElement, buttonDownAny ); _this.raiseEvent( "onFocus", _this ); }, exitHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { outTo( _this, $.ButtonState.GROUP ); if ( buttonDownElement ) { _this.raiseEvent( "onExit", _this ); } }, blurHandler: function( tracker, position, buttonDownElement, buttonDownAny ) { this.exitHandler( tracker, position, buttonDownElement, buttonDownAny ); _this.raiseEvent( "onBlur", _this ); }, pressHandler: function( tracker, position ) { inTo( _this, $.ButtonState.DOWN ); _this.raiseEvent( "onPress", _this ); }, releaseHandler: function( tracker, position, insideElementPress, insideElementRelease ) { if ( insideElementPress && insideElementRelease ) { outTo( _this, $.ButtonState.HOVER ); _this.raiseEvent( "onRelease", _this ); } else if ( insideElementPress ) { outTo( _this, $.ButtonState.GROUP ); } else { inTo( _this, $.ButtonState.HOVER ); } }, clickHandler: function( tracker, position, quick, shift ) { if ( quick ) { _this.raiseEvent("onClick", _this); } }, keyHandler: function( tracker, key ){ //console.log( "%s : handling key %s!", _this.tooltip, key); if( 13 === key ){ _this.raiseEvent( "onClick", _this ); _this.raiseEvent( "onRelease", _this ); return false; } return true; } }).setTracking( true ); outTo( this, $.ButtonState.REST ); }; $.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 ); }, disable: function(){ this.notifyGroupExit(); this.element.disabled = true; $.setElementOpacity( this.element, 0.2, true ); }, enable: function(){ this.element.disabled = false; $.setElementOpacity( this.element, 1.0, true ); this.notifyGroupEnter(); } }); function scheduleFade( button ) { window.setTimeout(function(){ updateFade( button ); }, 20 ); }; function updateFade( button ) { var currentTime, deltaTime, opacity; if ( button.shouldFade ) { currentTime = +new Date(); deltaTime = currentTime - button.fadeBeginTime; opacity = 1.0 - deltaTime / button.fadeLength; opacity = Math.min( 1.0, opacity ); opacity = Math.max( 0.0, opacity ); $.setElementOpacity( button.imgGroup, opacity, true ); if ( opacity > 0 ) { // fade again scheduleFade( button ); } } }; function beginFading( button ) { button.shouldFade = true; button.fadeBeginTime = +new Date() + button.fadeDelay; window.setTimeout( function(){ scheduleFade( button ); }, button.fadeDelay ); }; function stopFading( button ) { button.shouldFade = false; $.setElementOpacity( button.imgGroup, 1.0, true ); }; function inTo( button, newState ) { if( button.element.disabled ){ return; } if ( newState >= $.ButtonState.GROUP && button.currentState == $.ButtonState.REST ) { stopFading( button ); button.currentState = $.ButtonState.GROUP; } if ( newState >= $.ButtonState.HOVER && button.currentState == $.ButtonState.GROUP ) { button.imgHover.style.visibility = ""; button.currentState = $.ButtonState.HOVER; } if ( newState >= $.ButtonState.DOWN && button.currentState == $.ButtonState.HOVER ) { button.imgDown.style.visibility = ""; button.currentState = $.ButtonState.DOWN; } }; function outTo( button, newState ) { if( button.element.disabled ){ return; } if ( newState <= $.ButtonState.HOVER && button.currentState == $.ButtonState.DOWN ) { button.imgDown.style.visibility = "hidden"; button.currentState = $.ButtonState.HOVER; } if ( newState <= $.ButtonState.GROUP && button.currentState == $.ButtonState.HOVER ) { button.imgHover.style.visibility = "hidden"; button.currentState = $.ButtonState.GROUP; } if ( newState <= $.ButtonState.REST && button.currentState == $.ButtonState.GROUP ) { beginFading( button ); button.currentState = $.ButtonState.REST; } }; }( OpenSeadragon )); (function( $ ){ /** * Manages events on groups of buttons. * @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 ) { $.extend( true, this, { buttons: [], clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold, clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold }, options ); // copy the botton elements var buttons = this.buttons.concat([]), _this = this, i; this.element = options.group || $.makeNeutralElement( "fieldgroup" ); this.label = $.makeNeutralElement( "label" ); //TODO: support labels for ButtonGroups //this.label.innerHTML = "test"; this.element.style.display = "inline-block"; this.element.appendChild( this.label ); for ( i = 0; i < buttons.length; i++ ) { this.element.appendChild( buttons[ i ].element ); } this.tracker = new $.MouseTracker({ element: this.element, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, enterHandler: function() { var i; for ( i = 0; i < _this.buttons.length; i++ ) { _this.buttons[ i ].notifyGroupEnter(); } }, exitHandler: function() { var i, buttonDownElement = arguments.length > 2 ? arguments[ 2 ] : null; if ( !buttonDownElement ) { for ( i = 0; i < _this.buttons.length; i++ ) { _this.buttons[ i ].notifyGroupExit(); } } }, releaseHandler: function() { var i, insideElementRelease = arguments.length > 3 ? arguments[ 3 ] : null; if ( !insideElementRelease ) { for ( i = 0; i < _this.buttons.length; i++ ) { _this.buttons[ i ].notifyGroupExit(); } } } }).setTracking( true ); }; $.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.enterHandler(); }, /** * 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.exitHandler(); } }; }( OpenSeadragon )); (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; this.y = typeof ( y ) == "number" ? y : 0; this.width = typeof ( width ) == "number" ? width : 0; this.height = typeof ( height ) == "number" ? height : 0; }; $.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, this.y + this.height ); }, /** * 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, this.y + this.height / 2.0 ); }, /** * 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 ) && ( 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 + "," + this.y + "," + this.width + "x" + this.height + "]"; } }; }( 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 ] ); this.minLevel = minLevel; this.maxLevel = maxLevel; } $.extend( $.DisplayRect.prototype, $.Rect.prototype ); }( OpenSeadragon )); (function( $ ){ /** * @class * @param {Object} options - Spring configuration settings. * @param {Number} options.initial - Initial value of spring, default to 0 so * spring is not in motion initally by default. * @param {Number} options.springStiffness - Spring stiffness. * @param {Number} options.animationTime - Animation duration per spring. * * @property {Number} initial - Initial value of spring, default to 0 so * spring is not in motion initally by default. * @property {Number} springStiffness - Spring stiffness. * @property {Number} animationTime - Animation duration per spring. * @property {Object} current * @property {Number} start * @property {Number} target */ $.Spring = function( options ) { var args = arguments; if( typeof( options ) != 'object' ){ //allows backward compatible use of ( initialValue, config ) as //constructor parameters options = { initial: args.length && typeof ( args[ 0 ] ) == "number" ? args[ 0 ] : 0, springStiffness: args.length > 1 ? args[ 1 ].springStiffness : 5.0, animationTime: args.length > 1 ? args[ 1 ].animationTime : 1.5 }; } $.extend( true, this, options); this.current = { value: typeof ( this.initial ) == "number" ? this.initial : 0, time: new Date().getTime() // always work in milliseconds }; this.start = { value: this.current.value, time: this.current.time }; this.target = { value: this.current.value, time: this.current.time }; }; $.Spring.prototype = { /** * @function * @param {Number} target */ resetTo: function( target ) { this.target.value = target; this.target.time = this.current.time; this.start.value = this.target.value; this.start.time = this.target.time; }, /** * @function * @param {Number} target */ springTo: function( target ) { this.start.value = this.current.value; this.start.time = this.current.time; this.target.value = target; this.target.time = this.start.time + 1000 * this.animationTime; }, /** * @function * @param {Number} delta */ shiftBy: function( delta ) { this.start.value += delta; this.target.value += delta; }, /** * @function */ update: function() { this.current.time = new Date().getTime(); this.current.value = (this.current.time >= this.target.time) ? this.target.value : this.start.value + ( this.target.value - this.start.value ) * transform( this.springStiffness, ( this.current.time - this.start.time ) / ( this.target.time - this.start.time ) ); } } /** * @private */ function transform( stiffness, x ) { return ( 1.0 - Math.exp( stiffness * -x ) ) / ( 1.0 - Math.exp( -stiffness ) ); }; }( OpenSeadragon )); (function( $ ){ /** * @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} element The HTML element for this tile * @property {Image} image The Image object for this tile * @property {String} style The alias of this.element.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; this.exists = exists; this.url = url; this.loaded = false; this.loading = false; this.element = null; this.image = null; this.style = null; this.position = null; this.size = null; this.blendStart = null; this.opacity = null; this.distance = null; this.visibility = null; 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 ), size = this.size.apply( Math.ceil ); if ( !this.loaded || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); return; } if ( !this.element ) { this.element = $.makeNeutralElement("img"); this.element.src = this.url; this.style = this.element.style; this.style.position = "absolute"; this.style.msInterpolationMode = "nearest-neighbor"; } if ( this.element.parentNode != container ) { container.appendChild( this.element ); } this.element.style.left = position.x + "px"; this.element.style.top = position.y + "px"; this.element.style.width = size.x + "px"; this.element.style.height = size.y + "px"; $.setElementOpacity( this.element, this.opacity ); }, /** * 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 || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); return; } context.globalAlpha = this.opacity; context.drawImage( this.image, position.x, position.y, size.x, size.y ); }, /** * Removes tile from it's contianer. * @function */ unload: function() { if ( this.element && this.element.parentNode ) { this.element.parentNode.removeChild( this.element ); } this.element = null; this.image = null; this.loaded = false; this.loading = false; } }; }( OpenSeadragon )); (function( $ ){ /** * An enumeration of positions that an overlay may be assigned relative * to the viewport including CENTER, TOP_LEFT (default), TOP, TOP_RIGHT, * RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, and LEFT. * @static */ $.OverlayPlacement = { CENTER: 0, TOP_LEFT: 1, TOP: 2, TOP_RIGHT: 3, RIGHT: 4, BOTTOM_RIGHT: 5, BOTTOM: 6, BOTTOM_LEFT: 7, LEFT: 8 }; /** * An Overlay provides a * @class */ $.Overlay = function( element, location, placement ) { this.element = element; this.scales = location instanceof $.Rect; this.bounds = new $.Rect( location.x, location.y, location.width, location.height ); this.position = new $.Point( location.x, location.y ); this.size = new $.Point( location.width, location.height ); this.style = element.style; // rects are always top-left this.placement = location instanceof $.Point ? placement : $.OverlayPlacement.TOP_LEFT; }; $.Overlay.prototype = { /** * @function * @param {OpenSeadragon.OverlayPlacement} position * @param {OpenSeadragon.Point} size */ adjust: function( position, size ) { switch ( this.placement ) { case $.OverlayPlacement.TOP_LEFT: break; case $.OverlayPlacement.TOP: position.x -= size.x / 2; break; case $.OverlayPlacement.TOP_RIGHT: position.x -= size.x; break; case $.OverlayPlacement.RIGHT: position.x -= size.x; position.y -= size.y / 2; break; case $.OverlayPlacement.BOTTOM_RIGHT: position.x -= size.x; position.y -= size.y; break; case $.OverlayPlacement.BOTTOM: position.x -= size.x / 2; position.y -= size.y; break; case $.OverlayPlacement.BOTTOM_LEFT: position.y -= size.y; break; case $.OverlayPlacement.LEFT: position.y -= size.y / 2; break; case $.OverlayPlacement.CENTER: default: position.x -= size.x / 2; position.y -= size.y / 2; break; } }, /** * @function */ destroy: function() { var element = this.element, style = this.style; if ( element.parentNode ) { element.parentNode.removeChild( element ); } style.top = ""; style.left = ""; style.position = ""; if ( this.scales ) { style.width = ""; style.height = ""; } }, /** * @function * @param {Element} container */ drawHTML: function( container ) { var element = this.element, style = this.style, scales = this.scales, position, size; if ( element.parentNode != container ) { container.appendChild( element ); } if ( !scales ) { this.size = $.getElementSize( element ); } position = this.position; size = this.size; this.adjust( position, size ); position = position.apply( Math.floor ); size = size.apply( Math.ceil ); style.left = position.x + "px"; style.top = position.y + "px"; style.position = "absolute"; if ( scales ) { style.width = size.x + "px"; style.height = size.y + "px"; } }, /** * @function * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location * @param {OpenSeadragon.OverlayPlacement} position */ update: function( location, placement ) { this.scales = location instanceof $.Rect; this.bounds = new $.Rect( location.x, location.y, location.width, location.height ); // rects are always top-left this.placement = location instanceof $.Point ? placement : $.OverlayPlacement.TOP_LEFT; } }; }( OpenSeadragon )); (function( $ ){ var TIMEOUT = 5000, BROWSER = $.Browser.vendor, BROWSER_VERSION = $.Browser.version, SUBPIXEL_RENDERING = ( ( BROWSER == $.BROWSERS.FIREFOX ) || ( BROWSER == $.BROWSERS.OPERA ) || ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) || ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) ) && ( !navigator.appVersion.match( 'Mobile' ) ), USE_CANVAS = $.isFunction( document.createElement( "canvas" ).getContext ) && SUBPIXEL_RENDERING; //console.error( 'USE_CANVAS ' + USE_CANVAS ); /** * @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 - TODO * @property {CanvasContext} context - TODO * @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 - A '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 - Last time for which the drawer was reset. * @property {Boolean} midUpdate - Is the drawer currently updating the viewport? * @property {Boolean} updateAgain - Does the drawer need to update the viewort again? * @property {Element} element - DEPRECATED Alias for container. */ $.Drawer = function( options ) { //backward compatibility for positional args while prefering more //idiomatic javascript options object as the only argument var args = arguments, i; if( !$.isPlainObject( options ) ){ options = { source: args[ 0 ], viewport: args[ 1 ], element: args[ 2 ] }; } $.extend( true, this, { //internal state properties downloading: 0, tilesMatrix: {}, tilesLoaded: [], coverage: {}, lastDrawn: [], lastResetTime: 0, midUpdate: false, updateAgain: true, //internal state / configurable settings overlays: [], //configurable settings maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, immediateRender: $.DEFAULT_SETTINGS.immediateRender, blendTime: $.DEFAULT_SETTINGS.blendTime, alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio }, options ); this.container = $.getElement( this.element ); this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" ); this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null; this.normHeight = this.source.dimensions.y / this.source.dimensions.x; this.element = this.container; 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 ); //create the correct type of overlay by convention if the overlays //are not already OpenSeadragon.Overlays for( i = 0; i < this.overlays.length; i++ ){ if( $.isPlainObject( this.overlays[ i ] ) ){ (function( _this, overlay ){ var link = document.createElement("a"), rect = new $.Rect( overlay.x, overlay.y, overlay.width, overlay.height ), id = Math.floor(Math.random()*10000000); link.href = "#/overlay/"+id; link.id = id; link.className = overlay.className ? overlay.className : "openseadragon-overlay"; _this.overlays[ i ] = new $.Overlay( link, rect ); }( this, this.overlays[ i ] )); } else if ( $.isFunction( this.overlays[ i ] ) ){ } } //this.profiler = new $.Profiler(); }; $.Drawer.prototype = { /** * Adds an html element as an overlay to the current viewport. Useful for * highlighting words or areas of interest on an image or other zoomable * interface. * @method * @param {Element|String} element - A reference to an element or an id for * the element which will overlayed. * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * rectangle which will be overlayed. * @param {OpenSeadragon.OverlayPlacement} placement - The position of the * viewport which the location coordinates will be treated as relative * to. */ addOverlay: function( element, location, placement ) { element = $.getElement( element ); if ( getOverlayIndex( this.overlays, element ) >= 0 ) { // they're trying to add a duplicate overlay return; } this.overlays.push( new $.Overlay( element, location, placement ) ); this.updateAgain = true; }, /** * Updates the overlay represented by the reference to the element or * element id moving it to the new location, relative to the new placement. * @method * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * rectangle which will be overlayed. * @param {OpenSeadragon.OverlayPlacement} placement - The position of the * viewport which the location coordinates will be treated as relative * to. */ updateOverlay: function( element, location, placement ) { var i; element = $.getElement( element ); i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].update( location, placement ); this.updateAgain = true; } }, /** * Removes and overlay identified by the reference element or element id * and schedules and update. * @method * @param {Element|String} element - A reference to the element or an * element id which represent the ovelay content to be removed. */ removeOverlay: function( element ) { var i; element = $.getElement( element ); i = getOverlayIndex( this.overlays, element ); if ( i >= 0 ) { this.overlays[ i ].destroy(); this.overlays.splice( i, 1 ); this.updateAgain = true; } }, /** * Removes all currently configured Overlays from this Drawer and schedules * and update. * @method */ clearOverlays: function() { while ( this.overlays.length > 0 ) { this.overlays.pop().destroy(); this.updateAgain = true; } }, /** * Returns whether the Drawer is scheduled for an update at the * soonest possible opportunity. * @method * @returns {Boolean} - Whether the Drawer is scheduled for an update at the * soonest possible opportunity. */ needsUpdate: function() { return this.updateAgain; }, /** * Returns the total number of tiles that have been loaded by this Drawer. * @method * @returns {Number} - The total number of tiles that have been loaded by * this Drawer. */ numTilesLoaded: function() { return this.tilesLoaded.length; }, /** * Clears all tiles and triggers an update on the next call to * Drawer.prototype.update(). * @method */ reset: function() { clearTiles( this ); this.lastResetTime = +new Date(); this.updateAgain = true; }, /** * Forces the Drawer to update. * @method */ update: function() { //this.profiler.beginUpdate(); this.midUpdate = true; updateViewport( this ); this.midUpdate = false; //this.profiler.endUpdate(); }, /** * Used internally to load images when required. May also be used to * preload a set of images so the browser will have them available in * the local cache to optimize user experience in certain cases. Because * the number of parallel image loads is configurable, if too many images * are currently being loaded, the request will be ignored. Since by * default drawer.imageLoaderLimit is 0, the native browser parallel * image loading policy will be used. * @method * @param {String} src - The url of the image to load. * @param {Function} callback - The function that will be called with the * Image object as the only parameter, whether on 'load' or on 'abort'. * For now this means the callback is expected to distinguish between * error and success conditions by inspecting the Image object. * @return {Boolean} loading - Wheter the request was submitted or ignored * based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit. */ loadImage: function( src, callback ) { var _this = this, loading = false, image, jobid, complete; if ( !this.imageLoaderLimit || this.downloading < this.imageLoaderLimit ) { this.downloading++; image = new Image(); complete = function( imagesrc ){ _this.downloading--; if (typeof ( callback ) == "function") { try { callback( image ); } catch ( e ) { $.console.error( "%s while executing %s callback: %s", e.name, src, e.message, e ); } } }; image.onload = function(){ finishLoadingImage( image, complete, true ); }; image.onabort = image.onerror = function(){ finishLoadingImage( image, complete, false ); }; jobid = window.setTimeout( function(){ finishLoadingImage( image, complete, false, jobid ); }, TIMEOUT ); loading = true; image.src = src; } return loading; } }; /** * @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.minZoomImageRatio ) / Math.log( 2 ) ) ), highestLevel = Math.min( drawer.source.maxLevel, Math.floor( Math.log( zeroRatioC / drawer.minPixelRatio ) / Math.log( 2 ) ) ), renderPixelRatioC, renderPixelRatioT, zeroRatioT, optimalRatio, levelOpacity, levelVisibility; //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.wrapHorizontal && ( viewportBR.x < 0 || viewportTL.x > 1 ) ) { return; } else if ( !drawer.wrapVertical && ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) { return; } //TODO if ( !drawer.wrapHorizontal ) { viewportTL.x = Math.max( viewportTL.x, 0 ); viewportBR.x = Math.min( viewportBR.x, 1 ); } if ( !drawer.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-- ) { //Avoid calculations for draw if we have already drawn this renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints( drawer.source.getPixelRatio( level ), true ).x; if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) || ( level == lowestLevel ) ) { drawLevel = true; haveDrawn = true; } else if ( !haveDrawn ) { continue; } renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints( drawer.source.getPixelRatio( level ), false ).x; zeroRatioT = drawer.viewport.deltaPixelsFromPoints( drawer.source.getPixelRatio( 0 ), false ).x; optimalRatio = drawer.immediateRender ? 1 : zeroRatioT; levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); levelVisibility = optimalRatio / Math.abs( optimalRatio - renderPixelRatioT ); //TODO best = updateLevel( drawer, haveDrawn, level, levelOpacity, levelVisibility, 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, haveDrawn, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ var x, y, tileTL, tileBR, numberOfTiles, viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() ); //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 ); resetCoverage( drawer.coverage, level ); if ( !drawer.wrapHorizontal ) { tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); } if ( !drawer.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, newbest; 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 ) { // the tile is already in the download queue // thanks josh1093 for finally translating this typo } 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 >= drawer.maxImageCacheCount ) { 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.blendTime, deltaTime, opacity; if ( !tile.blendStart ) { tile.blendStart = currentTime; } deltaTime = currentTime - tile.blendStart; opacity = Math.min( 1, deltaTime / blendTimeMillis ); if ( drawer.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 ].element == 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; image.onabort = null; image.onerror = null; if ( jobid ) { window.clearTimeout( jobid ); } window.setTimeout( function() { callback( image.src, successful ? image : null); }, 1 ); }; 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 )); (function( $ ){ /** * @class */ $.Viewport = function( options ) { //backward compatibility for positional args while prefering more //idiomatic javascript options object as the only argument var args = arguments; if( args.length && args[ 0 ] instanceof $.Point ){ options = { containerSize: args[ 0 ], contentSize: args[ 1 ], config: args[ 2 ] }; } //options.config and the general config argument are deprecated //in favor of the more direct specification of optional settings //being pass directly on the options object if ( options.config ){ $.extend( true, options, options.config ); delete options.config; } $.extend( true, this, { //required settings containerSize: null, contentSize: null, //internal state properties zoomPoint: null, //configurable options springStiffness: $.DEFAULT_SETTINGS.springStiffness, animationTime: $.DEFAULT_SETTINGS.animationTime, minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, maxZoomPixelRatio: $.DEFAULT_SETTINGS.maxZoomPixelRatio, visibilityRatio: $.DEFAULT_SETTINGS.visibilityRatio, wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, wrapVertical: $.DEFAULT_SETTINGS.wrapVertical }, options ); this.centerSpringX = new $.Spring({ initial: 0, springStiffness: this.springStiffness, animationTime: this.animationTime }); this.centerSpringY = new $.Spring({ initial: 0, springStiffness: this.springStiffness, animationTime: this.animationTime }); this.zoomSpring = new $.Spring({ initial: 1, springStiffness: this.springStiffness, animationTime: this.animationTime }); this.resetContentSize( this.contentSize ); this.goHome( true ); //this.fitHorizontally( true ); this.update(); }; $.Viewport.prototype = { resetContentSize: function( contentSize ){ this.contentSize = contentSize; this.contentAspectX = this.contentSize.x / this.contentSize.y; this.contentAspectY = this.contentSize.y / this.contentSize.x; this.homeBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX ); this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); }, /** * @function */ getHomeZoom: function() { var aspectFactor = Math.min( this.contentAspectX, this.contentAspectY ) / this.getAspectRatio(); return ( aspectFactor >= 1 ) ? 1 : aspectFactor; }, /** * @function */ getMinZoom: function() { var homeZoom = this.getHomeZoom() zoom = this.minZoomImageRatio * homeZoom; return Math.min( zoom, homeZoom ); }, /** * @function */ getMaxZoom: function() { var zoom = this.contentSize.x * this.maxZoomPixelRatio / this.containerSize.x; return Math.max( zoom, this.getHomeZoom() ); }, /** * @function */ getAspectRatio: function() { return this.containerSize.x / this.containerSize.y; }, /** * @function */ getContainerSize: function() { return new $.Point( this.containerSize.x, this.containerSize.y ); }, /** * @function */ getBounds: function( current ) { var center = this.getCenter( current ), width = 1.0 / this.getZoom( current ), height = width / this.getAspectRatio(); return new $.Rect( center.x - ( width / 2.0 ), center.y - ( height / 2.0 ), width, height ); }, /** * @function */ getCenter: function( current ) { var centerCurrent = new $.Point( this.centerSpringX.current.value, this.centerSpringY.current.value ), centerTarget = new $.Point( this.centerSpringX.target.value, this.centerSpringY.target.value ), oldZoomPixel, zoom, width, height, bounds, newZoomPixel, deltaZoomPixels, deltaZoomPoints; if ( current ) { return centerCurrent; } else if ( !this.zoomPoint ) { return centerTarget; } oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true); zoom = this.getZoom(); width = 1.0 / zoom; height = width / this.getAspectRatio(); bounds = new $.Rect( centerCurrent.x - width / 2.0, centerCurrent.y - height / 2.0, width, height ); newZoomPixel = this.zoomPoint.minus( bounds.getTopLeft() ).times( this.containerSize.x / bounds.width ); deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom ); return centerTarget.plus( deltaZoomPoints ); }, /** * @function */ getZoom: function( current ) { if ( current ) { return this.zoomSpring.current.value; } else { return this.zoomSpring.target.value; } }, /** * @function */ applyConstraints: function( immediately ) { var actualZoom = this.getZoom(), constrainedZoom = Math.max( Math.min( actualZoom, this.getMaxZoom() ), this.getMinZoom() ), bounds, horizontalThreshold, verticalThreshold, left, right, top, bottom, dx = 0, dy = 0; if ( actualZoom != constrainedZoom ) { this.zoomTo( constrainedZoom, this.zoomPoint, immediately ); } bounds = this.getBounds(); horizontalThreshold = this.visibilityRatio * bounds.width; verticalThreshold = this.visibilityRatio * bounds.height; left = bounds.x + bounds.width; right = 1 - bounds.x; top = bounds.y + bounds.height; bottom = this.contentAspectY - bounds.y; if ( this.wrapHorizontal ) { //do nothing } else if ( left < horizontalThreshold ) { dx = horizontalThreshold - left; } else if ( right < horizontalThreshold ) { dx = right - horizontalThreshold; } if ( this.wrapVertical ) { //do nothing } else if ( top < verticalThreshold ) { dy = verticalThreshold - top; } else if ( bottom < verticalThreshold ) { dy = bottom - verticalThreshold; } if ( dx || dy ) { bounds.x += dx; bounds.y += dy; this.fitBounds( bounds, immediately ); } }, /** * @function * @param {Boolean} immediately */ ensureVisible: function( immediately ) { this.applyConstraints( immediately ); }, /** * @function * @param {OpenSeadragon.Rect} bounds * @param {Boolean} immediately */ fitBounds: function( bounds, immediately ) { var aspect = this.getAspectRatio(), center = bounds.getCenter(), newBounds = new $.Rect( bounds.x, bounds.y, bounds.width, bounds.height ), oldBounds, oldZoom, newZoom, referencePoint; if ( newBounds.getAspectRatio() >= aspect ) { newBounds.height = bounds.width / aspect; newBounds.y = center.y - newBounds.height / 2; } else { newBounds.width = bounds.height * aspect; newBounds.x = center.x - newBounds.width / 2; } this.panTo( this.getCenter( true ), true ); this.zoomTo( this.getZoom( true ), null, true ); oldBounds = this.getBounds(); oldZoom = this.getZoom(); newZoom = 1.0 / newBounds.width; if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) { this.panTo( center, immediately ); return; } referencePoint = oldBounds.getTopLeft().times( this.containerSize.x / oldBounds.width ).minus( newBounds.getTopLeft().times( this.containerSize.x / newBounds.width ) ).divide( this.containerSize.x / oldBounds.width - this.containerSize.x / newBounds.width ); this.zoomTo( newZoom, referencePoint, immediately ); }, /** * @function * @param {Boolean} immediately */ goHome: function( immediately ) { return this.fitVertically( immediately ); }, /** * @function * @param {Boolean} immediately */ fitVertically: function( immediately ) { var center = this.getCenter(); if ( this.wrapHorizontal ) { center.x = ( 1 + ( center.x % 1 ) ) % 1; this.centerSpringX.resetTo( center.x ); this.centerSpringX.update(); } if ( this.wrapVertical ) { center.y = ( this.contentAspectY + ( center.y % this.contentAspectY ) ) % this.contentAspectY; this.centerSpringY.resetTo( center.y ); this.centerSpringY.update(); } this.fitBounds( this.homeBounds, immediately ); }, /** * @function * @param {Boolean} immediately */ fitHorizontally: function( immediately ) { var center = this.getCenter(); if ( this.wrapHorizontal ) { center.x = ( this.contentAspectX + ( center.x % this.contentAspectX ) ) % this.contentAspectX; this.centerSpringX.resetTo( center.x ); this.centerSpringX.update(); } if ( this.wrapVertical ) { center.y = ( 1 + ( center.y % 1 ) ) % 1; this.centerSpringY.resetTo( center.y ); this.centerSpringY.update(); } this.fitBounds( this.fitWidthBounds, immediately ); }, /** * @function * @param {OpenSeadragon.Point} delta * @param {Boolean} immediately */ panBy: function( delta, immediately ) { var center = new $.Point( this.centerSpringX.target.value, this.centerSpringY.target.value ); this.panTo( center.plus( delta ), immediately ); }, /** * @function * @param {OpenSeadragon.Point} center * @param {Boolean} immediately */ panTo: function( center, immediately ) { if ( immediately ) { this.centerSpringX.resetTo( center.x ); this.centerSpringY.resetTo( center.y ); } else { this.centerSpringX.springTo( center.x ); this.centerSpringY.springTo( center.y ); } }, /** * @function */ zoomBy: function( factor, refPoint, immediately ) { this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately ); }, /** * @function */ zoomTo: function( zoom, refPoint, immediately ) { if ( immediately ) { this.zoomSpring.resetTo( zoom ); } else { this.zoomSpring.springTo( zoom ); } this.zoomPoint = refPoint instanceof $.Point ? refPoint : null; }, /** * @function */ resize: function( newContainerSize, maintain ) { var oldBounds = this.getBounds(), newBounds = oldBounds, widthDeltaFactor = newContainerSize.x / this.containerSize.x; this.containerSize = new $.Point( newContainerSize.x, newContainerSize.y ); if (maintain) { newBounds.width = oldBounds.width * widthDeltaFactor; newBounds.height = newBounds.width / this.getAspectRatio(); } this.fitBounds( newBounds, true ); }, /** * @function */ update: function() { var oldCenterX = this.centerSpringX.current.value, oldCenterY = this.centerSpringY.current.value, oldZoom = this.zoomSpring.current.value, oldZoomPixel, newZoomPixel, deltaZoomPixels, deltaZoomPoints; if (this.zoomPoint) { oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); } this.zoomSpring.update(); if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) { newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true ); this.centerSpringX.shiftBy( deltaZoomPoints.x ); this.centerSpringY.shiftBy( deltaZoomPoints.y ); } else { this.zoomPoint = null; } this.centerSpringX.update(); this.centerSpringY.update(); return this.centerSpringX.current.value != oldCenterX || this.centerSpringY.current.value != oldCenterY || this.zoomSpring.current.value != oldZoom; }, /** * @function */ deltaPixelsFromPoints: function( deltaPoints, current ) { return deltaPoints.times( this.containerSize.x * this.getZoom( current ) ); }, /** * @function */ deltaPointsFromPixels: function( deltaPixels, current ) { return deltaPixels.divide( this.containerSize.x * this.getZoom( current ) ); }, /** * @function */ pixelFromPoint: function( point, current ) { var bounds = this.getBounds( current ); return point.minus( bounds.getTopLeft() ).times( this.containerSize.x / bounds.width ); }, /** * @function */ pointFromPixel: function( pixel, current ) { var bounds = this.getBounds( current ); return pixel.divide( this.containerSize.x / bounds.width ).plus( bounds.getTopLeft() ); } }; }( OpenSeadragon ));