1 /**
  2  * @version  OpenSeadragon 0.9.50
  3  *
  4  * @fileOverview 
  5  * <h2>
  6  * <strong>
  7  * OpenSeadragon - Javascript Deep Zooming
  8  * </strong>
  9  * </h2> 
 10  * <p>
 11  * OpenSeadragon is provides an html interface for creating 
 12  * deep zoom user interfaces.  The simplest examples include deep 
 13  * zoom for large resolution images, and complex examples include
 14  * zoomable map interfaces driven by SVG files.
 15  * </p>
 16  * 
 17  * @author <br/>(c) 2011, 2012 Christopher Thatcher 
 18  * @author <br/>(c) 2010 OpenSeadragon Team 
 19  * @author <br/>(c) 2010 CodePlex Foundation 
 20  * 
 21  * <p>
 22  * <strong>Original license preserved below: </strong><br/>
 23  * <pre>
 24  * ----------------------------------------------------------------------------
 25  * 
 26  *  License: New BSD License (BSD)
 27  *  Copyright (c) 2010, OpenSeadragon
 28  *  All rights reserved.
 29  * 
 30  *  Redistribution and use in source and binary forms, with or without 
 31  *  modification, are permitted provided that the following conditions are met:
 32  *  
 33  *  * Redistributions of source code must retain the above copyright notice, this 
 34  *    list of conditions and the following disclaimer.
 35  *  
 36  *  * Redistributions in binary form must reproduce the above copyright notice, 
 37  *    this list of conditions and the following disclaimer in the documentation 
 38  *    and/or other materials provided with the distribution.
 39  * 
 40  *  * Neither the name of OpenSeadragon nor the names of its contributors may be 
 41  *    used to endorse or promote products derived from this software without 
 42  *    specific prior written permission.
 43  * 
 44  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 45  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 46  *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 47  *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 48  *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 49  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 50  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 51  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 52  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 53  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 54  *  POSSIBILITY OF SUCH DAMAGE.
 55  * 
 56  * ---------------------------------------------------------------------------
 57  * </pre>
 58  * </p>
 59  * <p>
 60  * <strong> Work done by Chris Thatcher adds an MIT license </strong><br/>
 61  * <pre>
 62  * ----------------------------------------------------------------------------
 63  * (c) Christopher Thatcher 2011, 2012. All rights reserved.
 64  * 
 65  * Licensed with the MIT License
 66  * 
 67  * Permission is hereby granted, free of charge, to any person obtaining a copy
 68  * of this software and associated documentation files (the "Software"), to deal
 69  * in the Software without restriction, including without limitation the rights
 70  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 71  * copies of the Software, and to permit persons to whom the Software is
 72  * furnished to do so, subject to the following conditions:
 73  * 
 74  * The above copyright notice and this permission notice shall be included in
 75  * all copies or substantial portions of the Software.
 76  * 
 77  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 78  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 79  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 80  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 81  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 82  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 83  * THE SOFTWARE.
 84  * ---------------------------------------------------------------------------
 85  * </pre>
 86  * </p>
 87  **/
 88 
 89  /** 
 90   * The root namespace for OpenSeadragon, this function also serves as a single
 91   * point of instantiation for an {@link OpenSeadragon.Viewer}, including all 
 92   * combinations of out-of-the-box configurable features.  All utility methods 
 93   * and classes are defined on or below this namespace. 
 94   *
 95   * @namespace
 96   * @function
 97   * @name OpenSeadragon
 98   * @exports $ as OpenSeadragon
 99   *
100   * @param {Object} options All required and optional settings for instantiating
101   *     a new instance of an OpenSeadragon image viewer. 
102   *
103   * @param {String} options.xmlPath 
104   *     DEPRECATED. A relative path to load a DZI file from the server. 
105   *     Prefer the newer options.tileSources.
106   *
107   * @param {Array|String|Function|Object[]|Array[]|String[]|Function[]} options.tileSources
108   *     As an Array, the tileSource can hold either be all Objects or mixed 
109   *     types of Arrays of Objects, String, Function. When a value is a String, 
110   *     the tileSource is used to create a {@link OpenSeadragon.DziTileSource}.  
111   *     When a value is a Function, the function is used to create a new 
112   *     {@link OpenSeadragon.TileSource} whose abstract method 
113   *     getUrl( level, x, y ) is implemented by the function. Finally, when it 
114   *     is an Array of objects, it is used to create a 
115   *     {@link OpenSeadragon.LegacyTileSource}.
116   *
117   * @param {Boolean} [options.debugMode=true]
118   *     Currently does nothing. TODO: provide an in-screen panel providing event
119   *     detail feedback.
120   *
121   * @param {Number} [options.animationTime=1.5]
122   *     Specifies the animation duration per each {@link OpenSeadragon.Spring}
123   *     which occur when the image is dragged or zoomed.
124   *
125   * @param {Number} [options.blendTime=0.5] 
126   *     Specifies the duration of animation as higher or lower level tiles are
127   *     replacing the existing tile.
128   *
129   * @param {Boolean} [options.alwaysBlend=false]
130   *     Forces the tile to always blend.  By default the tiles skip blending
131   *     when the blendTime is surpassed and the current animation frame would
132   *     not complete the blend.
133   *
134   * @param {Boolean} [options.autoHideControls=true]
135   *     If the user stops interacting with the viewport, fade the navigation 
136   *     controls.  Useful for presentation since the controls are by default
137   *     floated on top of the image the user is viewing.
138   *
139   * @param {Boolean} [options.immediateRender=false]
140   *     Render the best closest level first, ignoring the lowering levels which
141   *     provide the effect of very blurry to sharp. It is recommended to change 
142   *     setting to true for mobile devices.
143   *
144   * @param {Boolean} [options.wrapHorizontal=false]
145   *     Set to true to force the image to wrap horizontally within the viewport.
146   *     Useful for maps or images representing the surface of a sphere or cylinder.
147   *
148   * @param {Boolean} [options.wrapVertical=false]
149   *     Set to true to force the image to wrap vertically within the viewport.
150   *     Useful for maps or images representing the surface of a sphere or cylinder.
151   *
152   * @param {Number} [options.minZoomImageRatio=0.8]
153   *     The minimum percentage ( expressed as a number between 0 and 1 ) of 
154   *     the viewport height or width at which the zoom out will be constrained.
155   *     Setting it to 0, for example will allow you to zoom out infinitly.
156   *
157   * @param {Number} [options.maxZoomPixelRatio=2]
158   *     The maximum ratio to allow a zoom-in to affect the highest level pixel
159   *     ratio. This can be set to Infinity to allow 'infinite' zooming into the
160   *     image though it is less effective visually if the HTML5 Canvas is not 
161   *     availble on the viewing device.
162   *
163   * @param {Number} [options.visibilityRatio=0.5]
164   *     The percentage ( as a number from 0 to 1 ) of the source image which
165   *     must be kept within the viewport.  If the image is dragged beyond that
166   *     limit, it will 'bounce' back until the minimum visibility ration is 
167   *     achieved.  Setting this to 0 and wrapHorizontal ( or wrapVertical ) to
168   *     true will provide the effect of an infinitely scrolling viewport.
169   *
170   * @param {Number} [options.springStiffness=5.0]
171   *
172   * @param {Number} [options.imageLoaderLimit=0]
173   *     The maximum number of image requests to make concurrently.  By default
174   *     it is set to 0 allowing the browser to make the maximum number of
175   *     image requests in parallel as allowed by the browsers policy.
176   *
177   * @param {Number} [options.clickTimeThreshold=200]
178   *     If multiple mouse clicks occurs within less than this number of 
179   *     milliseconds, treat them as a single click.
180   *
181   * @param {Number} [options.clickDistThreshold=5]
182   *     If a mouse or touch drag occurs and the distance to the starting drag
183   *     point is less than this many pixels, ignore the drag event.
184   *
185   * @param {Number} [options.zoomPerClick=2.0]
186   *     The "zoom distance" per mouse click or touch tap.
187   *
188   * @param {Number} [options.zoomPerScroll=1.2]
189   *     The "zoom distance" per mouse scroll or touch pinch.
190   *
191   * @param {Number} [options.zoomPerSecond=2.0]
192   *     The number of seconds to animate a single zoom event over.
193   *
194   * @param {Boolean} [options.showNavigationControl=true]
195   *     Set to false to prevent the appearance of the default navigation controls.
196   *
197   * @param {Number} [options.controlsFadeDelay=2000]
198   *     The number of milliseconds to wait once the user has stopped interacting
199   *     with the interface before begining to fade the controls. Assumes
200   *     showNavigationControl and autoHideControls are both true.
201   *
202   * @param {Number} [options.controlsFadeLength=1500]
203   *     The number of milliseconds to animate the controls fading out.
204   *
205   * @param {Number} [options.maxImageCacheCount=100]
206   *     The max number of images we should keep in memory (per drawer).
207   *
208   * @param {Number} [options.minPixelRatio=0.5]
209   *     The higher the minPixelRatio, the lower the quality of the image that
210   *     is considered sufficient to stop rendering a given zoom level.  For
211   *     example, if you are targeting mobile devices with less bandwith you may 
212   *     try setting this to 1.5 or higher.
213   *
214   * @param {Boolean} [options.mouseNavEnabled=true]
215   *     Is the user able to interact with the image via mouse or touch. Default 
216   *     interactions include draging the image in a plane, and zooming in toward
217   *     and away from the image.
218   *
219   * @param {Boolean} [options.preserveViewport=false]
220   *     If the viewer has been configured with a sequence of tile sources, then
221   *     normally navigating to through each image resets the viewport to 'home'
222   *     position.  If preserveViewport is set to true, then the viewport position
223   *     is preserved when navigating between images in the sequence.
224   *
225   * @param {String} [options.prefixUrl='']
226   *     Appends the prefixUrl to navImages paths, which is very useful
227   *     since the default paths are rarely useful for production
228   *     environments.
229   *
230   * @param {Object} [options.navImages=]
231   *     An object with a property for each button or other built-in navigation
232   *     control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'.
233   *     Each of those in turn provides an image path for each state of the botton
234   *     or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the
235   *     image paths, by default assume there is a folder on the servers root path
236   *     called '/images', eg '/images/zoomin_rest.png'.  If you need to adjust
237   *     these paths, prefer setting the option.prefixUrl rather than overriding 
238   *     every image path directly through this setting.
239   *
240   * @returns {OpenSeadragon.Viewer}
241   */
242 OpenSeadragon = window.OpenSeadragon || function( options ){
243     
244     return new OpenSeadragon.Viewer( options );
245 
246 };
247 
248 (function( $ ){
249     
250 
251     /**
252      * Taken from jquery 1.6.1
253      * [[Class]] -> type pairs
254      * @private
255      */
256     var class2type = {
257         '[object Boolean]':     'boolean',
258         '[object Number]':      'number',
259         '[object String]':      'string',
260         '[object Function]':    'function',
261         '[object Array]':       'array',
262         '[object Date]':        'date',
263         '[object RegExp]':      'regexp',
264         '[object Object]':      'object'
265     },
266     // Save a reference to some core methods
267     toString    = Object.prototype.toString,
268     hasOwn      = Object.prototype.hasOwnProperty,
269     push        = Array.prototype.push,
270     slice       = Array.prototype.slice,
271     trim        = String.prototype.trim,
272     indexOf     = Array.prototype.indexOf;
273 
274 
275     /**
276      * Taken from jQuery 1.6.1
277      * @name $.isFunction
278      * @function
279      * @see <a href='http://www.jquery.com/'>jQuery</a>
280      */
281     $.isFunction = function( obj ) {
282         return $.type(obj) === "function";
283     };
284 
285 
286     /**
287      * Taken from jQuery 1.6.1
288      * @name $.isArray
289      * @function
290      * @see <a href='http://www.jquery.com/'>jQuery</a>
291      */
292     $.isArray = Array.isArray || function( obj ) {
293         return $.type(obj) === "array";
294     };
295 
296 
297     /**
298      * A crude way of determining if an object is a window.
299      * Taken from jQuery 1.6.1
300      * @name $.isWindow
301      * @function
302      * @see <a href='http://www.jquery.com/'>jQuery</a>
303      */
304     $.isWindow = function( obj ) {
305         return obj && typeof obj === "object" && "setInterval" in obj;
306     };
307 
308 
309     /**
310      * Taken from jQuery 1.6.1
311      * @name $.type
312      * @function
313      * @see <a href='http://www.jquery.com/'>jQuery</a>
314      */
315     $.type = function( obj ) {
316         return obj == null ?
317             String( obj ) :
318             class2type[ toString.call(obj) ] || "object";
319     };
320 
321 
322     /**
323      * Taken from jQuery 1.6.1
324      * @name $.isPlainObject
325      * @function
326      * @see <a href='http://www.jquery.com/'>jQuery</a>
327      */
328     $.isPlainObject = function( obj ) {
329         // Must be an Object.
330         // Because of IE, we also have to check the presence of the constructor property.
331         // Make sure that DOM nodes and window objects don't pass through, as well
332         if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) {
333             return false;
334         }
335 
336         // Not own constructor property must be Object
337         if ( obj.constructor &&
338             !hasOwn.call(obj, "constructor") &&
339             !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
340             return false;
341         }
342 
343         // Own properties are enumerated firstly, so to speed up,
344         // if last one is own, then all properties are own.
345 
346         var key;
347         for ( key in obj ) {}
348 
349         return key === undefined || hasOwn.call( obj, key );
350     };
351 
352 
353     /**
354      * Taken from jQuery 1.6.1
355      * @name $.isEmptyObject
356      * @function
357      * @see <a href='http://www.jquery.com/'>jQuery</a>
358      */
359     $.isEmptyObject = function( obj ) {
360         for ( var name in obj ) {
361             return false;
362         }
363         return true;
364     };
365 
366 
367 }( OpenSeadragon ));
368 
369 /**
370  *  This closure defines all static methods available to the OpenSeadragon
371  *  namespace.  Many, if not most, are taked directly from jQuery for use
372  *  to simplify and reduce common programming patterns.  More static methods 
373  *  from jQuery may eventually make their way into this though we are
374  *  attempting to avoid substaintial plagarism or the more explicit dependency
375  *  on jQuery only because OpenSeadragon is a broadly useful code base and
376  *  would be made less broad by requiring jQuery fully.
377  *
378  *  Some static methods have also been refactored from the original OpenSeadragon 
379  *  project.
380  */
381 (function( $ ){
382 
383     /**
384      * Taken from jQuery 1.6.1
385      * @see <a href='http://www.jquery.com/'>jQuery</a>
386      */
387     $.extend = function() {
388         var options, 
389             name, 
390             src, 
391             copy, 
392             copyIsArray, 
393             clone,
394             target  = arguments[ 0 ] || {},
395             length  = arguments.length,
396             deep    = false,
397             i       = 1;
398 
399         // Handle a deep copy situation
400         if ( typeof target === "boolean" ) {
401             deep    = target;
402             target  = arguments[ 1 ] || {};
403             // skip the boolean and the target
404             i = 2;
405         }
406 
407         // Handle case when target is a string or something (possible in deep copy)
408         if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
409             target = {};
410         }
411 
412         // extend jQuery itself if only one argument is passed
413         if ( length === i ) {
414             target = this;
415             --i;
416         }
417 
418         for ( ; i < length; i++ ) {
419             // Only deal with non-null/undefined values
420             if ( ( options = arguments[ i ] ) != null ) {
421                 // Extend the base object
422                 for ( name in options ) {
423                     src = target[ name ];
424                     copy = options[ name ];
425 
426                     // Prevent never-ending loop
427                     if ( target === copy ) {
428                         continue;
429                     }
430 
431                     // Recurse if we're merging plain objects or arrays
432                     if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) {
433                         if ( copyIsArray ) {
434                             copyIsArray = false;
435                             clone = src && OpenSeadragon.isArray( src ) ? src : [];
436 
437                         } else {
438                             clone = src && OpenSeadragon.isPlainObject( src ) ? src : {};
439                         }
440 
441                         // Never move original objects, clone them
442                         target[ name ] = OpenSeadragon.extend( deep, clone, copy );
443 
444                     // Don't bring in undefined values
445                     } else if ( copy !== undefined ) {
446                         target[ name ] = copy;
447                     }
448                 }
449             }
450         }
451 
452         // Return the modified object
453         return target;
454     };
455     
456 
457     $.extend( $, {
458         /**
459          * These are the default values for the optional settings documented
460          * in the {@link OpenSeadragon} constructor detail.
461          * @name $.DEFAULT_SETTINGS
462          * @static
463          */
464         DEFAULT_SETTINGS: {
465             //DATA SOURCE DETAILS
466             xmlPath:                null,
467             tileSources:            null, 
468             tileHost:               null,
469              
470             //INTERFACE FEATURES
471             debugMode:              true,
472             animationTime:          1.5,
473             blendTime:              0.5,
474             alwaysBlend:            false,
475             autoHideControls:       true,
476             immediateRender:        false,
477             wrapHorizontal:         false,
478             wrapVertical:           false,
479             panHorizontal:          true,
480             panVertical:            true,
481             visibilityRatio:        0.5,
482             springStiffness:        5.0,
483             clickTimeThreshold:     200,
484             clickDistThreshold:     5,
485             zoomPerClick:           2.0,
486             zoomPerScroll:          1.2,
487             zoomPerSecond:          2.0,
488             showNavigationControl:  true,
489             showSequenceControl:    true,
490             controlsFadeDelay:      2000,
491             controlsFadeLength:     1500,
492             mouseNavEnabled:        true,
493             showNavigator:          false,
494             navigatorElement:       null,
495             navigatorHeight:        null,
496             navigatorWidth:         null,
497             navigatorPosition:      null,
498             navigatorSizeRatio:     0.25,
499             preserveViewport:       false,
500             
501             //PERFORMANCE SETTINGS
502             minPixelRatio:          0.5,
503             imageLoaderLimit:       0,
504             maxImageCacheCount:     200,
505             minZoomImageRatio:      0.8,
506             maxZoomPixelRatio:      2,
507 
508             //INTERFACE RESOURCE SETTINGS
509             prefixUrl:              null,
510             navImages: {
511                 zoomIn: {
512                     REST:   '/images/zoomin_rest.png',
513                     GROUP:  '/images/zoomin_grouphover.png',
514                     HOVER:  '/images/zoomin_hover.png',
515                     DOWN:   '/images/zoomin_pressed.png'
516                 },
517                 zoomOut: {
518                     REST:   '/images/zoomout_rest.png',
519                     GROUP:  '/images/zoomout_grouphover.png',
520                     HOVER:  '/images/zoomout_hover.png',
521                     DOWN:   '/images/zoomout_pressed.png'
522                 },
523                 home: {
524                     REST:   '/images/home_rest.png',
525                     GROUP:  '/images/home_grouphover.png',
526                     HOVER:  '/images/home_hover.png',
527                     DOWN:   '/images/home_pressed.png'
528                 },
529                 fullpage: {
530                     REST:   '/images/fullpage_rest.png',
531                     GROUP:  '/images/fullpage_grouphover.png',
532                     HOVER:  '/images/fullpage_hover.png',
533                     DOWN:   '/images/fullpage_pressed.png'
534                 },
535                 previous: {
536                     REST:   '/images/previous_rest.png',
537                     GROUP:  '/images/previous_grouphover.png',
538                     HOVER:  '/images/previous_hover.png',
539                     DOWN:   '/images/previous_pressed.png'
540                 },
541                 next: {
542                     REST:   '/images/next_rest.png',
543                     GROUP:  '/images/next_grouphover.png',
544                     HOVER:  '/images/next_hover.png',
545                     DOWN:   '/images/next_pressed.png'
546                 }
547             }
548         },
549 
550 
551         /**
552          * TODO: get rid of this.  I can't see how it's required at all.  Looks
553          *       like an early legacy code artifact.
554          * @static
555          * @ignore
556          */
557         SIGNAL: "----seadragon----",
558 
559 
560         /**
561          * Invokes the the method as if it where a method belonging to the object.
562          * @name $.delegate
563          * @function
564          * @param {Object} object 
565          * @param {Function} method
566          */
567         delegate: function( object, method ) {
568             return function() {
569                 if ( arguments === undefined )
570                     arguments = [];
571                 return method.apply( object, arguments );
572             };
573         },
574         
575         
576         /**
577          * An enumeration of Browser vendors including UNKNOWN, IE, FIREFOX,
578          * SAFARI, CHROME, and OPERA.
579          * @name $.BROWSERS
580          * @static
581          */
582         BROWSERS: {
583             UNKNOWN:    0,
584             IE:         1,
585             FIREFOX:    2,
586             SAFARI:     3,
587             CHROME:     4,
588             OPERA:      5
589         },
590 
591 
592         /**
593          * Returns a DOM Element for the given id or element.
594          * @function
595          * @name OpenSeadragon.getElement
596          * @param {String|Element} element Accepts an id or element.
597          * @returns {Element} The element with the given id, null, or the element itself.
598          */
599         getElement: function( element ) { 
600             if ( typeof ( element ) == "string" ) {
601                 element = document.getElementById( element );
602             }
603             return element;
604         },
605 
606 
607         /**
608          * Determines the position of the upper-left corner of the element.
609          * @function
610          * @name OpenSeadragon.getElementPosition
611          * @param {Element|String} element - the elemenet we want the position for.
612          * @returns {Point} - the position of the upper left corner of the element. 
613          */
614         getElementPosition: function( element ) {
615             var result = new $.Point(),
616                 isFixed,
617                 offsetParent;
618 
619             element      = $.getElement( element );
620             isFixed      = $.getElementStyle( element ).position == "fixed";
621             offsetParent = getOffsetParent( element, isFixed );
622 
623             while ( offsetParent ) {
624 
625                 result.x += element.offsetLeft;
626                 result.y += element.offsetTop;
627 
628                 if ( isFixed ) {
629                     result = result.plus( $.getPageScroll() );
630                 }
631 
632                 element = offsetParent;
633                 isFixed = $.getElementStyle( element ).position == "fixed";
634                 offsetParent = getOffsetParent( element, isFixed );
635             }
636 
637             return result;
638         },
639 
640 
641         /**
642          * Determines the height and width of the given element.
643          * @function
644          * @name OpenSeadragon.getElementSize
645          * @param {Element|String} element
646          * @returns {Point}
647          */
648         getElementSize: function( element ) {
649             element = $.getElement( element );
650 
651             return new $.Point(
652                 element.clientWidth, 
653                 element.clientHeight
654             );
655         },
656 
657 
658         /**
659          * Returns the CSSStyle object for the given element.
660          * @function
661          * @name OpenSeadragon.getElementStyle
662          * @param {Element|String} element
663          * @returns {CSSStyle}
664          */
665         getElementStyle: 
666             document.documentElement.currentStyle ? 
667             function( element ) {
668                 element = $.getElement( element );
669                 return element.currentStyle;
670             } : 
671             function( element ) {
672                 element = $.getElement( element );
673                 return window.getComputedStyle( element, "" );
674             },
675 
676 
677         /**
678          * Gets the latest event, really only useful internally since its 
679          * specific to IE behavior.  TODO: Deprecate this from the api and
680          * use it internally.
681          * @function
682          * @name OpenSeadragon.getEvent
683          * @param {Event} [event]
684          * @returns {Event}
685          */
686         getEvent: function( event ) {
687             if( event ){
688                 $.getEvent = function( event ){
689                     return event;
690                 };
691             } else {
692                 $.getEvent = function( event ){
693                     return window.event;
694                 };
695             }
696             return $.getEvent( event );
697         },
698 
699 
700         /**
701          * Gets the position of the mouse on the screen for a given event.
702          * @function
703          * @name OpenSeadragon.getMousePosition
704          * @param {Event} [event]
705          * @returns {Point}
706          */
707         getMousePosition: function( event ) {
708 
709             if ( typeof( event.pageX ) == "number" ) {
710                 $.getMousePosition = function( event ){
711                     var result = new $.Point();
712 
713                     event = $.getEvent( event );
714                     result.x = event.pageX;
715                     result.y = event.pageY;
716 
717                     return result;
718                 };
719             } else if ( typeof( event.clientX ) == "number" ) {
720                 $.getMousePosition = function( event ){
721                     var result = new $.Point();
722 
723                     event = $.getEvent( event );
724                     result.x = 
725                         event.clientX + 
726                         document.body.scrollLeft + 
727                         document.documentElement.scrollLeft;
728                     result.y = 
729                         event.clientY + 
730                         document.body.scrollTop + 
731                         document.documentElement.scrollTop;
732 
733                     return result;
734                 };
735             } else {
736                 throw new Error(
737                     "Unknown event mouse position, no known technique."
738                 );
739             }
740 
741             return $.getMousePosition( event );
742         },
743 
744 
745         /**
746          * Determines the pages current scroll position.
747          * @function
748          * @name OpenSeadragon.getPageScroll
749          * @returns {Point}
750          */
751         getPageScroll: function() {
752             var docElement  = document.documentElement || {},
753                 body        = document.body || {};
754 
755             if ( typeof( window.pageXOffset ) == "number" ) {
756                 $.getPageScroll = function(){
757                     return new $.Point(
758                         window.pageXOffset,
759                         window.pageYOffset
760                     );
761                 };
762             } else if ( body.scrollLeft || body.scrollTop ) {
763                 $.getPageScroll = function(){
764                     return new $.Point(
765                         document.body.scrollLeft,
766                         document.body.scrollTop
767                     );
768                 };
769             } else if ( docElement.scrollLeft || docElement.scrollTop ) {
770                 $.getPageScroll = function(){
771                     return new $.Point(
772                         document.documentElement.scrollLeft,
773                         document.documentElement.scrollTop
774                     );
775                 };
776             }
777 
778             return $.getPageScroll();
779         },
780 
781 
782         /**
783          * Determines the size of the browsers window.
784          * @function
785          * @name OpenSeadragon.getWindowSize
786          * @returns {Point}
787          */
788         getWindowSize: function() {
789             var docElement = document.documentElement || {},
790                 body    = document.body || {};
791 
792             if ( typeof( window.innerWidth ) == 'number' ) {
793                 $.getWindowSize = function(){
794                     return new $.Point(
795                         window.innerWidth,
796                         window.innerHeight
797                     );
798                 }
799             } else if ( docElement.clientWidth || docElement.clientHeight ) {
800                 $.getWindowSize = function(){
801                     return new $.Point(
802                         document.documentElement.clientWidth,
803                         document.documentElement.clientHeight
804                     );
805                 }
806             } else if ( body.clientWidth || body.clientHeight ) {
807                 $.getWindowSize = function(){
808                     return new $.Point(
809                         document.body.clientWidth,
810                         document.body.clientHeight
811                     );
812                 }
813             } else {
814                 throw new Error("Unknown window size, no known technique.");
815             }
816 
817             return $.getWindowSize();
818         },
819 
820 
821         /**
822          * Wraps the given element in a nest of divs so that the element can
823          * be easily centered.
824          * @function
825          * @name OpenSeadragon.makeCenteredNode
826          * @param {Element|String} element
827          * @returns {Element}
828          */
829         makeCenteredNode: function( element ) {
830 
831             var div      = $.makeNeutralElement( "div" ),
832                 html     = [],
833                 innerDiv,
834                 innerDivs;
835 
836             element = $.getElement( element );
837 
838             //TODO: I dont understand the use of # inside the style attributes
839             //      below.  Invetigate the results of the constructed html in
840             //      the browser and clean up the mark-up to make this clearer.
841             html.push('<div style="display:table; height:100%; width:100%;');
842             html.push('border:none; margin:0px; padding:0px;'); // neutralizing
843             html.push('#position:relative; overflow:hidden; text-align:left;">');
844             html.push('<div style="#position:absolute; #top:50%; width:100%; ');
845             html.push('border:none; margin:0px; padding:0px;'); // neutralizing
846             html.push('display:table-cell; vertical-align:middle;">');
847             html.push('<div style="#position:relative; #top:-50%; width:100%; ');
848             html.push('border:none; margin:0px; padding:0px;'); // neutralizing
849             html.push('text-align:center;"></div></div></div>');
850 
851             div.innerHTML = html.join( '' );
852             div           = div.firstChild;
853 
854             innerDiv    = div;
855             innerDivs   = div.getElementsByTagName( "div" );
856             while ( innerDivs.length > 0 ) {
857                 innerDiv  = innerDivs[ 0 ];
858                 innerDivs = innerDiv.getElementsByTagName( "div" );
859             }
860 
861             innerDiv.appendChild( element );
862 
863             return div;
864         },
865 
866 
867         /**
868          * Creates an easily positionable element of the given type that therefor
869          * serves as an excellent container element.
870          * @function
871          * @name OpenSeadragon.makeNeutralElement
872          * @param {String} tagName
873          * @returns {Element}
874          */
875         makeNeutralElement: function( tagName ) {
876             var element = document.createElement( tagName ),
877                 style   = element.style;
878 
879             style.background = "transparent none";
880             style.border     = "none";
881             style.margin     = "0px";
882             style.padding    = "0px";
883             style.position   = "static";
884 
885             return element;
886         },
887 
888 
889         /**
890          * Ensures an image is loaded correctly to support alpha transparency.
891          * Generally only IE has issues doing this correctly for formats like 
892          * png.
893          * @function
894          * @name OpenSeadragon.makeTransparentImage
895          * @param {String} src
896          * @returns {Element}
897          */
898         makeTransparentImage: function( src ) {
899 
900             $.makeTransparentImage = function( src ){
901                 var img = $.makeNeutralElement( "img" );
902                 
903                 img.src = src;
904                 
905                 return img;
906             };
907 
908             if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) {
909 
910                 $.makeTransparentImage = function( src ){
911                     var img     = $.makeNeutralElement( "img" ),
912                         element = null;
913 
914                     element = $.makeNeutralElement("span");
915                     element.style.display = "inline-block";
916 
917                     img.onload = function() {
918                         element.style.width  = element.style.width || img.width + "px";
919                         element.style.height = element.style.height || img.height + "px";
920 
921                         img.onload = null;
922                         img = null;     // to prevent memory leaks in IE
923                     };
924 
925                     img.src = src;
926                     element.style.filter =
927                         "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" +
928                         src + 
929                         "', sizingMethod='scale')";
930 
931                     return element;
932                 };
933 
934             } 
935 
936             return $.makeTransparentImage( src );
937         },
938 
939 
940         /**
941          * Sets the opacity of the specified element.
942          * @function
943          * @name OpenSeadragon.setElementOpacity
944          * @param {Element|String} element
945          * @param {Number} opacity
946          * @param {Boolean} [usesAlpha]
947          */
948         setElementOpacity: function( element, opacity, usesAlpha ) {
949 
950             var previousFilter,
951                 ieOpacity,
952                 ieFilter;
953 
954             element = $.getElement( element );
955 
956             if ( usesAlpha && !$.Browser.alpha ) {
957                 opacity = Math.round( opacity );
958             }
959 
960             if ( opacity < 1 ) {
961                 element.style.opacity = opacity;
962             } else {
963                 element.style.opacity = "";
964             }
965 
966             if ( opacity == 1 ) {
967                 prevFilter = element.style.filter || "";
968                 element.style.filter = prevFilter.replace(/alpha\(.*?\)/g, "");
969                 return;
970             }
971 
972             ieOpacity = Math.round( 100 * opacity );
973             ieFilter  = " alpha(opacity=" + ieOpacity + ") ";
974 
975             //TODO: find out why this uses a try/catch instead of a predetermined
976             //      routine or at least an if/elseif/else
977             try {
978                 if ( element.filters && element.filters.alpha ) {
979                     element.filters.alpha.opacity = ieOpacity;
980                 } else {
981                     element.style.filter += ieFilter;
982                 }
983             } catch ( e ) {
984                 element.style.filter += ieFilter;
985             }
986         },
987 
988 
989         /**
990          * Adds an event listener for the given element, eventName and handler.
991          * @function
992          * @name OpenSeadragon.addEvent
993          * @param {Element|String} element
994          * @param {String} eventName
995          * @param {Function} handler
996          * @param {Boolean} [useCapture]
997          * @throws {Error}
998          */
999         addEvent: function( element, eventName, handler, useCapture ) {
1000             element = $.getElement( element );
1001 
1002             //TODO: Why do this if/else on every method call instead of just
1003             //      defining this function once based on the same logic
1004             if ( element.addEventListener ) {
1005                 $.addEvent = function( element, eventName, handler, useCapture ){
1006                     element = $.getElement( element );
1007                     element.addEventListener( eventName, handler, useCapture );                    
1008                 };
1009             } else if ( element.attachEvent ) {
1010                 $.addEvent = function( element, eventName, handler, useCapture ){
1011                     element = $.getElement( element );
1012                     element.attachEvent( "on" + eventName, handler );
1013                     if ( useCapture && element.setCapture ) {
1014                         element.setCapture();
1015                     }                    
1016                 };
1017             } else {
1018                 throw new Error(
1019                     "Unable to attach event handler, no known technique."
1020                 );
1021             }
1022 
1023             return $.addEvent( element, eventName, handler, useCapture );
1024         },
1025 
1026 
1027         /**
1028          * Remove a given event listener for the given element, event type and 
1029          * handler.
1030          * @function
1031          * @name OpenSeadragon.removeEvent
1032          * @param {Element|String} element
1033          * @param {String} eventName
1034          * @param {Function} handler
1035          * @param {Boolean} [useCapture]
1036          * @throws {Error}
1037          */
1038         removeEvent: function( element, eventName, handler, useCapture ) {
1039             element = $.getElement( element );
1040 
1041             //TODO: Why do this if/else on every method call instead of just
1042             //      defining this function once based on the same logic
1043             if ( element.removeEventListener ) {
1044                 $.removeEvent = function( element, eventName, handler, useCapture ) {
1045                     element = $.getElement( element );
1046                     element.removeEventListener( eventName, handler, useCapture );
1047                 };
1048             } else if ( element.detachEvent ) {
1049                 $.removeEvent = function( element, eventName, handler, useCapture ) {
1050                     element = $.getElement( element );
1051                     element.detachEvent("on" + eventName, handler);
1052                     if ( useCapture && element.releaseCapture ) {
1053                         element.releaseCapture();
1054                     }
1055                 };
1056             } else {
1057                 throw new Error(
1058                     "Unable to detach event handler, no known technique."
1059                 );
1060             }
1061             return $.removeEvent( element, eventName, handler, useCapture );
1062         },
1063 
1064 
1065         /**
1066          * Cancels the default browser behavior had the event propagated all
1067          * the way up the DOM to the window object.
1068          * @function
1069          * @name OpenSeadragon.cancelEvent
1070          * @param {Event} [event]
1071          */
1072         cancelEvent: function( event ) {
1073             event = $.getEvent( event );
1074 
1075             if ( event.preventDefault ) {
1076                 $.cancelEvent = function( event ){
1077                     // W3C for preventing default
1078                     event.preventDefault();
1079                 }
1080             } else {
1081                 $.cancelEvent = function( event ){
1082                     event = $.getEvent( event );
1083                     // legacy for preventing default
1084                     event.cancel = true;
1085                     // IE for preventing default
1086                     event.returnValue = false;
1087                 };
1088             }
1089             $.cancelEvent( event );
1090         },
1091 
1092 
1093         /**
1094          * Stops the propagation of the event up the DOM.
1095          * @function
1096          * @name OpenSeadragon.stopEvent
1097          * @param {Event} [event]
1098          */
1099         stopEvent: function( event ) {
1100             event = $.getEvent( event );
1101 
1102             if ( event.stopPropagation ) {    
1103                 // W3C for stopping propagation
1104                 $.stopEvent = function( event ){
1105                     event.stopPropagation();
1106                 };
1107             } else {      
1108                 // IE for stopping propagation
1109                 $.stopEvent = function( event ){
1110                     event = $.getEvent( event );
1111                     event.cancelBubble = true;
1112                 };
1113                 
1114             }
1115 
1116             $.stopEvent( event );
1117         },
1118 
1119 
1120         /**
1121          * Similar to OpenSeadragon.delegate, but it does not immediately call 
1122          * the method on the object, returning a function which can be called
1123          * repeatedly to delegate the method. It also allows additonal arguments
1124          * to be passed during construction which will be added during each
1125          * invocation, and each invocation can add additional arguments as well.
1126          * 
1127          * @function
1128          * @name OpenSeadragon.createCallback
1129          * @param {Object} object
1130          * @param {Function} method
1131          * @param [args] any additional arguments are passed as arguments to the 
1132          *  created callback
1133          * @returns {Function}
1134          */
1135         createCallback: function( object, method ) {
1136             //TODO: This pattern is painful to use and debug.  It's much cleaner
1137             //      to use pinning plus anonymous functions.  Get rid of this
1138             //      pattern!
1139             var initialArgs = [],
1140                 i;
1141             for ( i = 2; i < arguments.length; i++ ) {
1142                 initialArgs.push( arguments[ i ] );
1143             }
1144 
1145             return function() {
1146                 var args = initialArgs.concat( [] ),
1147                     i;
1148                 for ( i = 0; i < arguments.length; i++ ) {
1149                     args.push( arguments[ i ] );
1150                 }
1151 
1152                 return method.apply( object, args );
1153             };
1154         },
1155 
1156 
1157         /**
1158          * Retreives the value of a url parameter from the window.location string.
1159          * @function
1160          * @name OpenSeadragon.getUrlParameter
1161          * @param {String} key
1162          * @returns {String} The value of the url parameter or null if no param matches.
1163          */
1164         getUrlParameter: function( key ) {
1165             var value = URLPARAMS[ key ];
1166             return value ? value : null;
1167         },
1168 
1169 
1170         createAjaxRequest: function(){
1171             var request;
1172 
1173             if ( window.ActiveXObject ) {
1174                 //TODO: very bad...Why check every time using try/catch when
1175                 //      we could determine once at startup which activeX object
1176                 //      was supported.  This will have significant impact on 
1177                 //      performance for IE Browsers
1178                 for ( i = 0; i < ACTIVEX.length; i++ ) {
1179                     try {
1180                         request = new ActiveXObject( ACTIVEX[ i ] );
1181                         $.createAjaxRequest = function( ){
1182                             return new ActiveXObject( ACTIVEX[ i ] );
1183                         };
1184                         break;
1185                     } catch (e) {
1186                         continue;
1187                     }
1188                 }
1189             } else if ( window.XMLHttpRequest ) {
1190                 $.createAjaxRequest = function( ){
1191                     return new XMLHttpRequest();
1192                 };
1193                 request = new XMLHttpRequest();
1194             }
1195 
1196             if ( !request ) {
1197                 throw new Error( "Browser doesn't support XMLHttpRequest." );
1198             }
1199 
1200             return request;
1201         },
1202         /**
1203          * Makes an AJAX request.
1204          * @function
1205          * @name OpenSeadragon.makeAjaxRequest
1206          * @param {String} url - the url to request 
1207          * @param {Function} [callback] - a function to call when complete
1208          * @throws {Error}
1209          */
1210         makeAjaxRequest: function( url, callback ) {
1211             var async   = typeof( callback ) == "function",
1212                 request = $.createAjaxRequest(),
1213                 actual,
1214                 i;
1215 
1216             if ( async ) {
1217                 actual = callback;
1218                 callback = function() {
1219                     window.setTimeout(
1220                         $.createCallback( null, actual, request ), 
1221                         1
1222                     );
1223                 };
1224                 /** @ignore */
1225                 request.onreadystatechange = function() {
1226                     if ( request.readyState == 4) {
1227                         request.onreadystatechange = new function() { };
1228                         callback();
1229                     }
1230                 };
1231             }
1232 
1233             try {
1234                 request.open( "GET", url, async );
1235                 request.send( null );
1236             } catch (e) {
1237                 $.console.log(
1238                     "%s while making AJAX request: %s",
1239                     e.name, 
1240                     e.message
1241                 );
1242 
1243                 request.onreadystatechange = null;
1244                 request = null;
1245 
1246                 if ( async ) {
1247                     callback();
1248                 }
1249             }
1250 
1251             return async ? null : request;
1252         },
1253 
1254 
1255         /**
1256          * Taken from jQuery 1.6.1
1257          * @function
1258          * @name OpenSeadragon.jsonp
1259          * @param {Object} options
1260          * @param {String} options.url
1261          * @param {Function} options.callback
1262          * @param {String} [options.param='callback'] The name of the url parameter
1263          *      to request the jsonp provider with.
1264          * @param {String} [options.callbackName=] The name of the callback to
1265          *      request the jsonp provider with.
1266          */
1267         jsonp: function( options ){
1268             var script,
1269                 url     = options.url,
1270                 head    = document.head || 
1271                     document.getElementsByTagName( "head" )[ 0 ] || 
1272                     document.documentElement,
1273                 jsonpCallback = options.callbackName || 'openseadragon' + (+new Date()),
1274                 previous      = window[ jsonpCallback ],
1275                 replace       = "$1" + jsonpCallback + "$2",
1276                 callbackParam = options.param || 'callback',
1277                 callback      = options.callback;
1278 
1279             url = url.replace( /(\=)\?(&|$)|\?\?/i, replace );
1280             // Add callback manually
1281             url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
1282 
1283             // Install callback
1284             window[ jsonpCallback ] = function( response ) {
1285                 if ( !previous ){
1286                     delete window[ jsonpCallback ];
1287                 } else {
1288                     window[ jsonpCallback ] = previous;
1289                 }
1290                 if( callback && $.isFunction( callback ) ){
1291                     callback( response );
1292                 }
1293             };
1294 
1295             script = document.createElement( "script" );
1296 
1297             script.async = "async";
1298 
1299             if ( options.scriptCharset ) {
1300                 script.charset = options.scriptCharset;
1301             }
1302 
1303             script.src = url;
1304 
1305             // Attach handlers for all browsers
1306             script.onload = script.onreadystatechange = function( _, isAbort ) {
1307 
1308                 if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
1309 
1310                     // Handle memory leak in IE
1311                     script.onload = script.onreadystatechange = null;
1312 
1313                     // Remove the script
1314                     if ( head && script.parentNode ) {
1315                         head.removeChild( script );
1316                     }
1317 
1318                     // Dereference the script
1319                     script = undefined;
1320                 }
1321             };
1322             // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
1323             // This arises when a base node is used (#2709 and #4378).
1324             head.insertBefore( script, head.firstChild );
1325         
1326         },
1327 
1328 
1329         /**
1330          * Loads a Deep Zoom Image description from a url or XML string and
1331          * provides a callback hook for the resulting Document
1332          * @function
1333          * @name OpenSeadragon.createFromDZI
1334          * @param {String} xmlUrl
1335          * @param {String} xmlString
1336          * @param {Function} callback
1337          */
1338         createFromDZI: function( dzi, callback, tileHost ) {
1339             var async       = typeof ( callback ) == "function",
1340                 xmlUrl      = dzi.substring(0,1) != '<' ? dzi : null,
1341                 xmlString   = xmlUrl ? null : dzi,
1342                 error       = null,
1343                 urlParts,
1344                 filename,
1345                 lastDot,
1346                 tilesUrl;
1347 
1348 
1349             if( tileHost ){
1350 
1351                 tilesUrl = tileHost + "/_files/";
1352                 
1353             } else if( xmlUrl ) {
1354 
1355                 urlParts = xmlUrl.split( '/' );
1356                 filename = urlParts[ urlParts.length - 1 ];
1357                 lastDot  = filename.lastIndexOf( '.' );
1358 
1359                 if ( lastDot > -1 ) {
1360                     urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
1361                 }
1362 
1363                 tilesUrl = urlParts.join( '/' ) + "_files/";
1364 
1365             }
1366 
1367             function finish( func, obj ) {
1368                 try {
1369                     return func( obj, tilesUrl );
1370                 } catch ( e ) {
1371                     if ( async ) {
1372                         return null;
1373                     } else {
1374                         throw e;
1375                     }
1376                 }
1377             }
1378 
1379             if ( async ) {
1380                 if ( xmlString ) {
1381                     window.setTimeout( function() {
1382                         var source = finish( processDZIXml, parseXml( xmlString ) );
1383                         // call after finish sets error
1384                         callback( source, error );    
1385                     }, 1);
1386                 } else {
1387                     $.makeAjaxRequest( xmlUrl, function( xhr ) {
1388                         var source = finish( processDZIResponse, xhr );
1389                         // call after finish sets error
1390                         callback( source, error );
1391                     });
1392                 }
1393 
1394                 return null;
1395             }
1396 
1397             if ( xmlString ) {
1398                 return finish( 
1399                     processDZIXml,
1400                     parseXml( xmlString ) 
1401                 );
1402             } else {
1403                 return finish( 
1404                     processDZIResponse, 
1405                     $.makeAjaxRequest( xmlUrl )
1406                 );
1407             }
1408         }
1409 
1410     });
1411 
1412 
1413     /**
1414      * The current browser vendor, version, and related information regarding
1415      * detected features.  Features include <br/>
1416      *  <strong>'alpha'</strong> - Does the browser support image alpha 
1417      *  transparency.<br/>
1418      * @name $.Browser
1419      * @static
1420      */
1421     $.Browser = {
1422         vendor:     $.BROWSERS.UNKNOWN,
1423         version:    0,
1424         alpha:      true
1425     };
1426 
1427 
1428     var ACTIVEX = [
1429             "Msxml2.XMLHTTP", 
1430             "Msxml3.XMLHTTP", 
1431             "Microsoft.XMLHTTP"
1432         ],  
1433         FILEFORMATS = {
1434             "bmp":  false,
1435             "jpeg": true,
1436             "jpg":  true,
1437             "png":  true,
1438             "tif":  false,
1439             "wdp":  false
1440         },
1441         URLPARAMS = {};
1442 
1443     (function() {
1444         //A small auto-executing routine to determine the browser vendor, 
1445         //version and supporting feature sets.
1446         var app = navigator.appName,
1447             ver = navigator.appVersion,
1448             ua  = navigator.userAgent;
1449 
1450         //console.error( 'appName: ' + navigator.appName );
1451         //console.error( 'appVersion: ' + navigator.appVersion );
1452         //console.error( 'userAgent: ' + navigator.userAgent );
1453 
1454         switch( navigator.appName ){
1455             case "Microsoft Internet Explorer":
1456                 if( !!window.attachEvent && 
1457                     !!window.ActiveXObject ) {
1458 
1459                     $.Browser.vendor = $.BROWSERS.IE;
1460                     $.Browser.version = parseFloat(
1461                         ua.substring( 
1462                             ua.indexOf( "MSIE" ) + 5, 
1463                             ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
1464                         );
1465                 }
1466                 break;
1467             case "Netscape":
1468                 if( !!window.addEventListener ){
1469                     if ( ua.indexOf( "Firefox" ) >= 0 ) {
1470                         $.Browser.vendor = $.BROWSERS.FIREFOX;
1471                         $.Browser.version = parseFloat(
1472                             ua.substring( ua.indexOf( "Firefox" ) + 8 )
1473                         );
1474                     } else if ( ua.indexOf( "Safari" ) >= 0 ) {
1475                         $.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ? 
1476                             $.BROWSERS.CHROME : 
1477                             $.BROWSERS.SAFARI;
1478                         $.Browser.version = parseFloat(
1479                             ua.substring( 
1480                                 ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1, 
1481                                 ua.indexOf( "Safari" )
1482                             )
1483                         );
1484                     }
1485                 }
1486                 break;
1487             case "Opera":
1488                 $.Browser.vendor = $.BROWSERS.OPERA;
1489                 $.Browser.version = parseFloat( ver );
1490                 break;
1491         }
1492 
1493             // ignore '?' portion of query string
1494         var query = window.location.search.substring( 1 ),
1495             parts = query.split('&'),
1496             part,
1497             sep,
1498             i;
1499 
1500         for ( i = 0; i < parts.length; i++ ) {
1501             part = parts[ i ];
1502             sep  = part.indexOf( '=' );
1503 
1504             if ( sep > 0 ) {
1505                 URLPARAMS[ part.substring( 0, sep ) ] =
1506                     decodeURIComponent( part.substring( sep + 1 ) );
1507             }
1508         }
1509 
1510         //determine if this browser supports image alpha transparency
1511         $.Browser.alpha = !( 
1512             ( 
1513                 $.Browser.vendor == $.BROWSERS.IE && 
1514                 $.Browser.version < 9
1515             ) || (
1516                 $.Browser.vendor == $.BROWSERS.CHROME && 
1517                 $.Browser.version < 2
1518             )
1519         );
1520 
1521     })();
1522 
1523     //TODO: $.console is often used inside a try/catch block which generally
1524     //      prevents allowings errors to occur with detection until a debugger
1525     //      is attached.  Although I've been guilty of the same anti-pattern
1526     //      I eventually was convinced that errors should naturally propogate in
1527     //      all but the most special cases.
1528     /**
1529      * A convenient alias for console when available, and a simple null 
1530      * function when console is unavailable.
1531      * @static
1532      * @private
1533      */
1534     var nullfunction = function( msg ){
1535             //document.location.hash = msg;
1536         };
1537 
1538     $.console = window.console || {
1539         log:    nullfunction,
1540         debug:  nullfunction,
1541         info:   nullfunction,
1542         warn:   nullfunction,
1543         error:  nullfunction
1544     };
1545         
1546 
1547     /**
1548      * @private
1549      * @inner
1550      * @function
1551      * @param {Element} element 
1552      * @param {Boolean} [isFixed]
1553      * @returns {Element}
1554      */
1555     function getOffsetParent( element, isFixed ) {
1556         if ( isFixed && element != document.body ) {
1557             return document.body;
1558         } else {
1559             return element.offsetParent;
1560         }
1561     };
1562 
1563     /**
1564      * @private
1565      * @inner
1566      * @function
1567      * @param {XMLHttpRequest} xhr
1568      * @param {String} tilesUrl
1569      */
1570     function processDZIResponse( xhr, tilesUrl ) {
1571         var status,
1572             statusText,
1573             doc = null;
1574 
1575         if ( !xhr ) {
1576             throw new Error( $.getString( "Errors.Security" ) );
1577         } else if ( xhr.status !== 200 && xhr.status !== 0 ) {
1578             status     = xhr.status;
1579             statusText = ( status == 404 ) ? 
1580                 "Not Found" : 
1581                 xhr.statusText;
1582             throw new Error( $.getString( "Errors.Status", status, statusText ) );
1583         }
1584 
1585         if ( xhr.responseXML && xhr.responseXML.documentElement ) {
1586             doc = xhr.responseXML;
1587         } else if ( xhr.responseText ) {
1588             doc = parseXml( xhr.responseText );
1589         }
1590 
1591         return processDZIXml( doc, tilesUrl );
1592     };
1593 
1594     /**
1595      * @private
1596      * @inner
1597      * @function
1598      * @param {Document} xmlDoc
1599      * @param {String} tilesUrl
1600      */
1601     function processDZIXml( xmlDoc, tilesUrl ) {
1602 
1603         if ( !xmlDoc || !xmlDoc.documentElement ) {
1604             throw new Error( $.getString( "Errors.Xml" ) );
1605         }
1606 
1607         var root     = xmlDoc.documentElement,
1608             rootName = root.tagName;
1609 
1610         if ( rootName == "Image" ) {
1611             try {
1612                 return processDZI( root, tilesUrl );
1613             } catch ( e ) {
1614                 throw (e instanceof Error) ? 
1615                     e : 
1616                     new Error( $.getString("Errors.Dzi") );
1617             }
1618         } else if ( rootName == "Collection" ) {
1619             throw new Error( $.getString( "Errors.Dzc" ) );
1620         } else if ( rootName == "Error" ) {
1621             return processDZIError( root );
1622         }
1623 
1624         throw new Error( $.getString( "Errors.Dzi" ) );
1625     };
1626 
1627     /**
1628      * @private
1629      * @inner
1630      * @function
1631      * @param {Element} imageNode
1632      * @param {String} tilesUrl
1633      */
1634     function processDZI( imageNode, tilesUrl ) {
1635         var fileFormat    = imageNode.getAttribute( "Format" ),
1636             sizeNode      = imageNode.getElementsByTagName( "Size" )[ 0 ],
1637             dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ),
1638             width         = parseInt( sizeNode.getAttribute( "Width" ) ),
1639             height        = parseInt( sizeNode.getAttribute( "Height" ) ),
1640             tileSize      = parseInt( imageNode.getAttribute( "TileSize" ) ),
1641             tileOverlap   = parseInt( imageNode.getAttribute( "Overlap" ) ),
1642             dispRects     = [],
1643             dispRectNode,
1644             rectNode,
1645             i;
1646 
1647         if ( !imageFormatSupported( fileFormat ) ) {
1648             throw new Error(
1649                 $.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
1650             );
1651         }
1652 
1653         for ( i = 0; i < dispRectNodes.length; i++ ) {
1654             dispRectNode = dispRectNodes[ i ];
1655             rectNode     = dispRectNode.getElementsByTagName( "Rect" )[ 0 ];
1656 
1657             dispRects.push( new $.DisplayRect(
1658                 parseInt( rectNode.getAttribute( "X" ) ),
1659                 parseInt( rectNode.getAttribute( "Y" ) ),
1660                 parseInt( rectNode.getAttribute( "Width" ) ),
1661                 parseInt( rectNode.getAttribute( "Height" ) ),
1662                 0,  // ignore MinLevel attribute, bug in Deep Zoom Composer
1663                 parseInt( dispRectNode.getAttribute( "MaxLevel" ) )
1664             ));
1665         }
1666         return new $.DziTileSource(
1667             width, 
1668             height, 
1669             tileSize, 
1670             tileOverlap,
1671             tilesUrl, 
1672             fileFormat, 
1673             dispRects
1674         );
1675     };
1676 
1677     /**
1678      * @private
1679      * @inner
1680      * @function
1681      * @param {Document} errorNode
1682      * @throws {Error}
1683      */
1684     function processDZIError( errorNode ) {
1685         var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ],
1686             message     = messageNode.firstChild.nodeValue;
1687 
1688         throw new Error(message);
1689     };
1690 
1691     /**
1692      * Reports whether the image format is supported for tiling in this
1693      * version.
1694      * @private
1695      * @inner
1696      * @function
1697      * @param {String} [extension]
1698      * @returns {Boolean}
1699      */
1700     function imageFormatSupported( extension ) {
1701         extension = extension ? extension : "";
1702         return !!FILEFORMATS[ extension.toLowerCase() ];
1703     };
1704 
1705     /**
1706      * Parses an XML string into a DOM Document.
1707      * @private
1708      * @inner
1709      * @function
1710      * @name OpenSeadragon.parseXml
1711      * @param {String} string
1712      * @returns {Document}
1713      */
1714     function parseXml( string ) {
1715         //TODO: yet another example where we can determine the correct
1716         //      implementation once at start-up instead of everytime we use
1717         //      the function. DONE.
1718         if ( window.ActiveXObject ) {
1719 
1720             $.parseXml = function( string ){
1721                 var xmlDoc = null,
1722                     parser;
1723 
1724                 xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" );
1725                 xmlDoc.async = false;
1726                 xmlDoc.loadXML( string );
1727                 return xmlDoc;
1728             };
1729 
1730         } else if ( window.DOMParser ) {
1731             
1732             $.parseXml = function( string ){
1733                 var xmlDoc = null,
1734                     parser;
1735 
1736                 parser = new DOMParser();
1737                 xmlDoc = parser.parseFromString( string, "text/xml" );
1738                 return xmlDoc;
1739             };
1740 
1741         } else {
1742             throw new Error( "Browser doesn't support XML DOM." );
1743         }
1744 
1745         return $.parseXml( string );
1746     };
1747     
1748 }( OpenSeadragon ));
1749 
1750 (function($){
1751 
1752 /**
1753  * For use by classes which want to support custom, non-browser events.
1754  * @class
1755  */
1756 $.EventHandler = function() {
1757     this.events = {};
1758 };
1759 
1760 $.EventHandler.prototype = {
1761 
1762     /**
1763      * Add an event handler for a given event.
1764      * @function
1765      * @param {String} eventName - Name of event to register.
1766      * @param {Function} handler - Function to call when event is triggered.
1767      */
1768     addHandler: function( eventName, handler ) {
1769         var events = this.events[ eventName ];
1770         if( !events ){
1771             this.events[ eventName ] = events = [];
1772         }
1773         if( handler && $.isFunction( handler ) ){
1774             events[ events.length ] = handler;
1775         }
1776     },
1777 
1778     /**
1779      * Remove a specific event handler for a given event.
1780      * @function
1781      * @param {String} eventName - Name of event for which the handler is to be removed.
1782      * @param {Function} handler - Function to be removed.
1783      */
1784     removeHandler: function( eventName, handler ) {
1785         //Start Thatcher - unneccessary indirection.  Also, because events were
1786         //               - not actually being removed, we need to add the code
1787         //               - to do the removal ourselves. TODO
1788         var events = this.events[ eventName ];
1789         if ( !events ){ 
1790             return; 
1791         }
1792         //End Thatcher
1793     },
1794 
1795     /**
1796      * Retrive the list of all handlers registered for a given event.
1797      * @function
1798      * @param {String} eventName - Name of event to get handlers for.
1799      */
1800     getHandler: function( eventName ) {
1801         var events = this.events[ eventName ]; 
1802         if ( !events || !events.length ){ 
1803             return null; 
1804         }
1805         events = events.length === 1 ? 
1806             [ events[ 0 ] ] : 
1807             Array.apply( null, events );
1808         return function( source, args ) {
1809             var i, 
1810                 length = events.length;
1811             for ( i = 0; i < length; i++ ) {
1812                 if( events[ i ] ){
1813                     events[ i ]( source, args );
1814                 }
1815             }
1816         };
1817     },
1818 
1819     /**
1820      * Trigger an event, optionally passing additional information.
1821      * @function
1822      * @param {String} eventName - Name of event to register.
1823      * @param {Function} handler - Function to call when event is triggered.
1824      */
1825     raiseEvent: function( eventName, eventArgs ) {
1826         var handler = this.getHandler( eventName );
1827 
1828         if ( handler ) {
1829             if ( !eventArgs ) {
1830                 eventArgs = new Object();
1831             }
1832 
1833             handler( this, eventArgs );
1834         }
1835     }
1836 };
1837 
1838 }( OpenSeadragon ));
1839 
1840 (function( $ ){
1841         
1842         // is any button currently being pressed while mouse events occur
1843     var IS_BUTTON_DOWN  = false,
1844         // is any tracker currently capturing?
1845         IS_CAPTURING    = false,
1846         // dictionary from hash to MouseTracker
1847         ACTIVE          = {},   
1848         // list of trackers interested in capture
1849         CAPTURING       = [],
1850         // dictionary from hash to private properties
1851         THIS            = {};   
1852 
1853     /**
1854      * The MouseTracker allows other classes to set handlers for common mouse 
1855      * events on a specific element like, 'enter', 'exit', 'press', 'release',
1856      * 'scroll', 'click', and 'drag'.
1857      * @class
1858      * @param {Object} options 
1859      *      Allows configurable properties to be entirely specified by passing
1860      *      an options object to the constructor.  The constructor also supports 
1861      *      the original positional arguments 'elements', 'clickTimeThreshold',
1862      *      and 'clickDistThreshold' in that order.
1863      * @param {Element|String} options.element 
1864      *      A reference to an element or an element id for which the mouse 
1865      *      events will be monitored.
1866      * @param {Number} options.clickTimeThreshold 
1867      *      The number of milliseconds within which mutliple mouse clicks 
1868      *      will be treated as a single event.
1869      * @param {Number} options.clickDistThreshold 
1870      *      The distance between mouse click within multiple mouse clicks 
1871      *      will be treated as a single event.
1872      * @param {Function} options.enterHandler
1873      *      An optional handler for mouse enter.
1874      * @param {Function} options.exitHandler
1875      *      An optional handler for mouse exit.
1876      * @param {Function} options.pressHandler
1877      *      An optional handler for mouse press.
1878      * @param {Function} options.releaseHandler
1879      *      An optional handler for mouse release.
1880      * @param {Function} options.scrollHandler
1881      *      An optional handler for mouse scroll.
1882      * @param {Function} options.clickHandler
1883      *      An optional handler for mouse click.
1884      * @param {Function} options.dragHandler
1885      *      An optional handler for mouse drag.
1886      * @property {Number} hash 
1887      *      An unique hash for this tracker.
1888      * @property {Element} element 
1889      *      The element for which mouse event are being monitored.
1890      * @property {Number} clickTimeThreshold
1891      *      The number of milliseconds within which mutliple mouse clicks 
1892      *      will be treated as a single event.
1893      * @property {Number} clickDistThreshold
1894      *      The distance between mouse click within multiple mouse clicks 
1895      *      will be treated as a single event.
1896      */
1897     $.MouseTracker = function ( options ) {
1898 
1899         var args  = arguments;
1900 
1901         if( !$.isPlainObject( options ) ){
1902             options = {
1903                 element:            args[ 0 ],
1904                 clickTimeThreshold: args[ 1 ],
1905                 clickDistThreshold: args[ 2 ]
1906             };
1907         }
1908 
1909         this.hash               = Math.random(); 
1910         this.element            = $.getElement( options.element );
1911         this.clickTimeThreshold = options.clickTimeThreshold;
1912         this.clickDistThreshold = options.clickDistThreshold;
1913 
1914 
1915         this.enterHandler       = options.enterHandler   || null;
1916         this.exitHandler        = options.exitHandler    || null;
1917         this.pressHandler       = options.pressHandler   || null;
1918         this.releaseHandler     = options.releaseHandler || null;
1919         this.scrollHandler      = options.scrollHandler  || null;
1920         this.clickHandler       = options.clickHandler   || null;
1921         this.dragHandler        = options.dragHandler    || null;
1922         this.keyHandler         = options.keyHandler     || null;
1923         this.focusHandler       = options.focusHandler   || null;
1924         this.blurHandler        = options.blurHandler    || null;
1925 
1926         //Store private properties in a scope sealed hash map
1927         var _this = this;
1928 
1929         /**
1930          * @private
1931          * @property {Boolean} tracking
1932          *      Are we currently tracking mouse events.
1933          * @property {Boolean} capturing
1934          *      Are we curruently capturing mouse events.
1935          * @property {Boolean} buttonDown
1936          *      True if the left mouse button is currently being pressed and was 
1937          *      initiated inside the tracked element, otherwise false.
1938          * @property {Boolean} insideElement
1939          *      Are we currently inside the screen area of the tracked element.
1940          * @property {OpenSeadragon.Point} lastPoint 
1941          *      Position of last mouse down/move
1942          * @property {Number} lastMouseDownTime 
1943          *      Time of last mouse down.
1944          * @property {OpenSeadragon.Point} lastMouseDownPoint 
1945          *      Position of last mouse down
1946          */
1947         THIS[ this.hash ] = {
1948             "mouseover":        function( event ){ onMouseOver( _this, event ); },
1949             "mouseout":         function( event ){ onMouseOut( _this, event ); },
1950             "mousedown":        function( event ){ onMouseDown( _this, event ); },
1951             "mouseup":          function( event ){ onMouseUp( _this, event ); },
1952             "click":            function( event ){ onMouseClick( _this, event ); },
1953             "DOMMouseScroll":   function( event ){ onMouseWheelSpin( _this, event ); },
1954             "mousewheel":       function( event ){ onMouseWheelSpin( _this, event ); },
1955             "mouseupie":        function( event ){ onMouseUpIE( _this, event ); },
1956             "mousemoveie":      function( event ){ onMouseMoveIE( _this, event ); },
1957             "mouseupwindow":    function( event ){ onMouseUpWindow( _this, event ); },
1958             "mousemove":        function( event ){ onMouseMove( _this, event ); },
1959             "touchstart":       function( event ){ onTouchStart( _this, event ); },
1960             "touchmove":        function( event ){ onTouchMove( _this, event ); },
1961             "touchend":         function( event ){ onTouchEnd( _this, event ); },
1962             "keypress":         function( event ){ onKeyPress( _this, event ); },
1963             "focus":            function( event ){ onFocus( _this, event ); },
1964             "blur":             function( event ){ onBlur( _this, event ); },
1965             tracking:           false,
1966             capturing:          false,
1967             buttonDown:         false,
1968             insideElement:      false,
1969             lastPoint:          null,
1970             lastMouseDownTime:  null,
1971             lastMouseDownPoint: null,
1972             lastPinchDelta:     0
1973         };
1974 
1975     };
1976 
1977     $.MouseTracker.prototype = {
1978 
1979         /**
1980          * Are we currently tracking events on this element.
1981          * @deprecated Just use this.tracking
1982          * @function
1983          * @returns {Boolean} Are we currently tracking events on this element.
1984          */
1985         isTracking: function () {
1986             return THIS[ this.hash ].tracking;
1987         },
1988 
1989         /**
1990          * Enable or disable whether or not we are tracking events on this element.
1991          * @function
1992          * @param {Boolean} track True to start tracking, false to stop tracking.
1993          * @returns {OpenSeadragon.MouseTracker} Chainable.
1994          */
1995         setTracking: function ( track ) {
1996             if ( track ) {
1997                 startTracking( this );
1998             } else {
1999                 stopTracking( this );
2000             }
2001             //chain
2002             return this;
2003         },
2004         
2005         /**
2006          * Implement or assign implmentation to these handlers during or after
2007          * calling the constructor.
2008          * @function
2009          * @param {OpenSeadragon.MouseTracker} tracker  
2010          *      A reference to the tracker instance.
2011          * @param {OpenSeadragon.Point} position
2012          *      The poistion of the event on the screen.
2013          * @param {Boolean} buttonDown
2014          *      True if the left mouse button is currently being pressed and was 
2015          *      initiated inside the tracked element, otherwise false.
2016          * @param {Boolean} buttonDownAny
2017          *      Was the button down anywhere in the screen during the event.
2018          */
2019         enterHandler: function(){},
2020 
2021         /**
2022          * Implement or assign implmentation to these handlers during or after
2023          * calling the constructor.
2024          * @function
2025          * @param {OpenSeadragon.MouseTracker} tracker  
2026          *      A reference to the tracker instance.
2027          * @param {OpenSeadragon.Point} position
2028          *      The poistion of the event on the screen.
2029          * @param {Boolean} buttonDown
2030          *      True if the left mouse button is currently being pressed and was 
2031          *      initiated inside the tracked element, otherwise false.
2032          * @param {Boolean} buttonDownAny
2033          *      Was the button down anywhere in the screen during the event.
2034          */
2035         exitHandler: function(){},
2036 
2037         /**
2038          * Implement or assign implmentation to these handlers during or after
2039          * calling the constructor.
2040          * @function
2041          * @param {OpenSeadragon.MouseTracker} tracker  
2042          *      A reference to the tracker instance.
2043          * @param {OpenSeadragon.Point} position
2044          *      The poistion of the event on the screen.
2045          */
2046         pressHandler: function(){},
2047 
2048         /**
2049          * Implement or assign implmentation to these handlers during or after
2050          * calling the constructor.
2051          * @function
2052          * @param {OpenSeadragon.MouseTracker} tracker  
2053          *      A reference to the tracker instance.
2054          * @param {OpenSeadragon.Point} position
2055          *      The poistion of the event on the screen.
2056          * @param {Boolean} buttonDown
2057          *      True if the left mouse button is currently being pressed and was 
2058          *      initiated inside the tracked element, otherwise false.
2059          * @param {Boolean} insideElementRelease
2060          *      Was the mouse still inside the tracked element when the button
2061          *      was released.
2062          */
2063         releaseHandler: function(){},
2064 
2065         /**
2066          * Implement or assign implmentation to these handlers during or after
2067          * calling the constructor.
2068          * @function
2069          * @param {OpenSeadragon.MouseTracker} tracker  
2070          *      A reference to the tracker instance.
2071          * @param {OpenSeadragon.Point} position
2072          *      The poistion of the event on the screen.
2073          * @param {Number} scroll
2074          *      The scroll delta for the event.
2075          * @param {Boolean} shift
2076          *      Was the shift key being pressed during this event?
2077          */
2078         scrollHandler: function(){},
2079 
2080         /**
2081          * Implement or assign implmentation to these handlers during or after
2082          * calling the constructor. 
2083          * @function
2084          * @param {OpenSeadragon.MouseTracker} tracker  
2085          *      A reference to the tracker instance.
2086          * @param {OpenSeadragon.Point} position
2087          *      The poistion of the event on the screen.
2088          * @param {Boolean} quick
2089          *      True only if the clickDistThreshold and clickDeltaThreshold are 
2090          *      both pased. Useful for ignoring events.
2091          * @param {Boolean} shift
2092          *      Was the shift key being pressed during this event?
2093          */
2094         clickHandler: function(){},
2095 
2096         /**
2097          * Implement or assign implmentation to these handlers during or after
2098          * calling the constructor. 
2099          * @function
2100          * @param {OpenSeadragon.MouseTracker} tracker  
2101          *      A reference to the tracker instance.
2102          * @param {OpenSeadragon.Point} position
2103          *      The poistion of the event on the screen.
2104          * @param {OpenSeadragon.Point} delta
2105          *      The x,y components of the difference between start drag and
2106          *      end drag.  Usefule for ignoring or weighting the events.
2107          * @param {Boolean} shift
2108          *      Was the shift key being pressed during this event?
2109          */
2110         dragHandler: function(){},
2111 
2112         /**
2113          * Implement or assign implmentation to these handlers during or after
2114          * calling the constructor. 
2115          * @function
2116          * @param {OpenSeadragon.MouseTracker} tracker  
2117          *      A reference to the tracker instance.
2118          * @param {Number} keyCode
2119          *      The key code that was pressed.
2120          * @param {Boolean} shift
2121          *      Was the shift key being pressed during this event?
2122          */
2123         keyHandler: function(){},
2124 
2125         focusHandler: function(){},
2126 
2127         blurHandler: function(){}
2128     };
2129 
2130     /**
2131      * Starts tracking mouse events on this element.
2132      * @private
2133      * @inner
2134      */
2135     function startTracking( tracker ) {
2136         var events = [
2137                 "mouseover", "mouseout", "mousedown", "mouseup", 
2138                 "click",
2139                 "DOMMouseScroll", "mousewheel", 
2140                 "touchstart", "touchmove", "touchend",
2141                 "keypress",
2142                 "focus", "blur"
2143             ], 
2144             delegate = THIS[ tracker.hash ],
2145             event, 
2146             i;
2147 
2148         if ( !delegate.tracking ) {
2149             for( i = 0; i < events.length; i++ ){
2150                 event = events[ i ];
2151                 $.addEvent( 
2152                     tracker.element, 
2153                     event, 
2154                     delegate[ event ], 
2155                     false
2156                 );
2157             }
2158             delegate.tracking = true;
2159             ACTIVE[ tracker.hash ] = tracker;
2160         }
2161     };
2162 
2163     /**
2164      * Stops tracking mouse events on this element.
2165      * @private
2166      * @inner
2167      */
2168     function stopTracking( tracker ) {
2169         var events = [
2170                 "mouseover", "mouseout", "mousedown", "mouseup", 
2171                 "click",
2172                 "DOMMouseScroll", "mousewheel", 
2173                 "touchstart", "touchmove", "touchend",
2174                 "keypress",
2175                 "focus", "blur"
2176             ],
2177             delegate = THIS[ tracker.hash ],
2178             event, 
2179             i;
2180         
2181         if ( delegate.tracking ) {
2182             for( i = 0; i < events.length; i++ ){
2183                 event = events[ i ];
2184                 $.removeEvent( 
2185                     tracker.element, 
2186                     event, 
2187                     delegate[ event ], 
2188                     false
2189                 );
2190             }
2191 
2192             releaseMouse( tracker );
2193             delegate.tracking = false;
2194             delete ACTIVE[ tracker.hash ];
2195         }
2196     };
2197 
2198     /**
2199      * @private
2200      * @inner
2201      */
2202     function hasMouse( tracker ) {
2203         return THIS[ tracker.hash ].insideElement;
2204     };
2205 
2206     /**
2207      * Begin capturing mouse events on this element.
2208      * @private
2209      * @inner
2210      */
2211     function captureMouse( tracker ) {
2212         var delegate = THIS[ tracker.hash ];
2213         if ( !delegate.capturing ) {
2214 
2215             if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
2216                 $.removeEvent( 
2217                     tracker.element, 
2218                     "mouseup", 
2219                     delegate[ "mouseup" ], 
2220                     false 
2221                 );
2222                 $.addEvent( 
2223                     tracker.element, 
2224                     "mouseup", 
2225                     delegate[ "mouseupie" ], 
2226                     true 
2227                 );
2228                 $.addEvent( 
2229                     tracker.element, 
2230                     "mousemove", 
2231                     delegate[ "mousemoveie" ], 
2232                     true 
2233                 );
2234             } else {
2235                 $.addEvent( 
2236                     window, 
2237                     "mouseup", 
2238                     delegate[ "mouseupwindow" ], 
2239                     true 
2240                 );
2241                 $.addEvent( 
2242                     window, 
2243                     "mousemove", 
2244                     delegate[ "mousemove" ], 
2245                     true 
2246                 );
2247             }
2248             delegate.capturing = true;
2249         }
2250     };
2251 
2252         
2253     /**
2254      * Stop capturing mouse events on this element.
2255      * @private
2256      * @inner
2257      */
2258     function releaseMouse( tracker ) {
2259         var delegate = THIS[ tracker.hash ];
2260         if ( delegate.capturing ) {
2261 
2262             if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
2263                 $.removeEvent( 
2264                     tracker.element, 
2265                     "mousemove", 
2266                     delegate[ "mousemoveie" ], 
2267                     true 
2268                 );
2269                 $.removeEvent( 
2270                     tracker.element, 
2271                     "mouseup", 
2272                     delegate[ "mouseupie" ], 
2273                     true 
2274                 );
2275                 $.addEvent( 
2276                     tracker.element, 
2277                     "mouseup", 
2278                     delegate[ "mouseup" ], 
2279                     false 
2280                 );
2281             } else {
2282                 $.removeEvent( 
2283                     window, 
2284                     "mousemove", 
2285                     delegate[ "mousemove" ], 
2286                     true 
2287                 );
2288                 $.removeEvent( 
2289                     window, 
2290                     "mouseup", 
2291                     delegate[ "mouseupwindow" ], 
2292                     true 
2293                 );
2294             }
2295             delegate.capturing = false;
2296         }
2297     };
2298 
2299 
2300     /**
2301      * @private
2302      * @inner
2303      */
2304     function triggerOthers( tracker, handler, event ) {
2305         var otherHash;
2306         for ( otherHash in ACTIVE ) {
2307             if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) {
2308                 handler( ACTIVE[ otherHash ], event );
2309             }
2310         }
2311     };
2312 
2313 
2314     /**
2315      * @private
2316      * @inner
2317      */
2318     function onFocus( tracker, event ){
2319         //console.log( "focus %s", event );
2320         var propagate;
2321         if ( tracker.focusHandler ) {
2322             propagate = tracker.focusHandler( 
2323                 tracker, 
2324                 event
2325             );
2326             if( propagate === false ){
2327                 $.cancelEvent( event );
2328             }
2329         }
2330     };
2331 
2332 
2333     /**
2334      * @private
2335      * @inner
2336      */
2337     function onBlur( tracker, event ){
2338         //console.log( "blur %s", event );
2339         var propagate;
2340         if ( tracker.blurHandler ) {
2341             propagate = tracker.blurHandler( 
2342                 tracker, 
2343                 event
2344             );
2345             if( propagate === false ){
2346                 $.cancelEvent( event );
2347             }
2348         }
2349     };
2350 
2351     
2352     /**
2353      * @private
2354      * @inner
2355      */
2356     function onKeyPress( tracker, event ){
2357         //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
2358         var propagate;
2359         if ( tracker.keyHandler ) {
2360             propagate = tracker.keyHandler( 
2361                 tracker, 
2362                 event.keyCode ? event.keyCode : event.charCode,
2363                 event.shiftKey
2364             );
2365             if( propagate === false ){
2366                 $.cancelEvent( event );
2367             }
2368         }
2369     };
2370 
2371 
2372     /**
2373      * @private
2374      * @inner
2375      */
2376     function onMouseOver( tracker, event ) {
2377 
2378         var event = $.getEvent( event ),
2379             delegate = THIS[ tracker.hash ],
2380             propagate;
2381 
2382         if ( $.Browser.vendor == $.BROWSERS.IE && 
2383              $.Browser.version < 9 && 
2384              delegate.capturing && 
2385              !isChild( event.srcElement, tracker.element ) ) {
2386 
2387             triggerOthers( tracker, onMouseOver, event );
2388 
2389         }
2390 
2391         var to = event.target ? 
2392                 event.target : 
2393                 event.srcElement,
2394             from = event.relatedTarget ? 
2395                 event.relatedTarget : 
2396                 event.fromElement;
2397 
2398         if ( !isChild( tracker.element, to ) || 
2399               isChild( tracker.element, from ) ) {
2400             return;
2401         }
2402 
2403         delegate.insideElement = true;
2404 
2405         if ( tracker.enterHandler ) {
2406             propagate = tracker.enterHandler(
2407                 tracker, 
2408                 getMouseRelative( event, tracker.element ),
2409                 delegate.buttonDown, 
2410                 IS_BUTTON_DOWN
2411             );
2412             if( propagate === false ){
2413                 $.cancelEvent( event );
2414             }
2415         }
2416     };
2417 
2418 
2419     /**
2420      * @private
2421      * @inner
2422      */
2423     function onMouseOut( tracker, event ) {
2424         var event = $.getEvent( event ),
2425             delegate = THIS[ tracker.hash ],
2426             propagate;
2427 
2428         if ( $.Browser.vendor == $.BROWSERS.IE && 
2429              $.Browser.version < 9 &&
2430              delegate.capturing && 
2431              !isChild( event.srcElement, tracker.element ) ) {
2432 
2433             triggerOthers( tracker, onMouseOut, event );
2434 
2435         }
2436 
2437         var from = event.target ? 
2438                 event.target : 
2439                 event.srcElement,
2440             to = event.relatedTarget ? 
2441                 event.relatedTarget : 
2442                 event.toElement;
2443 
2444         if ( !isChild( tracker.element, from ) || 
2445               isChild( tracker.element, to ) ) {
2446             return;
2447         }
2448 
2449         delegate.insideElement = false;
2450 
2451         if ( tracker.exitHandler ) {
2452             propagate = tracker.exitHandler( 
2453                 tracker, 
2454                 getMouseRelative( event, tracker.element ),
2455                 delegate.buttonDown, 
2456                 IS_BUTTON_DOWN
2457             );
2458 
2459             if( propagate === false ){
2460                 $.cancelEvent( event );
2461             }
2462         }
2463     };
2464 
2465 
2466     /**
2467      * @private
2468      * @inner
2469      */
2470     function onMouseDown( tracker, event ) {
2471         var event = $.getEvent( event ),
2472             delegate = THIS[ tracker.hash ],
2473             propagate;
2474 
2475         if ( event.button == 2 ) {
2476             return;
2477         }
2478 
2479         delegate.buttonDown = true;
2480 
2481         delegate.lastPoint = getMouseAbsolute( event );
2482         delegate.lastMouseDownPoint = delegate.lastPoint;
2483         delegate.lastMouseDownTime = +new Date();
2484 
2485         if ( tracker.pressHandler ) {
2486             propagate = tracker.pressHandler( 
2487                 tracker, 
2488                 getMouseRelative( event, tracker.element )
2489             );
2490             if( propagate === false ){
2491                 $.cancelEvent( event );
2492             }
2493         }
2494 
2495         if ( tracker.pressHandler || tracker.dragHandler ) {
2496             $.cancelEvent( event );
2497         }
2498 
2499         if ( !( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) || 
2500              !IS_CAPTURING ) {
2501             captureMouse( tracker );
2502             IS_CAPTURING = true;
2503             // reset to empty & add us
2504             CAPTURING = [ tracker ];     
2505         } else if ( $.Browser.vendor == $.BROWSERS.IE  && $.Browser.version < 9 ) {
2506             // add us to the list
2507             CAPTURING.push( tracker );   
2508         }
2509     };
2510 
2511     /**
2512      * @private
2513      * @inner
2514      */
2515     function onTouchStart( tracker, event ) {
2516         var touchA,
2517             touchB;
2518 
2519         if( event.touches.length == 1 &&
2520             event.targetTouches.length == 1 && 
2521             event.changedTouches.length == 1 ){
2522             
2523             THIS[ tracker.hash ].lastTouch = event.touches[ 0 ];  
2524             onMouseOver( tracker, event.changedTouches[ 0 ] );
2525             onMouseDown( tracker, event.touches[ 0 ] );
2526         }
2527 
2528         if( event.touches.length == 2 ){
2529             
2530             touchA = getMouseAbsolute( event.touches[ 0 ] );
2531             touchB = getMouseAbsolute( event.touches[ 1 ] );
2532             THIS[ tracker.hash ].lastPinchDelta = 
2533                 Math.abs( touchA.x - touchB.x ) +
2534                 Math.abs( touchA.y - touchB.y );
2535             //$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta);
2536         }
2537 
2538         event.preventDefault();
2539     };
2540 
2541 
2542     /**
2543      * @private
2544      * @inner
2545      */
2546     function onMouseUp( tracker, event ) {
2547         var event = $.getEvent( event ),
2548             delegate = THIS[ tracker.hash ],
2549             //were we inside the tracked element when we were pressed
2550             insideElementPress = delegate.buttonDown,
2551             //are we still inside the tracked element when we released
2552             insideElementRelease = delegate.insideElement,
2553             propagate;
2554 
2555         if ( event.button == 2 ) {
2556             return;
2557         }
2558 
2559         delegate.buttonDown = false;
2560 
2561         if ( tracker.releaseHandler ) {
2562             propagate = tracker.releaseHandler(
2563                 tracker, 
2564                 getMouseRelative( event, tracker.element ),
2565                 insideElementPress, 
2566                 insideElementRelease
2567             );
2568             if( propagate === false ){
2569                 $.cancelEvent( event );
2570             }
2571         }
2572 
2573         if ( insideElementPress && insideElementRelease ) {
2574             handleMouseClick( tracker, event );
2575         }
2576     };
2577 
2578 
2579     /**
2580      * @private
2581      * @inner
2582      */
2583     function onTouchEnd( tracker, event ) {
2584 
2585         if( event.touches.length == 0 &&
2586             event.targetTouches.length == 0 && 
2587             event.changedTouches.length == 1 ){
2588 
2589             THIS[ tracker.hash ].lastTouch = null;
2590             onMouseUp( tracker, event.changedTouches[ 0 ] );
2591             onMouseOut( tracker, event.changedTouches[ 0 ] );
2592         }
2593         if( event.touches.length + event.changedTouches.length == 2 ){
2594             THIS[ tracker.hash ].lastPinchDelta = null;
2595             //$.console.debug("pinch end");
2596         }
2597         event.preventDefault();
2598     };
2599 
2600 
2601     /**
2602      * Only triggered once by the deepest element that initially received
2603      * the mouse down event. We want to make sure THIS event doesn't bubble.
2604      * Instead, we want to trigger the elements that initially received the
2605      * mouse down event (including this one) only if the mouse is no longer
2606      * inside them. Then, we want to release capture, and emulate a regular
2607      * mouseup on the event that this event was meant for.
2608      * @private
2609      * @inner
2610      */
2611     function onMouseUpIE( tracker, event ) {
2612         var event = $.getEvent( event ),
2613             othertracker,
2614             i;
2615 
2616         if ( event.button == 2 ) {
2617             return;
2618         }
2619 
2620         for ( i = 0; i < CAPTURING.length; i++ ) {
2621             othertracker = CAPTURING[ i ];
2622             if ( !hasMouse( othertracker ) ) {
2623                 onMouseUp( othertracker, event );
2624             }
2625         }
2626 
2627         releaseMouse( tracker );
2628         IS_CAPTURING = false;
2629         event.srcElement.fireEvent(
2630             "on" + event.type,
2631             document.createEventObject( event )
2632         );
2633 
2634         $.stopEvent( event );
2635     };
2636 
2637 
2638     /**
2639      * Only triggered in W3C browsers by elements within which the mouse was
2640      * initially pressed, since they are now listening to the window for
2641      * mouseup during the capture phase. We shouldn't handle the mouseup
2642      * here if the mouse is still inside this element, since the regular
2643      * mouseup handler will still fire.
2644      * @private
2645      * @inner
2646      */
2647     function onMouseUpWindow( tracker, event ) {
2648         if ( ! THIS[ tracker.hash ].insideElement ) {
2649             onMouseUp( tracker, event );
2650         }
2651         releaseMouse( tracker );
2652     };
2653 
2654 
2655     /**
2656      * @private
2657      * @inner
2658      */
2659     function onMouseClick( tracker, event ) {
2660         if ( tracker.clickHandler ) {
2661             $.cancelEvent( event );
2662         }
2663     };
2664 
2665 
2666     /**
2667      * @private
2668      * @inner
2669      */
2670     function onMouseWheelSpin( tracker, event ) {
2671         var nDelta = 0,
2672             propagate;
2673         
2674         if ( !event ) { // For IE, access the global (window) event object
2675             event = window.event;
2676         }
2677 
2678         if ( event.wheelDelta ) { // IE and Opera
2679             nDelta = event.wheelDelta;
2680             if ( window.opera ) {  // Opera has the values reversed
2681                 nDelta = -nDelta;
2682             }
2683         } else if (event.detail) { // Mozilla FireFox
2684             nDelta = -event.detail;
2685         }
2686         //The nDelta variable is gated to provide smooth z-index scrolling
2687         //since the mouse wheel allows for substantial deltas meant for rapid
2688         //y-index scrolling.
2689         nDelta = nDelta > 0 ? 1 : -1;
2690 
2691         if ( tracker.scrollHandler ) {
2692             propagate = tracker.scrollHandler(
2693                 tracker, 
2694                 getMouseRelative( event, tracker.element ), 
2695                 nDelta, 
2696                 event.shiftKey
2697             );
2698             if( propagate === false ){
2699                 $.cancelEvent( event );
2700             }
2701         }
2702     };
2703 
2704 
2705     /**
2706      * @private
2707      * @inner
2708      */
2709     function handleMouseClick( tracker, event ) {
2710         var event = $.getEvent( event ),
2711             delegate = THIS[ tracker.hash ],
2712             propagate;
2713 
2714         if ( event.button == 2 ) {
2715             return;
2716         }
2717 
2718         var time     = +new Date() - delegate.lastMouseDownTime,
2719             point    = getMouseAbsolute( event ),
2720             distance = delegate.lastMouseDownPoint.distanceTo( point ),
2721             quick    = time     <= tracker.clickTimeThreshold && 
2722                        distance <= tracker.clickDistThreshold;
2723 
2724         if ( tracker.clickHandler ) {
2725             propagate = tracker.clickHandler(
2726                 tracker, 
2727                 getMouseRelative( event, tracker.element ),
2728                 quick, 
2729                 event.shiftKey
2730             );
2731             if( propagate === false ){
2732                 $.cancelEvent( event );
2733             }
2734         }
2735     };
2736 
2737 
2738     /**
2739      * @private
2740      * @inner
2741      */
2742     function onMouseMove( tracker, event ) {
2743         var event = $.getEvent( event ),
2744             delegate = THIS[ tracker.hash ],
2745             point = getMouseAbsolute( event ),
2746             delta = point.minus( delegate.lastPoint ),
2747             propagate;
2748 
2749         delegate.lastPoint = point;
2750 
2751         if ( tracker.dragHandler ) {
2752             propagate = tracker.dragHandler(
2753                 tracker, 
2754                 getMouseRelative( event, tracker.element ),
2755                 delta, 
2756                 event.shiftKey
2757             );
2758             if( propagate === false ){
2759                 $.cancelEvent( event );
2760             }
2761         }
2762     };
2763 
2764 
2765     /**
2766      * @private
2767      * @inner
2768      */
2769     function onTouchMove( tracker, event ) {
2770         var touchA,
2771             touchB,
2772             pinchDelta;
2773 
2774         if( event.touches.length === 1 &&
2775             event.targetTouches.length === 1 && 
2776             event.changedTouches.length === 1 && 
2777             THIS[ tracker.hash ].lastTouch === event.touches[ 0 ]){
2778 
2779             onMouseMove( tracker, event.touches[ 0 ] );
2780 
2781         } else if (  event.touches.length === 2 ){
2782 
2783             touchA = getMouseAbsolute( event.touches[ 0 ] );
2784             touchB = getMouseAbsolute( event.touches[ 1 ] );
2785             pinchDelta =
2786                 Math.abs( touchA.x - touchB.x ) +
2787                 Math.abs( touchA.y - touchB.y );
2788             
2789             //TODO: make the 75px pinch threshold configurable
2790             if( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ){
2791                 //$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta);
2792 
2793                 onMouseWheelSpin( tracker, {
2794                     shift: false,
2795                     pageX: ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2,
2796                     pageY: ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2,
2797                     detail:( 
2798                         THIS[ tracker.hash ].lastPinchDelta > pinchDelta 
2799                     ) ? 1 : -1
2800                 });
2801 
2802                 THIS[ tracker.hash ].lastPinchDelta = pinchDelta;
2803             }
2804         }
2805         event.preventDefault();
2806     };
2807 
2808     /**
2809      * Only triggered once by the deepest element that initially received
2810      * the mouse down event. Since no other element has captured the mouse,
2811      * we want to trigger the elements that initially received the mouse
2812      * down event (including this one). The the param tracker isn't used
2813      * but for consistency with the other event handlers we include it.
2814      * @private
2815      * @inner
2816      */
2817     function onMouseMoveIE( tracker, event ) {
2818         var i;
2819         for ( i = 0; i < CAPTURING.length; i++ ) {
2820             onMouseMove( CAPTURING[ i ], event );
2821         }
2822 
2823         $.stopEvent( event );
2824     };
2825 
2826     /**
2827      * @private
2828      * @inner
2829      */
2830     function getMouseAbsolute( event ) {
2831         return $.getMousePosition( event );
2832     };
2833 
2834     /**
2835     * @private
2836     * @inner
2837     */
2838     function getMouseRelative( event, element ) {
2839         var mouse   = $.getMousePosition( event ),
2840             offset  = $.getElementPosition( element );
2841 
2842         return mouse.minus( offset );
2843     };
2844 
2845     /**
2846     * @private
2847     * @inner
2848     * Returns true if elementB is a child node of elementA, or if they're equal.
2849     */
2850     function isChild( elementA, elementB ) {
2851         var body = document.body;
2852         while ( elementB && elementA != elementB && body != elementB ) {
2853             try {
2854                 elementB = elementB.parentNode;
2855             } catch (e) {
2856                 return false;
2857             }
2858         }
2859         return elementA == elementB;
2860     };
2861 
2862     /**
2863     * @private
2864     * @inner
2865     */
2866     function onGlobalMouseDown() {
2867         IS_BUTTON_DOWN = true;
2868     };
2869 
2870     /**
2871     * @private
2872     * @inner
2873     */
2874     function onGlobalMouseUp() {
2875         IS_BUTTON_DOWN = false;
2876     };
2877 
2878 
2879     (function () {
2880         if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
2881             $.addEvent( document, "mousedown", onGlobalMouseDown, false );
2882             $.addEvent( document, "mouseup", onGlobalMouseUp, false );
2883         } else {
2884             $.addEvent( window, "mousedown", onGlobalMouseDown, true );
2885             $.addEvent( window, "mouseup", onGlobalMouseUp, true );
2886         }
2887     })();
2888     
2889 }( OpenSeadragon ));
2890 
2891 (function( $ ){
2892     
2893 /**
2894  * An enumeration of supported locations where controls can be anchored,
2895  * including NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, and BOTTOM_LEFT.
2896  * The anchoring is always relative to the container
2897  * @static
2898  */
2899 $.ControlAnchor = {
2900     NONE: 0,
2901     TOP_LEFT: 1,
2902     TOP_RIGHT: 2,
2903     BOTTOM_RIGHT: 3,
2904     BOTTOM_LEFT: 4
2905 };
2906 
2907 /**
2908  * A Control represents any interface element which is meant to allow the user 
2909  * to interact with the zoomable interface. Any control can be anchored to any 
2910  * element.
2911  * @class
2912  * @param {Element} element - the contol element to be anchored in the container.
2913  * @param {OpenSeadragon.ControlAnchor} anchor - the location to anchor at.
2914  * @param {Element} container - the element to control will be anchored too.
2915  * 
2916  * @property {Element} element - the element providing the user interface with 
2917  *  some type of control. Eg a zoom-in button
2918  * @property {OpenSeadragon.ControlAnchor} anchor - the position of the control 
2919  *  relative to the container.
2920  * @property {Element} container - the element within with the control is 
2921  *  positioned.
2922  * @property {Element} wrapper - a nuetral element surrounding the control 
2923  *  element.
2924  */
2925 $.Control = function ( element, anchor, container ) {
2926     this.element    = element;
2927     this.anchor     = anchor;
2928     this.container  = container;
2929     this.wrapper    = $.makeNeutralElement( "span" );
2930     this.wrapper.style.display = "inline-block";
2931     this.wrapper.appendChild( this.element );
2932 
2933     if ( this.anchor == $.ControlAnchor.NONE ) {
2934         // IE6 fix
2935         this.wrapper.style.width = this.wrapper.style.height = "100%";    
2936     }
2937 
2938     if ( this.anchor == $.ControlAnchor.TOP_RIGHT || 
2939          this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) {
2940         this.container.insertBefore( 
2941             this.wrapper, 
2942             this.container.firstChild 
2943         );
2944     } else {
2945         this.container.appendChild( this.wrapper );
2946     }
2947 };
2948 
2949 $.Control.prototype = {
2950 
2951     /**
2952      * Removes the control from the container.
2953      * @function
2954      */
2955     destroy: function() {
2956         this.wrapper.removeChild( this.element );
2957         this.container.removeChild( this.wrapper );
2958     },
2959 
2960     /**
2961      * Determines if the control is currently visible.
2962      * @function
2963      * @return {Boolean} true if currenly visible, false otherwise.
2964      */
2965     isVisible: function() {
2966         return this.wrapper.style.display != "none";
2967     },
2968 
2969     /**
2970      * Toggles the visibility of the control.
2971      * @function
2972      * @param {Boolean} visible - true to make visible, false to hide.
2973      */
2974     setVisible: function( visible ) {
2975         this.wrapper.style.display = visible ? 
2976             "inline-block" : 
2977             "none";
2978     },
2979 
2980     /**
2981      * Sets the opacity level for the control.
2982      * @function
2983      * @param {Number} opactiy - a value between 1 and 0 inclusively.
2984      */
2985     setOpacity: function( opacity ) {
2986         if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) {
2987             $.setElementOpacity( this.element, opacity, true );
2988         } else {
2989             $.setElementOpacity( this.wrapper, opacity, true );
2990         }
2991     }
2992 };
2993 
2994 }( OpenSeadragon ));
2995 (function( $ ){
2996 
2997     //id hash for private properties;
2998     var THIS = {};
2999     
3000     /**
3001      * @class
3002      */
3003     $.ControlDock = function( options ){
3004         var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'],
3005             layout,
3006             i;
3007         
3008         $.extend( true, this, {
3009             id: 'controldock-'+(+new Date())+'-'+Math.floor(Math.random()*1000000),
3010             container: $.makeNeutralElement('form'),
3011             controls: []
3012         }, options );
3013 
3014         if( this.element ){
3015             this.element = $.getElement( this.element );
3016             this.element.appendChild( this.container );   
3017             this.element.style.position = 'relative'; 
3018             this.container.style.width = '100%';
3019             this.container.style.height = '100%';
3020         }
3021 
3022         for( i = 0; i < layouts.length; i++ ){
3023             layout = layouts[ i ];
3024             this.controls[ layout ] = $.makeNeutralElement( "div" );
3025             this.controls[ layout ].style.position = 'absolute';
3026             if ( layout.match( 'left' ) ){
3027                 this.controls[ layout ].style.left = '0px';
3028             }
3029             if ( layout.match( 'right' ) ){
3030                 this.controls[ layout ].style.right = '0px';
3031             }
3032             if ( layout.match( 'top' ) ){
3033                 this.controls[ layout ].style.top = '0px';
3034             }
3035             if ( layout.match( 'bottom' ) ){
3036                 this.controls[ layout ].style.bottom = '0px';
3037             }
3038         }
3039 
3040         this.container.appendChild( this.controls.topleft );
3041         this.container.appendChild( this.controls.topright );
3042         this.container.appendChild( this.controls.bottomright );
3043         this.container.appendChild( this.controls.bottomleft );
3044     };
3045 
3046     $.ControlDock.prototype = {
3047 
3048         /**
3049          * @function
3050          */
3051         addControl: function ( element, anchor ) {
3052             var element = $.getElement( element ),
3053                 div = null;
3054 
3055             if ( getControlIndex( this, element ) >= 0 ) {
3056                 return;     // they're trying to add a duplicate control
3057             }
3058 
3059             switch ( anchor ) {
3060                 case $.ControlAnchor.TOP_RIGHT:
3061                     div = this.controls.topright;
3062                     element.style.position = "relative";
3063                     element.style.marginRight = "4px";
3064                     element.style.marginTop = "4px";
3065                     break;
3066                 case $.ControlAnchor.BOTTOM_RIGHT:
3067                     div = this.controls.bottomright;
3068                     element.style.position = "relative";
3069                     element.style.marginRight = "4px";
3070                     element.style.marginBottom = "4px";
3071                     break;
3072                 case $.ControlAnchor.BOTTOM_LEFT:
3073                     div = this.controls.bottomleft;
3074                     element.style.position = "relative";
3075                     element.style.marginLeft = "4px";
3076                     element.style.marginBottom = "4px";
3077                     break;
3078                 case $.ControlAnchor.TOP_LEFT:
3079                     div = this.controls.topleft;
3080                     element.style.position = "relative";
3081                     element.style.marginLeft = "4px";
3082                     element.style.marginTop = "4px";
3083                     break;
3084                 case $.ControlAnchor.NONE:
3085                 default:
3086                     div = this.container;
3087                     element.style.margin = "0px";
3088                     element.style.padding = "0px";
3089                     break;
3090             }
3091 
3092             this.controls.push(
3093                 new $.Control( element, anchor, div )
3094             );
3095             element.style.display = "inline-block";
3096         },
3097 
3098 
3099         /**
3100          * @function
3101          * @return {OpenSeadragon.ControlDock} Chainable.
3102          */
3103         removeControl: function ( element ) {
3104             var element  = $.getElement( element ),
3105                 i        = getControlIndex( this, element );
3106             
3107             if ( i >= 0 ) {
3108                 this.controls[ i ].destroy();
3109                 this.controls.splice( i, 1 );
3110             }
3111 
3112             return this;
3113         },
3114 
3115         /**
3116          * @function
3117          * @return {OpenSeadragon.ControlDock} Chainable.
3118          */
3119         clearControls: function () {
3120             while ( this.controls.length > 0 ) {
3121                 this.controls.pop().destroy();
3122             }
3123             
3124             return this;
3125         },
3126 
3127 
3128         /**
3129          * @function
3130          * @return {Boolean}
3131          */
3132         areControlsEnabled: function () {
3133             var i;
3134             
3135             for ( i = this.controls.length - 1; i >= 0; i-- ) {
3136                 if ( this.controls[ i ].isVisible() ) {
3137                     return true;
3138                 }
3139             }
3140 
3141             return false;
3142         },
3143 
3144 
3145         /**
3146          * @function
3147          * @return {OpenSeadragon.ControlDock} Chainable.
3148          */
3149         setControlsEnabled: function( enabled ) {
3150             var i;
3151 
3152             for ( i = this.controls.length - 1; i >= 0; i-- ) {
3153                 this.controls[ i ].setVisible( enabled );
3154             }
3155 
3156             return this;
3157         }
3158 
3159     };
3160 
3161 
3162     ///////////////////////////////////////////////////////////////////////////////
3163     // Utility methods
3164     ///////////////////////////////////////////////////////////////////////////////
3165     function getControlIndex( dock, element ) {
3166         var controls = dock.controls,
3167             i;
3168 
3169         for ( i = controls.length - 1; i >= 0; i-- ) {
3170             if ( controls[ i ].element == element ) {
3171                 return i;
3172             }
3173         }
3174 
3175         return -1;
3176     };
3177 
3178 }( OpenSeadragon ));
3179 (function( $ ){
3180      
3181 // dictionary from hash to private properties
3182 var THIS = {},
3183 // We keep a list of viewers so we can 'wake-up' each viewer on
3184 // a page after toggling between fullpage modes
3185     VIEWERS = {};  
3186 
3187 /**
3188  *
3189  * The main point of entry into creating a zoomable image on the page.
3190  *
3191  * We have provided an idiomatic javascript constructor which takes
3192  * a single object, but still support the legacy positional arguments.
3193  *
3194  * The options below are given in order that they appeared in the constructor
3195  * as arguments and we translate a positional call into an idiomatic call.
3196  *
3197  * @class
3198  * @extends OpenSeadragon.EventHandler
3199  * @extends OpenSeadragon.ControlDock
3200  * @param {Object} options
3201  * @param {String} options.element Id of Element to attach to,
3202  * @param {String} options.xmlPath  Xpath ( TODO: not sure! ),
3203  * @param {String} options.prefixUrl  Url used to prepend to paths, eg button 
3204  *  images, etc.
3205  * @param {Seadragon.Controls[]} options.controls Array of Seadragon.Controls,
3206  * @param {Seadragon.Overlays[]} options.overlays Array of Seadragon.Overlays,
3207  * @param {Seadragon.Controls[]} options.overlayControls An Array of ( TODO: 
3208  *  not sure! )
3209  *
3210  **/    
3211 $.Viewer = function( options ) {
3212 
3213     var args  = arguments,
3214         _this = this,
3215         i;
3216 
3217 
3218     //backward compatibility for positional args while prefering more 
3219     //idiomatic javascript options object as the only argument
3220     if( !$.isPlainObject( options ) ){
3221         options = {
3222             id:                 args[ 0 ],
3223             xmlPath:            args.length > 1 ? args[ 1 ] : undefined,
3224             prefixUrl:          args.length > 2 ? args[ 2 ] : undefined,
3225             controls:           args.length > 3 ? args[ 3 ] : undefined,
3226             overlays:           args.length > 4 ? args[ 4 ] : undefined,
3227             overlayControls:    args.length > 5 ? args[ 5 ] : undefined
3228         };
3229     }
3230 
3231     //options.config and the general config argument are deprecated
3232     //in favor of the more direct specification of optional settings
3233     //being pass directly on the options object
3234     if ( options.config ){
3235         $.extend( true, options, options.config );
3236         delete options.config;
3237     }
3238     
3239     //Public properties
3240     //Allow the options object to override global defaults
3241     $.extend( true, this, { 
3242 
3243         //internal state and dom identifiers
3244         id:             options.id,
3245         hash:           options.id,
3246 
3247         //dom nodes
3248         element:        null,
3249         canvas:         null,
3250         container:      null,
3251 
3252         //TODO: not sure how to best describe these
3253         overlays:       [],
3254         overlayControls:[],
3255 
3256         //private state properties
3257         previousBody:   [],
3258 
3259         //This was originally initialized in the constructor and so could never
3260         //have anything in it.  now it can because we allow it to be specified
3261         //in the options and is only empty by default if not specified. Also
3262         //this array was returned from get_controls which I find confusing
3263         //since this object has a controls property which is treated in other
3264         //functions like clearControls.  I'm removing the accessors.
3265         customControls: [],
3266 
3267         //These are originally not part options but declared as members
3268         //in initialize.  Its still considered idiomatic to put them here
3269         source:         null,
3270         drawer:         null,
3271         viewport:       null,
3272         navigator:      null, 
3273 
3274         //UI image resources
3275         //TODO: rename navImages to uiImages
3276         navImages:      null,
3277 
3278         //interface button controls
3279         buttons:        null, 
3280 
3281         //TODO: this is defunct so safely remove it
3282         profiler:       null
3283 
3284     }, $.DEFAULT_SETTINGS, options );
3285 
3286     //Private state properties
3287     THIS[ this.hash ] = {
3288         "fsBoundsDelta":     new $.Point( 1, 1 ),
3289         "prevContainerSize": null,
3290         "lastOpenStartTime": 0,
3291         "lastOpenEndTime":   0,
3292         "animating":         false,
3293         "forceRedraw":       false,
3294         "mouseInside":       false,
3295         "group":             null,
3296         // whether we should be continuously zooming
3297         "zooming":           false,
3298         // how much we should be continuously zooming by
3299         "zoomFactor":        null,  
3300         "lastZoomTime":      null,
3301         // did we decide this viewer has a sequence of tile sources
3302         "sequenced":         false,
3303         "sequence":          0
3304     };
3305 
3306     //Inherit some behaviors and properties
3307     $.EventHandler.call( this );
3308     $.ControlDock.call( this, options );
3309 
3310     //Deal with tile sources
3311     var initialTileSource,
3312         customTileSource;
3313 
3314     if ( this.xmlPath  ){
3315         //Deprecated option.  Now it is preferred to use the tileSources option
3316         this.tileSources = [ this.xmlPath ];
3317     }
3318 
3319     if ( this.tileSources  ){
3320         //tileSources is a complex option...
3321         //It can be a string, object, function, or an array of any of these.
3322         // - A String implies a DZI
3323         // - An Srray of Objects implies a simple image
3324         // - A Function implies a custom tile source callback
3325         // - An Array that is not an Array of simple Objects implies a sequence 
3326         //   of tile sources which can be any of the above
3327         if( $.isArray( this.tileSources ) ){
3328             if( $.isPlainObject( this.tileSources[ 0 ] ) ){
3329                 //This is a non-sequenced legacy tile source
3330                 initialTileSource = this.tileSources;
3331             } else {
3332                 //Sequenced tile source
3333                 initialTileSource = this.tileSources[ 0 ];
3334                 THIS[ this.hash ].sequenced = true;
3335             }
3336         } else {
3337             initialTileSource = this.tileSources;
3338         }
3339 
3340         this.openTileSource( initialTileSource );
3341     }
3342 
3343     this.element        = this.element || document.getElementById( this.id );
3344     this.canvas         = $.makeNeutralElement( "div" );
3345 
3346     (function( canvas ){
3347         canvas.width    = "100%";
3348         canvas.height   = "100%";
3349         canvas.overflow = "hidden";
3350         canvas.position = "absolute";
3351         canvas.top      = "0px";
3352         canvas.left     = "0px";
3353     }(  this.canvas.style ));
3354 
3355     (function( container ){
3356         container.width     = "100%";
3357         container.height    = "100%";
3358         container.position  = "relative";
3359         container.left      = "0px";
3360         container.top       = "0px";
3361         container.textAlign = "left";  // needed to protect against
3362     }( this.container.style ));
3363 
3364     this.container.insertBefore( this.canvas, this.container.firstChild );
3365     this.element.appendChild( this.container );
3366 
3367     //Used for toggling between fullscreen and default container size
3368     //TODO: these can be closure private and shared across Viewer
3369     //      instances.
3370     this.bodyWidth      = document.body.style.width;
3371     this.bodyHeight     = document.body.style.height;
3372     this.bodyOverflow   = document.body.style.overflow;
3373     this.docOverflow    = document.documentElement.style.overflow;
3374 
3375     this.innerTracker = new $.MouseTracker({
3376         element:            this.canvas, 
3377         clickTimeThreshold: this.clickTimeThreshold, 
3378         clickDistThreshold: this.clickDistThreshold,
3379         clickHandler:       $.delegate( this, onCanvasClick ),
3380         dragHandler:        $.delegate( this, onCanvasDrag ),
3381         releaseHandler:     $.delegate( this, onCanvasRelease ),
3382         scrollHandler:      $.delegate( this, onCanvasScroll )
3383     }).setTracking( this.mouseNavEnabled ? true : false ); // default state
3384 
3385     this.outerTracker = new $.MouseTracker({
3386         element:            this.container, 
3387         clickTimeThreshold: this.clickTimeThreshold, 
3388         clickDistThreshold: this.clickDistThreshold,
3389         enterHandler:       $.delegate( this, onContainerEnter ),
3390         exitHandler:        $.delegate( this, onContainerExit ),
3391         releaseHandler:     $.delegate( this, onContainerRelease )
3392     }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking
3393     
3394     if( this.toolbar ){
3395         this.toolbar = new $.ControlDock({ element: this.toolbar });
3396     }
3397     
3398     this.bindStandardControls();
3399     this.bindSequenceControls();
3400 
3401     for ( i = 0; i < this.customControls.length; i++ ) {
3402         this.addControl(
3403             this.customControls[ i ].id, 
3404             this.customControls[ i ].anchor
3405         );
3406     }
3407 
3408     window.setTimeout( function(){
3409         beginControlsAutoHide( _this );
3410     }, 1 );    // initial fade out
3411 
3412 };
3413 
3414 $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, {
3415 
3416 
3417     /**
3418      * @function
3419      * @name OpenSeadragon.Viewer.prototype.isOpen
3420      * @return {Boolean}
3421      */
3422     isOpen: function () {
3423         return !!this.source;
3424     },
3425 
3426     /**
3427      * If the string is xml is simply parsed and opened, otherwise the string 
3428      * is treated as an URL and an xml document is requested via ajax, parsed 
3429      * and then opened in the viewer.
3430      * @function
3431      * @name OpenSeadragon.Viewer.prototype.openDzi
3432      * @param {String} dzi and xml string or the url to a DZI xml document.
3433      * @return {OpenSeadragon.Viewer} Chainable.
3434      */
3435     openDzi: function ( dzi ) {
3436         var _this = this;
3437         $.createFromDZI(
3438             dzi,
3439             function( source ){
3440                _this.open( source );
3441             },
3442             this.tileHost
3443         );
3444         return this;
3445     },
3446 
3447     /**
3448      * @function
3449      * @name OpenSeadragon.Viewer.prototype.openTileSource
3450      * @return {OpenSeadragon.Viewer} Chainable.
3451      */
3452     openTileSource: function ( tileSource ) {
3453         var _this = this,
3454             customTileSource;
3455 
3456         setTimeout(function(){
3457             if ( $.type( tileSource ) == 'string') {
3458                 //Standard DZI format
3459                 _this.openDzi( tileSource );
3460             } else if ( $.isArray( tileSource ) ){
3461                 //Legacy image pyramid
3462                 _this.open( new $.LegacyTileSource( tileSource ) );
3463             } else if ( $.isPlainObject( tileSource ) && $.isFunction( tileSource.getTileUrl ) ){
3464                 //Custom tile source
3465                 customTileSource = new $.TileSource(
3466                     tileSource.width,
3467                     tileSource.height,
3468                     tileSource.tileSize,
3469                     tileSource.tileOverlap,
3470                     tileSource.minLevel,
3471                     tileSource.maxLevel
3472                 );
3473                 customTileSource.getTileUrl = tileSource.getTileUrl;
3474                 _this.open( customTileSource );
3475             } else {
3476                 //can assume it's already a tile source implementation
3477                 _this.open( tileSource );
3478             }
3479         }, 1);
3480 
3481         return this;
3482     },
3483 
3484     /**
3485      * @function
3486      * @name OpenSeadragon.Viewer.prototype.open
3487      * @return {OpenSeadragon.Viewer} Chainable.
3488      */
3489     open: function( source ) {
3490         var _this = this,
3491             overlay,
3492             i;
3493 
3494         if ( this.source ) {
3495             this.close( );
3496         }
3497         
3498         // to ignore earlier opens
3499         THIS[ this.hash ].lastOpenStartTime = +new Date();
3500 
3501         window.setTimeout( function () {
3502             if ( THIS[ _this.hash ].lastOpenStartTime > THIS[ _this.hash ].lastOpenEndTime ) {
3503                 THIS[ _this.hash ].setMessage( $.getString( "Messages.Loading" ) );
3504             }
3505         }, 2000);
3506 
3507         THIS[ this.hash ].lastOpenEndTime = +new Date();
3508         this.canvas.innerHTML = "";
3509         THIS[ this.hash ].prevContainerSize = $.getElementSize( this.container );
3510 
3511         if( source ){
3512             this.source = source;
3513         }
3514 
3515         this.viewport = this.viewport ? this.viewport : new $.Viewport({
3516             containerSize:      THIS[ this.hash ].prevContainerSize, 
3517             contentSize:        this.source.dimensions, 
3518             springStiffness:    this.springStiffness,
3519             animationTime:      this.animationTime,
3520             minZoomImageRatio:  this.minZoomImageRatio,
3521             maxZoomPixelRatio:  this.maxZoomPixelRatio,
3522             visibilityRatio:    this.visibilityRatio,
3523             wrapHorizontal:     this.wrapHorizontal,
3524             wrapVertical:       this.wrapVertical
3525         });
3526         if( this.preserveVewport ){
3527             this.viewport.resetContentSize( this.source.dimensions );
3528         }
3529 
3530         this.drawer = new $.Drawer({
3531             source:             this.source, 
3532             viewport:           this.viewport, 
3533             element:            this.canvas,
3534             overlays:           this.overlays,
3535             maxImageCacheCount: this.maxImageCacheCount,
3536             imageLoaderLimit:   this.imageLoaderLimit,
3537             minZoomImageRatio:  this.minZoomImageRatio,
3538             wrapHorizontal:     this.wrapHorizontal,
3539             wrapVertical:       this.wrapVertical,
3540             immediateRender:    this.immediateRender,
3541             blendTime:          this.blendTime,
3542             alwaysBlend:        this.alwaysBlend,
3543             minPixelRatio:      this.minPixelRatio
3544         });
3545 
3546         //Instantiate a navigator if configured
3547         if ( this.showNavigator  && ! this.navigator ){
3548             this.navigator = new $.Navigator({
3549                 id:          this.navigatorElement,
3550                 position:    this.navigatorPosition,
3551                 sizeRatio:   this.navigatorSizeRatio,
3552                 height:      this.navigatorHeight,
3553                 width:       this.navigatorWidth,
3554                 tileSources: this.tileSources,
3555                 tileHost:    this.tileHost,
3556                 prefixUrl:   this.prefixUrl,
3557                 overlays:    this.overlays,
3558                 viewer:      this
3559             });
3560         }
3561 
3562         //this.profiler = new $.Profiler();
3563 
3564         THIS[ this.hash ].animating = false;
3565         THIS[ this.hash ].forceRedraw = true;
3566         scheduleUpdate( this, updateMulti );
3567 
3568         //Assuming you had programatically created a bunch of overlays
3569         //and added them via configuration
3570         for ( i = 0; i < this.overlayControls.length; i++ ) {
3571             
3572             overlay = this.overlayControls[ i ];
3573             
3574             if ( overlay.point != null ) {
3575             
3576                 this.drawer.addOverlay(
3577                     overlay.id, 
3578                     new $.Point( 
3579                         overlay.point.X, 
3580                         overlay.point.Y 
3581                     ), 
3582                     $.OverlayPlacement.TOP_LEFT
3583                 );
3584             
3585             } else {
3586             
3587                 this.drawer.addOverlay(
3588                     overlay.id, 
3589                     new $.Rect(
3590                         overlay.rect.Point.X, 
3591                         overlay.rect.Point.Y, 
3592                         overlay.rect.Width, 
3593                         overlay.rect.Height
3594                     ), 
3595                     overlay.placement
3596                 );
3597             
3598             }
3599         }
3600         VIEWERS[ this.hash ] = this;
3601         this.raiseEvent( "open" );
3602 
3603         if( this.navigator ){
3604             this.navigator.open( source );
3605         }
3606 
3607         return this;
3608     },
3609 
3610     /**
3611      * @function
3612      * @name OpenSeadragon.Viewer.prototype.close
3613      * @return {OpenSeadragon.Viewer} Chainable.
3614      */
3615     close: function ( ) {
3616         this.source     = null;
3617         this.drawer     = null;
3618         this.viewport   = this.preserveViewport ? this.viewport : null;
3619         //this.profiler   = null;
3620         this.canvas.innerHTML = "";
3621 
3622         VIEWERS[ this.hash ] = null;
3623         delete VIEWERS[ this.hash ];
3624         
3625         return this;
3626     },
3627 
3628 
3629     /**
3630      * @function
3631      * @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled
3632      * @return {Boolean}
3633      */
3634     isMouseNavEnabled: function () {
3635         return this.innerTracker.isTracking();
3636     },
3637 
3638     /**
3639      * @function
3640      * @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled
3641      * @return {OpenSeadragon.Viewer} Chainable.
3642      */
3643     setMouseNavEnabled: function( enabled ){
3644         this.innerTracker.setTracking( enabled );
3645         return this;
3646     },
3647 
3648 
3649     /**
3650      * @function
3651      * @name OpenSeadragon.Viewer.prototype.isDashboardEnabled
3652      * @return {Boolean}
3653      */
3654     areControlsEnabled: function () {
3655         return this.controls.length && this.controls[ i ].isVisibile();
3656     },
3657 
3658 
3659     /**
3660      * @function
3661      * @name OpenSeadragon.Viewer.prototype.setDashboardEnabled
3662      * @return {OpenSeadragon.Viewer} Chainable.
3663      */
3664     setControlsEnabled: function( enabled ) {
3665         if( enabled ){
3666             abortControlsAutoHide( this );
3667         } else {
3668             beginControlsAutoHide( this );
3669         };
3670     },
3671 
3672     
3673     /**
3674      * @function
3675      * @name OpenSeadragon.Viewer.prototype.isFullPage
3676      * @return {Boolean}
3677      */
3678     isFullPage: function () {
3679         return this.container.parentNode == document.body;
3680     },
3681 
3682 
3683     /**
3684      * Toggle full page mode.
3685      * @function
3686      * @name OpenSeadragon.Viewer.prototype.setFullPage
3687      * @param {Boolean} fullPage
3688      *      If true, enter full page mode.  If false, exit full page mode.
3689      * @return {OpenSeadragon.Viewer} Chainable.
3690      */
3691     setFullPage: function( fullPage ) {
3692 
3693         var body            = document.body,
3694             bodyStyle       = body.style,
3695             docStyle        = document.documentElement.style,
3696             containerStyle  = this.container.style,
3697             canvasStyle     = this.canvas.style,
3698             oldBounds,
3699             newBounds,
3700             viewer,
3701             hash,
3702             nodes,
3703             i;
3704         
3705         //dont bother modifying the DOM if we are already in full page mode.
3706         if ( fullPage == this.isFullPage() ) {
3707             return;
3708         }
3709 
3710         if ( fullPage ) {
3711             
3712             this.bodyOverflow   = bodyStyle.overflow;
3713             this.docOverflow    = docStyle.overflow;
3714             bodyStyle.overflow  = "hidden";
3715             docStyle.overflow   = "hidden";
3716 
3717             this.bodyWidth      = bodyStyle.width;
3718             this.bodyHeight     = bodyStyle.height;
3719             bodyStyle.width     = "100%";
3720             bodyStyle.height    = "100%";
3721 
3722             canvasStyle.backgroundColor = "black";
3723             canvasStyle.color           = "white";
3724 
3725             containerStyle.position = "fixed";
3726 
3727             //when entering full screen on the ipad it wasnt sufficient to leave
3728             //the body intact as only only the top half of the screen would 
3729             //respond to touch events on the canvas, while the bottom half treated
3730             //them as touch events on the document body.  Thus we remove and store
3731             //the bodies elements and replace them when we leave full screen.
3732             this.previousBody = [];
3733             nodes = body.childNodes.length;
3734             for ( i = 0; i < nodes; i ++ ){
3735                 this.previousBody.push( body.childNodes[ 0 ] );
3736                 body.removeChild( body.childNodes[ 0 ] );
3737             }
3738             
3739             //If we've got a toolbar, we need to enable the user to use css to
3740             //preserve it in fullpage mode
3741             if( this.toolbar && this.toolbar.element ){
3742                 //save a reference to the parent so we can put it back
3743                 //in the long run we need a better strategy
3744                 this.toolbar.parentNode = this.toolbar.element.parentNode;
3745                 this.toolbar.nextSibling = this.toolbar.element.nextSibling;
3746                 body.appendChild( this.toolbar.element );
3747 
3748                 //Make sure the user has some ability to style the toolbar based
3749                 //on the mode
3750                 this.toolbar.element.setAttribute( 
3751                     'class',
3752                     this.toolbar.element.className +" fullpage"
3753                 );
3754                 this.toolbar.element.style.position = 'fixed';
3755 
3756                 this.container.style.top = $.getElementSize(
3757                     this.toolbar.element
3758                 ).y + 'px';
3759             }
3760             body.appendChild( this.container );
3761             THIS[ this.hash ].prevContainerSize = $.getWindowSize();
3762 
3763             // mouse will be inside container now
3764             $.delegate( this, onContainerEnter )();    
3765 
3766 
3767         } else {
3768             
3769             bodyStyle.overflow  = this.bodyOverflow;
3770             docStyle.overflow   = this.docOverflow;
3771 
3772             bodyStyle.width     = this.bodyWidth;
3773             bodyStyle.height    = this.bodyHeight;
3774 
3775             canvasStyle.backgroundColor = "";
3776             canvasStyle.color           = "";
3777 
3778             containerStyle.position = "relative";
3779             containerStyle.zIndex   = "";
3780 
3781             //If we've got a toolbar, we need to enable the user to use css to
3782             //reset it to its original state 
3783             if( this.toolbar && this.toolbar.element ){
3784                 body.removeChild( this.toolbar.element );
3785 
3786                 //Make sure the user has some ability to style the toolbar based
3787                 //on the mode
3788                 this.toolbar.element.setAttribute( 
3789                     'class',
3790                     this.toolbar.element.className.replace('fullpage','')
3791                 );
3792                 this.toolbar.element.style.position = 'relative';
3793                 this.toolbar.parentNode.insertBefore( 
3794                     this.toolbar.element,
3795                     this.toolbar.nextSibling
3796                 );
3797                 delete this.toolbar.parentNode;
3798                 delete this.toolbar.nextSibling;
3799 
3800                 this.container.style.top = 'auto';
3801             }
3802 
3803             body.removeChild( this.container );
3804             nodes = this.previousBody.length;
3805             for ( i = 0; i < nodes; i++ ){
3806                 body.appendChild( this.previousBody.shift() );
3807             }
3808             this.element.appendChild( this.container );
3809             THIS[ this.hash ].prevContainerSize = $.getElementSize( this.element );
3810             
3811             // mouse will likely be outside now
3812             $.delegate( this, onContainerExit )();      
3813 
3814         }
3815 
3816         if ( this.viewport ) {
3817             oldBounds = this.viewport.getBounds();
3818             this.viewport.resize( THIS[ this.hash ].prevContainerSize );
3819             newBounds = this.viewport.getBounds();
3820 
3821             if ( fullPage ) {
3822                 THIS[ this.hash ].fsBoundsDelta = new $.Point(
3823                     newBounds.width  / oldBounds.width,
3824                     newBounds.height / oldBounds.height
3825                 );
3826             } else {
3827                 this.viewport.update();
3828                 this.viewport.zoomBy(
3829                     Math.max( 
3830                         THIS[ this.hash ].fsBoundsDelta.x, 
3831                         THIS[ this.hash ].fsBoundsDelta.y 
3832                     ),
3833                     null, 
3834                     true
3835                 );
3836                 //Ensures that if multiple viewers are on a page, the viewers that
3837                 //were hidden during fullpage are 'reopened'
3838                 for( hash in VIEWERS ){
3839                     viewer = VIEWERS[ hash ];
3840                     if( viewer !== this && viewer != this.navigator ){
3841                         viewer.open( viewer.source );
3842                         if( viewer.navigator ){
3843                             viewer.navigator.open( viewer.source );
3844                         }
3845                     }
3846                 }
3847             }
3848 
3849             THIS[ this.hash ].forceRedraw = true;
3850             this.raiseEvent( "resize", this );
3851             updateOnce( this );
3852 
3853         }
3854         return this;
3855     },
3856 
3857     
3858     /**
3859      * @function
3860      * @name OpenSeadragon.Viewer.prototype.isVisible
3861      * @return {Boolean}
3862      */
3863     isVisible: function () {
3864         return this.container.style.visibility != "hidden";
3865     },
3866 
3867 
3868     /**
3869      * @function
3870      * @name OpenSeadragon.Viewer.prototype.setVisible
3871      * @return {OpenSeadragon.Viewer} Chainable.
3872      */
3873     setVisible: function( visible ){
3874         this.container.style.visibility = visible ? "" : "hidden";
3875         return this;
3876     },
3877 
3878     bindSequenceControls: function(){
3879         
3880         //////////////////////////////////////////////////////////////////////////
3881         // Image Sequence Controls
3882         //////////////////////////////////////////////////////////////////////////
3883         var onFocusHandler          = $.delegate( this, onFocus ),
3884             onBlurHandler           = $.delegate( this, onBlur ),
3885             onNextHandler           = $.delegate( this, onNext ),
3886             onPreviousHandler       = $.delegate( this, onPrevious ),
3887             navImages               = this.navImages,
3888             buttons                 = [],
3889             useGroup                = true ;
3890 
3891         if( this.showSequenceControl && THIS[ this.hash ].sequenced ){
3892             
3893             if( this.previousButton || this.nextButton ){
3894                 //if we are binding to custom buttons then layout and 
3895                 //grouping is the responsibility of the page author
3896                 useGroup = false;
3897             }
3898 
3899             this.previousButton = new $.Button({ 
3900                 element:    this.previousButton ? $.getElement( this.previousButton ) : null,
3901                 clickTimeThreshold: this.clickTimeThreshold,
3902                 clickDistThreshold: this.clickDistThreshold,
3903                 tooltip:    $.getString( "Tooltips.PreviousPage" ),
3904                 srcRest:    resolveUrl( this.prefixUrl, navImages.previous.REST ),
3905                 srcGroup:   resolveUrl( this.prefixUrl, navImages.previous.GROUP ),
3906                 srcHover:   resolveUrl( this.prefixUrl, navImages.previous.HOVER ),
3907                 srcDown:    resolveUrl( this.prefixUrl, navImages.previous.DOWN ),
3908                 onRelease:  onPreviousHandler,
3909                 onFocus:    onFocusHandler,
3910                 onBlur:     onBlurHandler
3911             });
3912 
3913             this.nextButton = new $.Button({ 
3914                 element:    this.nextButton ? $.getElement( this.nextButton ) : null,
3915                 clickTimeThreshold: this.clickTimeThreshold,
3916                 clickDistThreshold: this.clickDistThreshold,
3917                 tooltip:    $.getString( "Tooltips.NextPage" ),
3918                 srcRest:    resolveUrl( this.prefixUrl, navImages.next.REST ),
3919                 srcGroup:   resolveUrl( this.prefixUrl, navImages.next.GROUP ),
3920                 srcHover:   resolveUrl( this.prefixUrl, navImages.next.HOVER ),
3921                 srcDown:    resolveUrl( this.prefixUrl, navImages.next.DOWN ),
3922                 onRelease:  onNextHandler,
3923                 onFocus:    onFocusHandler,
3924                 onBlur:     onBlurHandler
3925             });
3926 
3927             this.previousButton.disable();
3928 
3929             if( useGroup ){
3930                 this.paging = new $.ButtonGroup({
3931                     buttons: [ 
3932                         this.previousButton, 
3933                         this.nextButton 
3934                     ],
3935                     clickTimeThreshold: this.clickTimeThreshold,
3936                     clickDistThreshold: this.clickDistThreshold
3937                 });
3938 
3939                 this.pagingControl = this.paging.element;
3940 
3941                 if( this.toolbar ){
3942                     this.toolbar.addControl( 
3943                         this.navControl, 
3944                         $.ControlAnchor.BOTTOM_RIGHT  
3945                     );
3946                 }else{
3947                     this.addControl( 
3948                         this.pagingControl, 
3949                         $.ControlAnchor.TOP_LEFT 
3950                     );
3951                 }
3952             }
3953         }
3954     },
3955 
3956     bindStandardControls: function(){
3957         //////////////////////////////////////////////////////////////////////////
3958         // Navigation Controls
3959         //////////////////////////////////////////////////////////////////////////
3960         var beginZoomingInHandler   = $.delegate( this, beginZoomingIn ),
3961             endZoomingHandler       = $.delegate( this, endZooming ),
3962             doSingleZoomInHandler   = $.delegate( this, doSingleZoomIn ),
3963             beginZoomingOutHandler  = $.delegate( this, beginZoomingOut ),
3964             doSingleZoomOutHandler  = $.delegate( this, doSingleZoomOut ),
3965             onHomeHandler           = $.delegate( this, onHome ),
3966             onFullPageHandler       = $.delegate( this, onFullPage ),
3967             onFocusHandler          = $.delegate( this, onFocus ),
3968             onBlurHandler           = $.delegate( this, onBlur ),
3969             navImages               = this.navImages,
3970             buttons                 = [],
3971             useGroup                = true ;
3972 
3973 
3974         if( this.showNavigationControl ){
3975 
3976             if( this.zoomInButton || this.zoomOutButton || this.homeButton || this.fullPageButton ){
3977                 //if we are binding to custom buttons then layout and 
3978                 //grouping is the responsibility of the page author
3979                 useGroup = false;
3980             }
3981             
3982             buttons.push( this.zoomInButton = new $.Button({ 
3983                 element:    this.zoomInButton ? $.getElement( this.zoomInButton ) : null,
3984                 clickTimeThreshold: this.clickTimeThreshold,
3985                 clickDistThreshold: this.clickDistThreshold,
3986                 tooltip:    $.getString( "Tooltips.ZoomIn" ), 
3987                 srcRest:    resolveUrl( this.prefixUrl, navImages.zoomIn.REST ),
3988                 srcGroup:   resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ),
3989                 srcHover:   resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ),
3990                 srcDown:    resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ),
3991                 onPress:    beginZoomingInHandler,
3992                 onRelease:  endZoomingHandler,
3993                 onClick:    doSingleZoomInHandler,
3994                 onEnter:    beginZoomingInHandler,
3995                 onExit:     endZoomingHandler,
3996                 onFocus:    onFocusHandler,
3997                 onBlur:     onBlurHandler
3998             }));
3999 
4000             buttons.push( this.zoomOutButton = new $.Button({ 
4001                 element:    this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null,
4002                 clickTimeThreshold: this.clickTimeThreshold,
4003                 clickDistThreshold: this.clickDistThreshold,
4004                 tooltip:    $.getString( "Tooltips.ZoomOut" ), 
4005                 srcRest:    resolveUrl( this.prefixUrl, navImages.zoomOut.REST ), 
4006                 srcGroup:   resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ), 
4007                 srcHover:   resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ), 
4008                 srcDown:    resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ),
4009                 onPress:    beginZoomingOutHandler, 
4010                 onRelease:  endZoomingHandler, 
4011                 onClick:    doSingleZoomOutHandler, 
4012                 onEnter:    beginZoomingOutHandler, 
4013                 onExit:     endZoomingHandler,
4014                 onFocus:    onFocusHandler,
4015                 onBlur:     onBlurHandler
4016             }));
4017 
4018             buttons.push( this.homeButton = new $.Button({ 
4019                 element:    this.homeButton ? $.getElement( this.homeButton ) : null,
4020                 clickTimeThreshold: this.clickTimeThreshold,
4021                 clickDistThreshold: this.clickDistThreshold,
4022                 tooltip:    $.getString( "Tooltips.Home" ), 
4023                 srcRest:    resolveUrl( this.prefixUrl, navImages.home.REST ), 
4024                 srcGroup:   resolveUrl( this.prefixUrl, navImages.home.GROUP ), 
4025                 srcHover:   resolveUrl( this.prefixUrl, navImages.home.HOVER ), 
4026                 srcDown:    resolveUrl( this.prefixUrl, navImages.home.DOWN ),
4027                 onRelease:  onHomeHandler,
4028                 onFocus:    onFocusHandler,
4029                 onBlur:     onBlurHandler
4030             }));
4031 
4032             buttons.push( this.fullPageButton = new $.Button({ 
4033                 element:    this.fullPageButton ? $.getElement( this.fullPageButton ) : null,
4034                 clickTimeThreshold: this.clickTimeThreshold,
4035                 clickDistThreshold: this.clickDistThreshold,
4036                 tooltip:    $.getString( "Tooltips.FullPage" ),
4037                 srcRest:    resolveUrl( this.prefixUrl, navImages.fullpage.REST ),
4038                 srcGroup:   resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ),
4039                 srcHover:   resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ),
4040                 srcDown:    resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ),
4041                 onRelease:  onFullPageHandler,
4042                 onFocus:    onFocusHandler,
4043                 onBlur:     onBlurHandler
4044             }));
4045 
4046             if( useGroup ){
4047                 this.buttons = new $.ButtonGroup({
4048                     buttons:            buttons,
4049                     clickTimeThreshold: this.clickTimeThreshold,
4050                     clickDistThreshold: this.clickDistThreshold
4051                 });
4052 
4053                 this.navControl  = this.buttons.element;
4054                 this.addHandler( 'open', $.delegate( this, lightUp ) );
4055 
4056                 if( this.toolbar ){
4057                     this.toolbar.addControl( 
4058                         this.navControl, 
4059                         $.ControlAnchor.TOP_LEFT  
4060                     );
4061                 }else{
4062                     this.addControl( 
4063                         this.navControl, 
4064                         $.ControlAnchor.BOTTOM_RIGHT 
4065                     );
4066                 }
4067             }
4068 
4069             
4070         }
4071     }
4072 
4073 });
4074 
4075 
4076 
4077 ///////////////////////////////////////////////////////////////////////////////
4078 // Schedulers provide the general engine for animation
4079 ///////////////////////////////////////////////////////////////////////////////
4080 function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){
4081     var currentTime,
4082         targetTime,
4083         deltaTime;
4084 
4085     if ( THIS[ viewer.hash ].animating ) {
4086         return window.setTimeout( function(){
4087             updateFunc( viewer );
4088         }, 1 );
4089     }
4090 
4091     currentTime     = +new Date();
4092     prevUpdateTime  = prevUpdateTime ? prevUpdateTime : currentTime;
4093     // 60 frames per second is ideal
4094     targetTime      = prevUpdateTime + 1000 / 60;
4095     deltaTime       = Math.max( 1, targetTime - currentTime );
4096     
4097     return window.setTimeout( function(){
4098         updateFunc( viewer );
4099     }, deltaTime );
4100 };
4101 
4102 
4103 //provides a sequence in the fade animation
4104 function scheduleControlsFade( viewer ) {
4105     window.setTimeout( function(){
4106         updateControlsFade( viewer );
4107     }, 20);
4108 };
4109 
4110 
4111 //initiates an animation to hide the controls
4112 function beginControlsAutoHide( viewer ) {
4113     if ( !viewer.autoHideControls ) {
4114         return;
4115     }
4116     viewer.controlsShouldFade = true;
4117     viewer.controlsFadeBeginTime = 
4118         +new Date() + 
4119         viewer.controlsFadeDelay;
4120 
4121     window.setTimeout( function(){
4122         scheduleControlsFade( viewer );
4123     }, viewer.controlsFadeDelay );
4124 };
4125 
4126 
4127 //determines if fade animation is done or continues the animation
4128 function updateControlsFade( viewer ) {
4129     var currentTime,
4130         deltaTime,
4131         opacity,
4132         i;
4133     if ( viewer.controlsShouldFade ) {
4134         currentTime = new Date().getTime();
4135         deltaTime = currentTime - viewer.controlsFadeBeginTime;
4136         opacity = 1.0 - deltaTime / viewer.controlsFadeLength;
4137 
4138         opacity = Math.min( 1.0, opacity );
4139         opacity = Math.max( 0.0, opacity );
4140 
4141         for ( i = viewer.controls.length - 1; i >= 0; i--) {
4142             viewer.controls[ i ].setOpacity( opacity );
4143         }
4144 
4145         if ( opacity > 0 ) {
4146             // fade again
4147             scheduleControlsFade( viewer ); 
4148         }
4149     }
4150 };
4151 
4152 
4153 //stop the fade animation on the controls and show them
4154 function abortControlsAutoHide( viewer ) {
4155     var i;
4156     viewer.controlsShouldFade = false;
4157     for ( i = viewer.controls.length - 1; i >= 0; i-- ) {
4158         viewer.controls[ i ].setOpacity( 1.0 );
4159     }
4160 };
4161 
4162 
4163 
4164 ///////////////////////////////////////////////////////////////////////////////
4165 // Default view event handlers.
4166 ///////////////////////////////////////////////////////////////////////////////
4167 function onFocus(){
4168     abortControlsAutoHide( this );
4169 };
4170 
4171 function onBlur(){
4172     beginControlsAutoHide( this );
4173     
4174 };
4175 
4176 function onCanvasClick( tracker, position, quick, shift ) {
4177     var zoomPreClick,
4178         factor;
4179     if ( this.viewport && quick ) {    // ignore clicks where mouse moved         
4180         zoomPerClick = this.zoomPerClick;
4181         factor = shift ? 1.0 / zoomPerClick : zoomPerClick;
4182         this.viewport.zoomBy(
4183             factor, 
4184             this.viewport.pointFromPixel( position, true )
4185         );
4186         this.viewport.applyConstraints();
4187     }
4188 };
4189 
4190 function onCanvasDrag( tracker, position, delta, shift ) {
4191     if ( this.viewport ) {
4192         if( !this.panHorizontal ){
4193             delta.x = 0;
4194         }
4195         if( !this.panVertical ){
4196             delta.y = 0;
4197         }
4198         this.viewport.panBy( 
4199             this.viewport.deltaPointsFromPixels( 
4200                 delta.negate() 
4201             ) 
4202         );
4203     }
4204 };
4205 
4206 function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) {
4207     if ( insideElementPress && this.viewport ) {
4208         this.viewport.applyConstraints();
4209     }
4210 };
4211 
4212 function onCanvasScroll( tracker, position, scroll, shift ) {
4213     var factor;
4214     if ( this.viewport ) {
4215         factor = Math.pow( this.zoomPerScroll, scroll );
4216         this.viewport.zoomBy( 
4217             factor, 
4218             this.viewport.pointFromPixel( position, true ) 
4219         );
4220         this.viewport.applyConstraints();
4221     }
4222     //cancels event
4223     return false;
4224 };
4225 
4226 function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) {
4227     if ( !buttonDownElement ) {
4228         THIS[ this.hash ].mouseInside = false;
4229         if ( !THIS[ this.hash ].animating ) {
4230             beginControlsAutoHide( this );
4231         }
4232     }
4233 };
4234 
4235 function onContainerRelease( tracker, position, insideElementPress, insideElementRelease ) {
4236     if ( !insideElementRelease ) {
4237         THIS[ this.hash ].mouseInside = false;
4238         if ( !THIS[ this.hash ].animating ) {
4239             beginControlsAutoHide( this );
4240         }
4241     }
4242 };
4243 
4244 function onContainerEnter( tracker, position, buttonDownElement, buttonDownAny ) {
4245     THIS[ this.hash ].mouseInside = true;
4246     abortControlsAutoHide( this );
4247 };
4248 
4249 
4250 ///////////////////////////////////////////////////////////////////////////////
4251 // Page update routines ( aka Views - for future reference )
4252 ///////////////////////////////////////////////////////////////////////////////
4253 
4254 function updateMulti( viewer ) {
4255 
4256     var beginTime;
4257 
4258     if ( !viewer.source ) {
4259         return;
4260     }
4261 
4262     beginTime = +new Date();
4263     updateOnce( viewer );
4264     scheduleUpdate( viewer, arguments.callee, beginTime );
4265 };
4266 
4267 function updateOnce( viewer ) {
4268 
4269     var containerSize,
4270         animated;
4271 
4272     if ( !viewer.source ) {
4273         return;
4274     }
4275 
4276     //viewer.profiler.beginUpdate();
4277 
4278     containerSize = $.getElementSize( viewer.container );
4279     if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) {
4280         // maintain image position
4281         viewer.viewport.resize( containerSize, true ); 
4282         THIS[ viewer.hash ].prevContainerSize = containerSize;
4283         viewer.raiseEvent( "resize" );
4284     }
4285 
4286     animated = viewer.viewport.update();
4287     if ( !THIS[ viewer.hash ].animating && animated ) {
4288         viewer.raiseEvent( "animationstart" );
4289         abortControlsAutoHide( viewer );
4290     }
4291 
4292     if ( animated ) {
4293         viewer.drawer.update();
4294         if( viewer.navigator ){
4295             viewer.navigator.update( viewer.viewport );
4296         }
4297         viewer.raiseEvent( "animation" );
4298     } else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) {
4299         viewer.drawer.update();
4300         if( viewer.navigator ){
4301             viewer.navigator.update( viewer.viewport );
4302         }
4303         THIS[ viewer.hash ].forceRedraw = false;
4304     } 
4305 
4306     if ( THIS[ viewer.hash ].animating && !animated ) {
4307         viewer.raiseEvent( "animationfinish" );
4308 
4309         if ( !THIS[ viewer.hash ].mouseInside ) {
4310             beginControlsAutoHide( viewer );
4311         }
4312     }
4313 
4314     THIS[ viewer.hash ].animating = animated;
4315 
4316     //viewer.profiler.endUpdate();
4317 };
4318 
4319 
4320 
4321 ///////////////////////////////////////////////////////////////////////////////
4322 // Navigation Controls
4323 ///////////////////////////////////////////////////////////////////////////////
4324 function resolveUrl( prefix, url ) {
4325     return prefix ? prefix + url : url;
4326 };
4327 
4328 
4329 
4330 function beginZoomingIn() {
4331     THIS[ this.hash ].lastZoomTime = +new Date();
4332     THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
4333     THIS[ this.hash ].zooming = true;
4334     scheduleZoom( this );
4335 };
4336 
4337 
4338 function beginZoomingOut() {
4339     THIS[ this.hash ].lastZoomTime = +new Date();
4340     THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond;
4341     THIS[ this.hash ].zooming = true;
4342     scheduleZoom( this );
4343 };
4344 
4345 
4346 function endZooming() {
4347     THIS[ this.hash ].zooming = false;
4348 };
4349 
4350 
4351 function scheduleZoom( viewer ) {
4352     window.setTimeout( $.delegate( viewer, doZoom ), 10 );
4353 };
4354 
4355 
4356 function doZoom() {
4357     var currentTime,
4358         deltaTime,
4359         adjustFactor;
4360 
4361     if ( THIS[ this.hash ].zooming && this.viewport) {
4362         currentTime     = +new Date();
4363         deltaTime       = currentTime - THIS[ this.hash ].lastZoomTime;
4364         adjustedFactor  = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 );
4365 
4366         this.viewport.zoomBy( adjustedFactor );
4367         this.viewport.applyConstraints();
4368         THIS[ this.hash ].lastZoomTime = currentTime;
4369         scheduleZoom( this );
4370     }
4371 };
4372 
4373 
4374 function doSingleZoomIn() {
4375     if ( this.viewport ) {
4376         THIS[ this.hash ].zooming = false;
4377         this.viewport.zoomBy( 
4378             this.zoomPerClick / 1.0 
4379         );
4380         this.viewport.applyConstraints();
4381     }
4382 };
4383 
4384 
4385 function doSingleZoomOut() {
4386     if ( this.viewport ) {
4387         THIS[ this.hash ].zooming = false;
4388         this.viewport.zoomBy(
4389             1.0 / this.zoomPerClick
4390         );
4391         this.viewport.applyConstraints();
4392     }
4393 };
4394 
4395 
4396 function lightUp() {
4397     this.buttons.emulateEnter();
4398     this.buttons.emulateExit();
4399 };
4400 
4401 
4402 function onHome() {
4403     if ( this.viewport ) {
4404         this.viewport.goHome();
4405     }
4406 };
4407 
4408 
4409 function onFullPage() {
4410     this.setFullPage( !this.isFullPage() );
4411     // correct for no mouseout event on change
4412     if( this.buttons ){
4413         this.buttons.emulateExit();
4414     }
4415     this.fullPageButton.element.focus();
4416     if ( this.viewport ) {
4417         this.viewport.applyConstraints();
4418     }
4419 };
4420 
4421 
4422 function onPrevious(){
4423     var previous = THIS[ this.hash ].sequence - 1,
4424         preserveVewport = true;
4425     if( previous >= 0 ){
4426 
4427         THIS[ this.hash ].sequence = previous;
4428 
4429         if( 0 === previous  ){
4430             //Disable previous button
4431             this.previousButton.disable();
4432         }
4433         if( this.tileSources.length > 0  ){
4434             //Enable next button
4435             this.nextButton.enable();
4436         }
4437 
4438         this.openTileSource( this.tileSources[ previous ] );
4439     }
4440 };
4441 
4442 
4443 function onNext(){
4444     var next = THIS[ this.hash ].sequence + 1,
4445         preserveVewport = true;
4446     if( this.tileSources.length > next ){
4447         
4448         THIS[ this.hash ].sequence = next;
4449 
4450         if( ( this.tileSources.length - 1 ) === next  ){
4451             //Disable next button
4452             this.nextButton.disable();
4453         }
4454         if( next > 0 ){
4455             //Enable previous button
4456             this.previousButton.enable();
4457         }
4458 
4459         this.openTileSource( this.tileSources[ next ] );
4460     }
4461 };
4462 
4463 
4464 }( OpenSeadragon ));
4465 (function( $ ){
4466     
4467 /**
4468  * The Navigator provides a small view of the current image as fixed
4469  * while representing the viewport as a moving box serving as a frame
4470  * of reference in the larger viewport as to which portion of the image
4471  * is currently being examined.  The navigators viewport can be interacted
4472  * with using the keyboard or the mouse.
4473  * @class 
4474  * @name OpenSeadragon.Navigator
4475  * @extends OpenSeadragon.Viewer
4476  * @extends OpenSeadragon.EventHandler
4477  * @param {Object} options
4478  * @param {String} options.viewerId
4479  */
4480 $.Navigator = function( options ){
4481 
4482     var _this       = this,
4483         viewer      = options.viewer,
4484         viewerSize  = $.getElementSize( viewer.element );
4485     
4486     //We may need to create a new element and id if they did not
4487     //provide the id for the existing element
4488     if( !options.id ){
4489         options.id              = 'navigator-' + (+new Date());
4490         this.element            = $.makeNeutralElement( "div" );
4491         this.element.id         = options.id;
4492         this.element.className  = 'navigator';
4493     }
4494 
4495     options = $.extend( true, {
4496         navigatorSizeRatio:     $.DEFAULT_SETTINGS.navigatorSizeRatio
4497     }, options, {
4498         element:                this.element,
4499         //These need to be overridden to prevent recursion since
4500         //the navigator is a viewer and a viewer has a navigator
4501         showNavigator:          false,
4502         mouseNavEnabled:        false,
4503         showNavigationControl:  false,
4504         showSequenceControl:    false
4505     });
4506 
4507     options.minPixelRatio = Math.min(
4508         options.navigatorSizeRatio * $.DEFAULT_SETTINGS.minPixelRatio,
4509         $.DEFAULT_SETTINGS.minPixelRatio
4510     );
4511 
4512     (function( style ){
4513         style.marginTop     = '0px';
4514         style.marginRight   = '0px';
4515         style.marginBottom  = '0px';
4516         style.marginLeft    = '0px';
4517         style.border        = '2px solid #555';
4518         style.background    = '#000';
4519         style.opacity       = 0.8;
4520         style.overflow      = 'hidden';
4521     }( this.element.style ));
4522 
4523     this.displayRegion           = $.makeNeutralElement( "textarea" );
4524     this.displayRegion.id        = this.element.id + '-displayregion';
4525     this.displayRegion.className = 'displayregion';
4526 
4527     (function( style ){
4528         style.position      = 'relative';
4529         style.top           = '0px';
4530         style.left          = '0px';
4531         style.fontSize      = '0px';
4532         style.border        = '2px solid #900';
4533         //TODO: IE doesnt like this property being set
4534         try{ style.outline  = '2px auto #900'; }catch(e){/*ignore*/}
4535         style.background    = 'transparent';
4536         style.float         = 'left'; //Webkit
4537         style.cssFloat      = 'left'; //Firefox
4538         style.styleFloat    = 'left'; //IE
4539         style.zIndex        = 999999999;
4540         style.cursor        = 'default';
4541     }( this.displayRegion.style ));
4542 
4543     this.element.innerTracker = new $.MouseTracker({
4544         element:        this.element,
4545         scrollHandler:  function(){
4546             //dont scroll the page up and down if the user is scrolling
4547             //in the navigator
4548             return false;
4549         }
4550     }).setTracking( true );
4551 
4552     this.displayRegion.innerTracker = new $.MouseTracker({
4553         element:            this.displayRegion, 
4554         clickTimeThreshold: this.clickTimeThreshold, 
4555         clickDistThreshold: this.clickDistThreshold,
4556         clickHandler:       $.delegate( this, onCanvasClick ),
4557         dragHandler:        $.delegate( this, onCanvasDrag ),
4558         releaseHandler:     $.delegate( this, onCanvasRelease ),
4559         scrollHandler:      $.delegate( this, onCanvasScroll ),
4560         focusHandler:       function(){
4561             var point    = $.getElementPosition( _this.viewer.element );
4562 
4563             window.scrollTo( 0, point.y );
4564 
4565             _this.viewer.setControlsEnabled( true );
4566             (function( style ){
4567                 style.border        = '2px solid #437AB2';
4568                 style.outline       = '2px auto #437AB2';
4569             }( this.element.style ));
4570 
4571         },
4572         blurHandler:       function(){
4573             _this.viewer.setControlsEnabled( false );
4574             (function( style ){
4575                 style.border        = '2px solid #900';
4576                 style.outline       = '2px auto #900';
4577             }( this.element.style ));
4578         },
4579         keyHandler:         function(tracker, keyCode, shiftKey){
4580             //console.log( keyCode );
4581             switch( keyCode ){
4582                 case 61://=|+
4583                     _this.viewer.viewport.zoomBy(1.1);
4584                     _this.viewer.viewport.applyConstraints();
4585                     return false;
4586                 case 45://-|_
4587                     _this.viewer.viewport.zoomBy(0.9);
4588                     _this.viewer.viewport.applyConstraints();
4589                     return false;
4590                 case 48://0|)
4591                     _this.viewer.viewport.goHome();
4592                     _this.viewer.viewport.applyConstraints();
4593                     return false;
4594                 case 119://w
4595                 case 87://W
4596                 case 38://up arrow
4597                     shiftKey ?
4598                         _this.viewer.viewport.zoomBy(1.1):
4599                         _this.viewer.viewport.panBy(new $.Point(0, -0.05));
4600                     _this.viewer.viewport.applyConstraints();
4601                     return false;
4602                 case 115://s
4603                 case 83://S
4604                 case 40://down arrow
4605                     shiftKey ?
4606                         _this.viewer.viewport.zoomBy(0.9):
4607                         _this.viewer.viewport.panBy(new $.Point(0, 0.05));
4608                     _this.viewer.viewport.applyConstraints();
4609                     return false;
4610                 case 97://a
4611                 case 37://left arrow
4612                     _this.viewer.viewport.panBy(new $.Point(-0.05, 0));
4613                     _this.viewer.viewport.applyConstraints();
4614                     return false;
4615                 case 100://d
4616                 case 39://right arrow
4617                     _this.viewer.viewport.panBy(new $.Point(0.05, 0));  
4618                     _this.viewer.viewport.applyConstraints();
4619                     return false;
4620                 default:
4621                     //console.log( 'navigator keycode %s', keyCode );
4622                     return true;
4623             }
4624         }
4625     }).setTracking( true ); // default state
4626 
4627     /*this.displayRegion.outerTracker = new $.MouseTracker({
4628         element:            this.container, 
4629         clickTimeThreshold: this.clickTimeThreshold, 
4630         clickDistThreshold: this.clickDistThreshold,
4631         enterHandler:       $.delegate( this, onContainerEnter ),
4632         exitHandler:        $.delegate( this, onContainerExit ),
4633         releaseHandler:     $.delegate( this, onContainerRelease )
4634     }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/
4635 
4636     this.element.appendChild( this.displayRegion );
4637 
4638     viewer.addControl( 
4639         this.element, 
4640         $.ControlAnchor.TOP_RIGHT 
4641     );
4642 
4643     if( options.width && options.height ){
4644         this.element.style.width  = options.width + 'px';
4645         this.element.style.height = options.height + 'px';
4646     } else {
4647         this.element.style.width  = ( viewerSize.x * options.navigatorSizeRatio ) + 'px';
4648         this.element.style.height = ( viewerSize.y * options.navigatorSizeRatio ) + 'px';
4649     }
4650 
4651     $.Viewer.apply( this, [ options ] ); 
4652 
4653 };
4654 
4655 $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, {
4656 
4657     /**
4658      * @function
4659      * @name OpenSeadragon.Navigator.prototype.update
4660      */
4661     update: function( viewport ){
4662 
4663         var bounds,
4664             topleft,
4665             bottomright;
4666 
4667         if( viewport && this.viewport ){
4668             bounds      = viewport.getBounds( true );
4669             topleft     = this.viewport.pixelFromPoint( bounds.getTopLeft() );
4670             bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() );
4671 
4672             //update style for navigator-box    
4673             (function(style){
4674 
4675                 style.top    = topleft.y + 'px';
4676                 style.left   = topleft.x + 'px';
4677                 style.width  = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px';
4678                 style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px';
4679 
4680             }( this.displayRegion.style ));  
4681         } 
4682 
4683     }
4684 
4685 });
4686 
4687 
4688 /**
4689  * @private
4690  * @inner
4691  * @function
4692  */
4693 function onCanvasClick( tracker, position, quick, shift ) {
4694     this.displayRegion.focus();
4695 };
4696 
4697 
4698 /**
4699  * @private
4700  * @inner
4701  * @function
4702  */
4703 function onCanvasDrag( tracker, position, delta, shift ) {
4704     if ( this.viewer.viewport ) {
4705         if( !this.panHorizontal ){
4706             delta.x = 0;
4707         }
4708         if( !this.panVertical ){
4709             delta.y = 0;
4710         }
4711         this.viewer.viewport.panBy( 
4712             this.viewport.deltaPointsFromPixels( 
4713                 delta
4714             ) 
4715         );
4716     }
4717 };
4718 
4719 
4720 /**
4721  * @private
4722  * @inner
4723  * @function
4724  */
4725 function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) {
4726     if ( insideElementPress && this.viewer.viewport ) {
4727         this.viewer.viewport.applyConstraints();
4728     }
4729 };
4730 
4731 
4732 /**
4733  * @private
4734  * @inner
4735  * @function
4736  */
4737 function onCanvasScroll( tracker, position, scroll, shift ) {
4738     var factor;
4739     if ( this.viewer.viewport ) {
4740         factor = Math.pow( this.zoomPerScroll, scroll );
4741         this.viewer.viewport.zoomBy( 
4742             factor, 
4743             //this.viewport.pointFromPixel( position, true ) 
4744             this.viewport.getCenter()
4745         );
4746         this.viewer.viewport.applyConstraints();
4747     }
4748     //cancels event
4749     return false;
4750 };
4751 
4752 
4753 }( OpenSeadragon ));
4754 (function( $ ){
4755     
4756 //TODO: I guess this is where the i18n needs to be reimplemented.  I'll look 
4757 //      into existing patterns for i18n in javascript but i think that mimicking
4758 //      pythons gettext might be a reasonable approach.
4759 var I18N = {
4760     Errors: {
4761         Failure:        "Sorry, but Seadragon Ajax can't run on your browser!\n" +
4762                         "Please try using IE 7 or Firefox 3.\n",
4763         Dzc:            "Sorry, we don't support Deep Zoom Collections!",
4764         Dzi:            "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
4765         Xml:            "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
4766         Empty:          "You asked us to open nothing, so we did just that.",
4767         ImageFormat:    "Sorry, we don't support {0}-based Deep Zoom Images.",
4768         Security:       "It looks like a security restriction stopped us from " +
4769                         "loading this Deep Zoom Image.",
4770         Status:         "This space unintentionally left blank ({0} {1}).",
4771         Unknown:        "Whoops, something inexplicably went wrong. Sorry!"
4772     },
4773 
4774     Messages: {
4775         Loading:        "Loading..."
4776     },
4777 
4778     Tooltips: {
4779         FullPage:       "Toggle full page",
4780         Home:           "Go home",
4781         ZoomIn:         "Zoom in",
4782         ZoomOut:        "Zoom out",
4783         NextPage:       "Next page",
4784         PreviousPage:   "Previous page"
4785     }
4786 };
4787 
4788 $.extend( $, {
4789 
4790     /**
4791      * @function
4792      * @name OpenSeadragon.getString
4793      * @param {String} property
4794      */
4795     getString: function( prop ) {
4796         
4797         var props   = prop.split('.'),
4798             string  = null,
4799             args    = arguments,
4800             i;
4801 
4802         for ( i = 0; i < props.length; i++ ) {
4803             // in case not a subproperty
4804             string = I18N[ props[ i ] ] || {};
4805         }
4806 
4807         if ( typeof( string ) != "string" ) {
4808             string = "";
4809         }
4810 
4811         return string.replace(/\{\d+\}/g, function(capture) {
4812             var i = parseInt( capture.match( /\d+/ ) ) + 1;
4813             return i < args.length ? 
4814                 args[ i ] : 
4815                 "";
4816         });
4817     },
4818 
4819     /**
4820      * @function
4821      * @name OpenSeadragon.setString
4822      * @param {String} property
4823      * @param {*} value
4824      */
4825     setString: function( prop, value ) {
4826 
4827         var props     = prop.split('.'),
4828             container = $.Strings,
4829             i;
4830 
4831         for ( i = 0; i < props.length - 1; i++ ) {
4832             if ( !container[ props[ i ] ] ) {
4833                 container[ props[ i ] ] = {};
4834             }
4835             container = container[ props[ i ] ];
4836         }
4837 
4838         container[ props[ i ] ] = value;
4839     }
4840 
4841 });
4842 
4843 }( OpenSeadragon ));
4844 
4845 (function( $ ){
4846 
4847 /**
4848  * A Point is really used as a 2-dimensional vector, equally useful for 
4849  * representing a point on a plane, or the height and width of a plane
4850  * not requiring any other frame of reference.
4851  * @class
4852  * @param {Number} [x] The vector component 'x'. Defaults to the origin at 0.
4853  * @param {Number} [y] The vector component 'y'. Defaults to the origin at 0.
4854  * @property {Number} [x] The vector component 'x'. 
4855  * @property {Number} [y] The vector component 'y'.
4856  */
4857 $.Point = function( x, y ) {
4858     this.x = typeof ( x ) == "number" ? x : 0;
4859     this.y = typeof ( y ) == "number" ? y : 0;
4860 };
4861 
4862 $.Point.prototype = {
4863 
4864     /**
4865      * Add another Point to this point and return a new Point.
4866      * @function
4867      * @param {OpenSeadragon.Point} point The point to add vector components.
4868      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4869      *  vector components
4870      */
4871     plus: function( point ) {
4872         return new $.Point(
4873             this.x + point.x, 
4874             this.y + point.y
4875         );
4876     },
4877 
4878     /**
4879      * Add another Point to this point and return a new Point.
4880      * @function
4881      * @param {OpenSeadragon.Point} point The point to add vector components.
4882      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4883      *  vector components
4884      */
4885     minus: function( point ) {
4886         return new $.Point(
4887             this.x - point.x, 
4888             this.y - point.y
4889         );
4890     },
4891 
4892     /**
4893      * Add another Point to this point and return a new Point.
4894      * @function
4895      * @param {OpenSeadragon.Point} point The point to add vector components.
4896      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4897      *  vector components
4898      */
4899     times: function( factor ) {
4900         return new $.Point(
4901             this.x * factor, 
4902             this.y * factor
4903         );
4904     },
4905 
4906     /**
4907      * Add another Point to this point and return a new Point.
4908      * @function
4909      * @param {OpenSeadragon.Point} point The point to add vector components.
4910      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4911      *  vector components
4912      */
4913     divide: function( factor ) {
4914         return new $.Point(
4915             this.x / factor, 
4916             this.y / factor
4917         );
4918     },
4919 
4920     /**
4921      * Add another Point to this point and return a new Point.
4922      * @function
4923      * @param {OpenSeadragon.Point} point The point to add vector components.
4924      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4925      *  vector components
4926      */
4927     negate: function() {
4928         return new $.Point( -this.x, -this.y );
4929     },
4930 
4931     /**
4932      * Add another Point to this point and return a new Point.
4933      * @function
4934      * @param {OpenSeadragon.Point} point The point to add vector components.
4935      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4936      *  vector components
4937      */
4938     distanceTo: function( point ) {
4939         return Math.sqrt(
4940             Math.pow( this.x - point.x, 2 ) +
4941             Math.pow( this.y - point.y, 2 )
4942         );
4943     },
4944 
4945     /**
4946      * Add another Point to this point and return a new Point.
4947      * @function
4948      * @param {OpenSeadragon.Point} point The point to add vector components.
4949      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4950      *  vector components
4951      */
4952     apply: function( func ) {
4953         return new $.Point( func( this.x ), func( this.y ) );
4954     },
4955 
4956     /**
4957      * Add another Point to this point and return a new Point.
4958      * @function
4959      * @param {OpenSeadragon.Point} point The point to add vector components.
4960      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4961      *  vector components
4962      */
4963     equals: function( point ) {
4964         return ( 
4965             point instanceof $.Point 
4966         ) && ( 
4967             this.x === point.x 
4968         ) && ( 
4969             this.y === point.y 
4970         );
4971     },
4972 
4973     /**
4974      * Add another Point to this point and return a new Point.
4975      * @function
4976      * @param {OpenSeadragon.Point} point The point to add vector components.
4977      * @returns {OpenSeadragon.Point} A new point representing the sum of the
4978      *  vector components
4979      */
4980     toString: function() {
4981         return "(" + this.x + "," + this.y + ")";
4982     }
4983 };
4984 
4985 }( OpenSeadragon ));
4986 
4987 (function( $ ){
4988 
4989 
4990 /**
4991  * @class
4992  * @param {Number} width
4993  * @param {Number} height
4994  * @param {Number} tileSize
4995  * @param {Number} tileOverlap
4996  * @param {Number} minLevel
4997  * @param {Number} maxLevel
4998  * @property {Number} aspectRatio
4999  * @property {Number} dimensions
5000  * @property {Number} tileSize
5001  * @property {Number} tileOverlap
5002  * @property {Number} minLevel
5003  * @property {Number} maxLevel
5004  */ 
5005 $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
5006     this.aspectRatio = width / height;
5007     this.dimensions  = new $.Point( width, height );
5008     this.tileSize    = tileSize ? tileSize : 0;
5009     this.tileOverlap = tileOverlap ? tileOverlap : 0;
5010     this.minLevel    = minLevel ? minLevel : 0;
5011     this.maxLevel    = maxLevel ? maxLevel :
5012         Math.ceil( 
5013             Math.log( Math.max( width, height ) ) / 
5014             Math.log( 2 ) 
5015         );
5016 };
5017 
5018 $.TileSource.prototype = {
5019     
5020     /**
5021      * @function
5022      * @param {Number} level
5023      */
5024     getLevelScale: function( level ) {
5025         return 1 / ( 1 << ( this.maxLevel - level ) );
5026     },
5027 
5028     /**
5029      * @function
5030      * @param {Number} level
5031      */
5032     getNumTiles: function( level ) {
5033         var scale = this.getLevelScale( level ),
5034             x = Math.ceil( scale * this.dimensions.x / this.tileSize ),
5035             y = Math.ceil( scale * this.dimensions.y / this.tileSize );
5036 
5037         return new $.Point( x, y );
5038     },
5039 
5040     /**
5041      * @function
5042      * @param {Number} level
5043      */
5044     getPixelRatio: function( level ) {
5045         var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
5046             rx = 1.0 / imageSizeScaled.x,
5047             ry = 1.0 / imageSizeScaled.y;
5048 
5049         return new $.Point(rx, ry);
5050     },
5051 
5052     /**
5053      * @function
5054      * @param {Number} level
5055      * @param {OpenSeadragon.Point} point
5056      */
5057     getTileAtPoint: function( level, point ) {
5058         var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ),
5059             tx = Math.floor( pixel.x / this.tileSize ),
5060             ty = Math.floor( pixel.y / this.tileSize );
5061 
5062         return new $.Point( tx, ty );
5063     },
5064 
5065     /**
5066      * @function
5067      * @param {Number} level
5068      * @param {Number} x
5069      * @param {Number} y
5070      */
5071     getTileBounds: function( level, x, y ) {
5072         var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
5073             px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap,
5074             py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap,
5075             sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
5076             sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap,
5077             scale = 1.0 / dimensionsScaled.x;
5078 
5079         sx = Math.min( sx, dimensionsScaled.x - px );
5080         sy = Math.min( sy, dimensionsScaled.y - py );
5081 
5082         return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
5083     },
5084 
5085     /**
5086      * This method is not implemented by this class other than to throw an Error
5087      * announcing you have to implement it.  Because of the variety of tile 
5088      * server technologies, and various specifications for building image
5089      * pyramids, this method is here to allow easy integration.
5090      * @function
5091      * @param {Number} level
5092      * @param {Number} x
5093      * @param {Number} y
5094      * @throws {Error}
5095      */
5096     getTileUrl: function( level, x, y ) {
5097         throw new Error( "Method not implemented." );
5098     },
5099 
5100     /**
5101      * @function
5102      * @param {Number} level
5103      * @param {Number} x
5104      * @param {Number} y
5105      */
5106     tileExists: function( level, x, y ) {
5107         var numTiles = this.getNumTiles( level );
5108         return  level >= this.minLevel && 
5109                 level <= this.maxLevel &&
5110                 x >= 0 && 
5111                 y >= 0 && 
5112                 x < numTiles.x && 
5113                 y < numTiles.y;
5114     }
5115 };
5116 
5117 }( OpenSeadragon ));
5118 
5119 (function( $ ){
5120     
5121 /**
5122  * @class
5123  * @extends OpenSeadragon.TileSource
5124  * @param {Number} width
5125  * @param {Number} height
5126  * @param {Number} tileSize
5127  * @param {Number} tileOverlap
5128  * @param {String} tilesUrl
5129  * @param {String} fileFormat
5130  * @param {OpenSeadragon.DisplayRect[]} displayRects
5131  * @property {String} tilesUrl
5132  * @property {String} fileFormat
5133  * @property {OpenSeadragon.DisplayRect[]} displayRects
5134  */ 
5135 $.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects ) {
5136     var i,
5137         rect,
5138         level;
5139 
5140     $.TileSource.call( this, width, height, tileSize, tileOverlap, null, null );
5141 
5142     this._levelRects  = {};
5143     this.tilesUrl     = tilesUrl;
5144     this.fileFormat   = fileFormat;
5145     this.displayRects = displayRects;
5146     
5147     if ( this.displayRects ) {
5148         for ( i = this.displayRects.length - 1; i >= 0; i-- ) {
5149             rect = this.displayRects[ i ];
5150             for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) {
5151                 if ( !this._levelRects[ level ] ) {
5152                     this._levelRects[ level ] = [];
5153                 }
5154                 this._levelRects[ level ].push( rect );
5155             }
5156         }
5157     }
5158 
5159 };
5160 
5161 $.extend( $.DziTileSource.prototype, $.TileSource.prototype, {
5162     
5163     /**
5164      * @function
5165      * @name OpenSeadragon.DziTileSource.prototype.getTileUrl
5166      * @param {Number} level
5167      * @param {Number} x
5168      * @param {Number} y
5169      */
5170     getTileUrl: function( level, x, y ) {
5171         return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' );
5172     },
5173 
5174     /**
5175      * @function
5176      * @name OpenSeadragon.DziTileSource.prototype.tileExists
5177      * @param {Number} level
5178      * @param {Number} x
5179      * @param {Number} y
5180      */
5181     tileExists: function( level, x, y ) {
5182         var rects = this._levelRects[ level ],
5183             rect,
5184             scale,
5185             xMin,
5186             yMin,
5187             xMax,
5188             yMax,
5189             i;
5190 
5191         if ( !rects || !rects.length ) {
5192             return true;
5193         }
5194 
5195         for ( i = rects.length - 1; i >= 0; i-- ) {
5196             rect = rects[ i ];
5197 
5198             if ( level < rect.minLevel || level > rect.maxLevel ) {
5199                 continue;
5200             }
5201 
5202             scale = this.getLevelScale( level );
5203             xMin = rect.x * scale;
5204             yMin = rect.y * scale;
5205             xMax = xMin + rect.width * scale;
5206             yMax = yMin + rect.height * scale;
5207 
5208             xMin = Math.floor( xMin / this.tileSize );
5209             yMin = Math.floor( yMin / this.tileSize );
5210             xMax = Math.ceil( xMax / this.tileSize );
5211             yMax = Math.ceil( yMax / this.tileSize );
5212 
5213             if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) {
5214                 return true;
5215             }
5216         }
5217 
5218         return false;
5219     }
5220 });
5221 
5222 
5223 
5224 }( OpenSeadragon ));
5225 
5226 (function( $ ){
5227 
5228 
5229 /**
5230  * The LegacyTileSource allows simple, traditional image pyramids to be loaded
5231  * into an OpenSeadragon Viewer.  Basically, this translates to the historically
5232  * common practice of starting with a 'master' image, maybe a tiff for example,
5233  * and generating a set of 'service' images like one or more thumbnails, a medium 
5234  * resolution image and a high resolution image in standard web formats like
5235  * png or jpg.
5236  * @class
5237  * @param {Array} files An array of file descriptions, each is an object with
5238  *      a 'url', a 'width', and a 'height'.  Overriding classes can expect more
5239  *      properties but these properties are sufficient for this implementation.
5240  *      Additionally, the files are required to be listed in order from
5241  *      smallest to largest.
5242  * @property {Number} aspectRatio
5243  * @property {Number} dimensions
5244  * @property {Number} tileSize
5245  * @property {Number} tileOverlap
5246  * @property {Number} minLevel
5247  * @property {Number} maxLevel
5248  * @property {Array} files
5249  */ 
5250 $.LegacyTileSource = function( files ) {
5251     var width   = files[ files.length - 1 ].width,
5252         height  = files[ files.length - 1 ].height;
5253 
5254     $.TileSource.apply( this, [ 
5255         width,      
5256         height, 
5257         Math.max( height, width ),  //tileSize
5258         0,                          //overlap
5259         0,                          //mimLevel
5260         files.length - 1            //maxLevel
5261     ] );
5262 
5263     this.files = files;
5264 };
5265 
5266 $.LegacyTileSource.prototype = {
5267     
5268     /**
5269      * @function
5270      * @param {Number} level
5271      */
5272     getLevelScale: function( level ) {
5273         var levelScale = NaN;
5274         if (  level >= this.minLevel && level <= this.maxLevel ){
5275             levelScale = 
5276                 this.files[ level ].width / 
5277                 this.files[ this.maxLevel ].width;
5278         } 
5279         return levelScale;
5280     },
5281 
5282     /**
5283      * @function
5284      * @param {Number} level
5285      */
5286     getNumTiles: function( level ) {
5287         var scale = this.getLevelScale( level );
5288         if ( scale ){
5289             return new $.Point( 1, 1 );
5290         } else {
5291             return new $.Point( 0, 0 );
5292         }
5293     },
5294 
5295     /**
5296      * @function
5297      * @param {Number} level
5298      */
5299     getPixelRatio: function( level ) {
5300         var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
5301             rx = 1.0 / imageSizeScaled.x,
5302             ry = 1.0 / imageSizeScaled.y;
5303 
5304         return new $.Point(rx, ry);
5305     },
5306 
5307     /**
5308      * @function
5309      * @param {Number} level
5310      * @param {OpenSeadragon.Point} point
5311      */
5312     getTileAtPoint: function( level, point ) {
5313         return new $.Point( 0, 0 );
5314     },
5315 
5316     /**
5317      * @function
5318      * @param {Number} level
5319      * @param {Number} x
5320      * @param {Number} y
5321      */
5322     getTileBounds: function( level, x, y ) {
5323         var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
5324             px = ( x === 0 ) ? 0 : this.files[ level ].width,
5325             py = ( y === 0 ) ? 0 : this.files[ level ].height,
5326             sx = this.files[ level ].width,
5327             sy = this.files[ level ].height,
5328             scale = 1.0 / ( this.width >= this.height  ? 
5329                 dimensionsScaled.y :
5330                 dimensionsScaled.x 
5331             );
5332 
5333         sx = Math.min( sx, dimensionsScaled.x - px );
5334         sy = Math.min( sy, dimensionsScaled.y - py );
5335 
5336         return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
5337     },
5338 
5339     /**
5340      * This method is not implemented by this class other than to throw an Error
5341      * announcing you have to implement it.  Because of the variety of tile 
5342      * server technologies, and various specifications for building image
5343      * pyramids, this method is here to allow easy integration.
5344      * @function
5345      * @param {Number} level
5346      * @param {Number} x
5347      * @param {Number} y
5348      * @throws {Error}
5349      */
5350     getTileUrl: function( level, x, y ) {
5351         var url = null;
5352         if( level >= this.minLevel && level <= this.maxLevel ){   
5353             url = this.files[ level ].url;
5354         }
5355         return url;
5356     },
5357 
5358     /**
5359      * @function
5360      * @param {Number} level
5361      * @param {Number} x
5362      * @param {Number} y
5363      */
5364     tileExists: function( level, x, y ) {
5365         var numTiles = this.getNumTiles( level );
5366         return  level >= this.minLevel && 
5367                 level <= this.maxLevel &&
5368                 x >= 0 && 
5369                 y >= 0 && 
5370                 x < numTiles.x && 
5371                 y < numTiles.y;
5372     }
5373 };
5374 
5375 }( OpenSeadragon ));
5376 
5377 
5378 (function( $ ){
5379 
5380 /**
5381  * An enumeration of button states including, REST, GROUP, HOVER, and DOWN
5382  * @static
5383  */
5384 $.ButtonState = {
5385     REST:   0,
5386     GROUP:  1,
5387     HOVER:  2,
5388     DOWN:   3
5389 };
5390 
5391 /**
5392  * Manages events, hover states for individual buttons, tool-tips, as well 
5393  * as fading the bottons out when the user has not interacted with them
5394  * for a specified period.
5395  * @class
5396  * @extends OpenSeadragon.EventHandler
5397  * @param {Object} options
5398  * @param {String} options.tooltip Provides context help for the button we the
5399  *  user hovers over it.
5400  * @param {String} options.srcRest URL of image to use in 'rest' state
5401  * @param {String} options.srcGroup URL of image to use in 'up' state
5402  * @param {String} options.srcHover URL of image to use in 'hover' state
5403  * @param {String} options.srcDown URL of image to use in 'domn' state
5404  * @param {Element} [options.element] Element to use as a container for the 
5405  *  button.
5406  * @property {String} tooltip Provides context help for the button we the
5407  *  user hovers over it.
5408  * @property {String} srcRest URL of image to use in 'rest' state
5409  * @property {String} srcGroup URL of image to use in 'up' state
5410  * @property {String} srcHover URL of image to use in 'hover' state
5411  * @property {String} srcDown URL of image to use in 'domn' state
5412  * @property {Object} config Configurable settings for this button. DEPRECATED.
5413  * @property {Element} [element] Element to use as a container for the 
5414  *  button.
5415  * @property {Number} fadeDelay How long to wait before fading
5416  * @property {Number} fadeLength How long should it take to fade the button.
5417  * @property {Number} fadeBeginTime When the button last began to fade.
5418  * @property {Boolean} shouldFade Whether this button should fade after user 
5419  *  stops interacting with the viewport.
5420     this.fadeDelay      = 0;      // begin fading immediately
5421     this.fadeLength     = 2000;   // fade over a period of 2 seconds
5422     this.fadeBeginTime  = null;
5423     this.shouldFade     = false;
5424  */
5425 $.Button = function( options ) {
5426 
5427     var _this = this;
5428 
5429     $.EventHandler.call( this );
5430 
5431     $.extend( true, this, {
5432         
5433         tooltip:            null,
5434         srcRest:            null,
5435         srcGroup:           null,
5436         srcHover:           null,
5437         srcDown:            null,
5438         clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
5439         clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
5440         // begin fading immediately
5441         fadeDelay:          0,  
5442         // fade over a period of 2 seconds    
5443         fadeLength:         2000,
5444         onPress:            null,
5445         onRelease:          null,
5446         onClick:            null,
5447         onEnter:            null,
5448         onExit:             null,
5449         onFocus:            null,
5450         onBlur:             null
5451 
5452     }, options );
5453 
5454     this.element        = options.element   || $.makeNeutralElement( "button" );
5455     this.element.href   = this.element.href || '#';
5456     
5457     //if the user has specified the element to bind the control to explicitly
5458     //then do not add the default control images
5459     if( !options.element ){
5460         this.imgRest      = $.makeTransparentImage( this.srcRest );
5461         this.imgGroup     = $.makeTransparentImage( this.srcGroup );
5462         this.imgHover     = $.makeTransparentImage( this.srcHover );
5463         this.imgDown      = $.makeTransparentImage( this.srcDown );
5464         
5465         this.element.appendChild( this.imgRest );
5466         this.element.appendChild( this.imgGroup );
5467         this.element.appendChild( this.imgHover );
5468         this.element.appendChild( this.imgDown );
5469 
5470         this.imgGroup.style.position = 
5471         this.imgHover.style.position = 
5472         this.imgDown.style.position  = 
5473             "absolute";
5474 
5475         this.imgGroup.style.top = 
5476         this.imgHover.style.top = 
5477         this.imgDown.style.top  = 
5478             "0px";
5479 
5480         this.imgGroup.style.left = 
5481         this.imgHover.style.left = 
5482         this.imgDown.style.left  = 
5483             "0px";
5484 
5485         this.imgHover.style.visibility = 
5486         this.imgDown.style.visibility  = 
5487             "hidden";
5488 
5489         if ( $.Browser.vendor == $.BROWSERS.FIREFOX  && $.Browser.version < 3 ){
5490             this.imgGroup.style.top = 
5491             this.imgHover.style.top = 
5492             this.imgDown.style.top  = 
5493                 "";
5494         }
5495     }
5496 
5497 
5498     this.addHandler( "onPress",     this.onPress );
5499     this.addHandler( "onRelease",   this.onRelease );
5500     this.addHandler( "onClick",     this.onClick );
5501     this.addHandler( "onEnter",     this.onEnter );
5502     this.addHandler( "onExit",      this.onExit );
5503     this.addHandler( "onFocus",     this.onFocus );
5504     this.addHandler( "onBlur",      this.onBlur );
5505 
5506     this.currentState = $.ButtonState.GROUP;
5507 
5508     this.fadeBeginTime  = null;
5509     this.shouldFade     = false;
5510 
5511     this.element.style.display  = "inline-block";
5512     this.element.style.position = "relative";
5513     this.element.title          = this.tooltip;
5514 
5515     this.tracker = new $.MouseTracker({
5516 
5517         element:            this.element, 
5518         clickTimeThreshold: this.clickTimeThreshold, 
5519         clickDistThreshold: this.clickDistThreshold,
5520 
5521         enterHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
5522             if ( buttonDownElement ) {
5523                 inTo( _this, $.ButtonState.DOWN );
5524                 _this.raiseEvent( "onEnter", _this );
5525             } else if ( !buttonDownAny ) {
5526                 inTo( _this, $.ButtonState.HOVER );
5527             }
5528         },
5529 
5530         focusHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
5531             this.enterHandler( tracker, position, buttonDownElement, buttonDownAny );
5532             _this.raiseEvent( "onFocus", _this );
5533         },
5534 
5535         exitHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
5536             outTo( _this, $.ButtonState.GROUP );
5537             if ( buttonDownElement ) {
5538                 _this.raiseEvent( "onExit", _this );
5539             }
5540         },
5541 
5542         blurHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
5543             this.exitHandler( tracker, position, buttonDownElement, buttonDownAny );
5544             _this.raiseEvent( "onBlur", _this );
5545         },
5546 
5547         pressHandler: function( tracker, position ) {
5548             inTo( _this, $.ButtonState.DOWN );
5549             _this.raiseEvent( "onPress", _this );
5550         },
5551 
5552         releaseHandler: function( tracker, position, insideElementPress, insideElementRelease ) {
5553             if ( insideElementPress && insideElementRelease ) {
5554                 outTo( _this, $.ButtonState.HOVER );
5555                 _this.raiseEvent( "onRelease", _this );
5556             } else if ( insideElementPress ) {
5557                 outTo( _this, $.ButtonState.GROUP );
5558             } else {
5559                 inTo( _this, $.ButtonState.HOVER );
5560             }
5561         },
5562 
5563         clickHandler: function( tracker, position, quick, shift ) {
5564             if ( quick ) {
5565                 _this.raiseEvent("onClick", _this);
5566             }
5567         },
5568 
5569         keyHandler: function( tracker, key ){
5570             //console.log( "%s : handling key %s!", _this.tooltip, key);
5571             if( 13 === key ){
5572                 _this.raiseEvent( "onClick", _this );
5573                 _this.raiseEvent( "onRelease", _this );
5574                 return false;
5575             }
5576             return true;
5577         }
5578 
5579     }).setTracking( true );
5580 
5581     outTo( this, $.ButtonState.REST );
5582 };
5583 
5584 $.extend( $.Button.prototype, $.EventHandler.prototype, {
5585 
5586     /**
5587      * TODO: Determine what this function is intended to do and if it's actually
5588      * useful as an API point.
5589      * @function
5590      * @name OpenSeadragon.Button.prototype.notifyGroupEnter
5591      */
5592     notifyGroupEnter: function() {
5593         inTo( this, $.ButtonState.GROUP );
5594     },
5595 
5596     /**
5597      * TODO: Determine what this function is intended to do and if it's actually
5598      * useful as an API point.
5599      * @function
5600      * @name OpenSeadragon.Button.prototype.notifyGroupExit
5601      */
5602     notifyGroupExit: function() {
5603         outTo( this, $.ButtonState.REST );
5604     },
5605 
5606     disable: function(){
5607         this.notifyGroupExit();
5608         this.element.disabled = true;
5609         $.setElementOpacity( this.element, 0.2, true );
5610     },
5611 
5612     enable: function(){
5613         this.element.disabled = false;
5614         $.setElementOpacity( this.element, 1.0, true );
5615         this.notifyGroupEnter();
5616     }
5617 
5618 });
5619 
5620 
5621 function scheduleFade( button ) {
5622     window.setTimeout(function(){
5623         updateFade( button );
5624     }, 20 );
5625 };
5626 
5627 function updateFade( button ) {
5628     var currentTime,
5629         deltaTime,
5630         opacity;
5631 
5632     if ( button.shouldFade ) {
5633         currentTime = +new Date();
5634         deltaTime   = currentTime - button.fadeBeginTime;
5635         opacity     = 1.0 - deltaTime / button.fadeLength;
5636         opacity     = Math.min( 1.0, opacity );
5637         opacity     = Math.max( 0.0, opacity );
5638 
5639         if( button.imgGroup ){
5640             $.setElementOpacity( button.imgGroup, opacity, true );
5641         }
5642         if ( opacity > 0 ) {
5643             // fade again
5644             scheduleFade( button );
5645         }
5646     }
5647 };
5648 
5649 function beginFading( button ) {
5650     button.shouldFade = true;
5651     button.fadeBeginTime = +new Date() + button.fadeDelay;
5652     window.setTimeout( function(){ 
5653         scheduleFade( button );
5654     }, button.fadeDelay );
5655 };
5656 
5657 function stopFading( button ) {
5658     button.shouldFade = false;
5659     if( button.imgGroup ){
5660         $.setElementOpacity( button.imgGroup, 1.0, true );
5661     }
5662 };
5663 
5664 function inTo( button, newState ) {
5665 
5666     if( button.element.disabled ){
5667         return;
5668     }
5669 
5670     if ( newState >= $.ButtonState.GROUP && 
5671          button.currentState == $.ButtonState.REST ) {
5672         stopFading( button );
5673         button.currentState = $.ButtonState.GROUP;
5674     }
5675 
5676     if ( newState >= $.ButtonState.HOVER && 
5677          button.currentState == $.ButtonState.GROUP ) {
5678         if( button.imgHover ){
5679             button.imgHover.style.visibility = "";
5680         }
5681         button.currentState = $.ButtonState.HOVER;
5682     }
5683 
5684     if ( newState >= $.ButtonState.DOWN && 
5685          button.currentState == $.ButtonState.HOVER ) {
5686         if( button.imgDown ){
5687             button.imgDown.style.visibility = "";
5688         }
5689         button.currentState = $.ButtonState.DOWN;
5690     }
5691 };
5692 
5693 
5694 function outTo( button, newState ) {
5695 
5696     if( button.element.disabled ){
5697         return;
5698     }
5699 
5700     if ( newState <= $.ButtonState.HOVER && 
5701          button.currentState == $.ButtonState.DOWN ) {
5702         if( button.imgDown ){
5703             button.imgDown.style.visibility = "hidden";
5704         }
5705         button.currentState = $.ButtonState.HOVER;
5706     }
5707 
5708     if ( newState <= $.ButtonState.GROUP && 
5709          button.currentState == $.ButtonState.HOVER ) {
5710         if( button.imgHover ){
5711             button.imgHover.style.visibility = "hidden";
5712         }
5713         button.currentState = $.ButtonState.GROUP;
5714     }
5715 
5716     if ( newState <= $.ButtonState.REST && 
5717          button.currentState == $.ButtonState.GROUP ) {
5718         beginFading( button );
5719         button.currentState = $.ButtonState.REST;
5720     }
5721 };
5722 
5723 
5724 
5725 }( OpenSeadragon ));
5726 
5727 (function( $ ){
5728 /**
5729  * Manages events on groups of buttons.
5730  * @class
5731  * @param {Object} options - a dictionary of settings applied against the entire 
5732  * group of buttons
5733  * @param {Array}    options.buttons Array of buttons
5734  * @param {Element}  [options.group]   Element to use as the container,
5735  * @param {Object}   options.config  Object with Viewer settings ( TODO: is 
5736  *  this actually used anywhere? )
5737  * @param {Function} [options.enter]   Function callback for when the mouse 
5738  *  enters group
5739  * @param {Function} [options.exit]    Function callback for when mouse leaves 
5740  *  the group
5741  * @param {Function} [options.release] Function callback for when mouse is 
5742  *  released
5743  * @property {Array} buttons - An array containing the buttons themselves.
5744  * @property {Element} element - The shared container for the buttons.
5745  * @property {Object} config - Configurable settings for the group of buttons.
5746  * @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross
5747  *  the group of buttons.
5748  **/
5749 $.ButtonGroup = function( options ) {
5750 
5751     $.extend( true, this, {
5752         buttons:            [],
5753         clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
5754         clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
5755         labelText:          ""
5756     }, options );
5757 
5758     // copy the botton elements
5759     var buttons = this.buttons.concat([]),   
5760         _this = this,
5761         i;
5762 
5763     this.element = options.element || $.makeNeutralElement( "fieldgroup" );
5764     
5765     if( !options.group ){
5766         this.label   = $.makeNeutralElement( "label" );
5767         //TODO: support labels for ButtonGroups
5768         //this.label.innerHTML = this.labelText;
5769         this.element.style.display = "inline-block";
5770         this.element.appendChild( this.label );
5771         for ( i = 0; i < buttons.length; i++ ) {
5772             this.element.appendChild( buttons[ i ].element );
5773         }
5774     }
5775 
5776     this.tracker = new $.MouseTracker({
5777         element:            this.element, 
5778         clickTimeThreshold: this.clickTimeThreshold, 
5779         clickDistThreshold: this.clickDistThreshold,
5780         enterHandler: function() {
5781             var i;
5782             for ( i = 0; i < _this.buttons.length; i++ ) {
5783                 _this.buttons[ i ].notifyGroupEnter();
5784             }
5785         },
5786         exitHandler: function() {
5787             var i,
5788                 buttonDownElement = arguments.length > 2 ? 
5789                     arguments[ 2 ] : 
5790                     null;
5791             if ( !buttonDownElement ) {
5792                 for ( i = 0; i < _this.buttons.length; i++ ) {
5793                     _this.buttons[ i ].notifyGroupExit();
5794                 }
5795             }
5796         },
5797         releaseHandler: function() {
5798             var i,
5799                 insideElementRelease = arguments.length > 3 ? 
5800                     arguments[ 3 ] : 
5801                     null;
5802             if ( !insideElementRelease ) {
5803                 for ( i = 0; i < _this.buttons.length; i++ ) {
5804                     _this.buttons[ i ].notifyGroupExit();
5805                 }
5806             }
5807         }
5808     }).setTracking( true );
5809 };
5810 
5811 $.ButtonGroup.prototype = {
5812 
5813     /**
5814      * TODO: Figure out why this is used on the public API and if a more useful
5815      * api can be created.
5816      * @function
5817      * @name OpenSeadragon.ButtonGroup.prototype.emulateEnter
5818      */
5819     emulateEnter: function() {
5820         this.tracker.enterHandler();
5821     },
5822 
5823     /**
5824      * TODO: Figure out why this is used on the public API and if a more useful
5825      * api can be created.
5826      * @function
5827      * @name OpenSeadragon.ButtonGroup.prototype.emulateExit
5828      */
5829     emulateExit: function() {
5830         this.tracker.exitHandler();
5831     }
5832 };
5833 
5834 
5835 }( OpenSeadragon ));
5836 
5837 (function( $ ){
5838     
5839 /**
5840  * A Rectangle really represents a 2x2 matrix where each row represents a
5841  * 2 dimensional vector component, the first is (x,y) and the second is 
5842  * (width, height).  The latter component implies the equation of a simple 
5843  * plane.
5844  *
5845  * @class
5846  * @param {Number} x The vector component 'x'.
5847  * @param {Number} y The vector component 'y'.
5848  * @param {Number} width The vector component 'height'.
5849  * @param {Number} height The vector component 'width'.
5850  * @property {Number} x The vector component 'x'.
5851  * @property {Number} y The vector component 'y'.
5852  * @property {Number} width The vector component 'width'.
5853  * @property {Number} height The vector component 'height'.
5854  */
5855 $.Rect = function( x, y, width, height ) {
5856     this.x = typeof ( x ) == "number" ? x : 0;
5857     this.y = typeof ( y ) == "number" ? y : 0;
5858     this.width  = typeof ( width )  == "number" ? width : 0;
5859     this.height = typeof ( height ) == "number" ? height : 0;
5860 };
5861 
5862 $.Rect.prototype = {
5863 
5864     /**
5865      * The aspect ratio is simply the ratio of width to height.
5866      * @function
5867      * @returns {Number} The ratio of width to height.
5868      */
5869     getAspectRatio: function() {
5870         return this.width / this.height;
5871     },
5872 
5873     /**
5874      * Provides the coordinates of the upper-left corner of the rectanglea s a
5875      * point.
5876      * @function
5877      * @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of
5878      *  the rectangle.
5879      */
5880     getTopLeft: function() {
5881         return new $.Point( this.x, this.y );
5882     },
5883 
5884     /**
5885      * Provides the coordinates of the bottom-right corner of the rectangle as a
5886      * point.
5887      * @function
5888      * @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of
5889      *  the rectangle.
5890      */
5891     getBottomRight: function() {
5892         return new $.Point(
5893             this.x + this.width, 
5894             this.y + this.height
5895         );
5896     },
5897 
5898     /**
5899      * Computes the center of the rectangle.
5900      * @function
5901      * @returns {OpenSeadragon.Point} The center of the rectangle as represnted 
5902      *  as represented by a 2-dimensional vector (x,y)
5903      */
5904     getCenter: function() {
5905         return new $.Point(
5906             this.x + this.width / 2.0,
5907             this.y + this.height / 2.0
5908         );
5909     },
5910 
5911     /**
5912      * Returns the width and height component as a vector OpenSeadragon.Point
5913      * @function
5914      * @returns {OpenSeadragon.Point} The 2 dimensional vector represnting the
5915      *  the width and height of the rectangle.
5916      */
5917     getSize: function() {
5918         return new $.Point( this.width, this.height );
5919     },
5920 
5921     /**
5922      * Determines if two Rectanlges have equivalent components.  
5923      * @function
5924      * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to.
5925      * @return {Boolean} 'true' if all components are equal, otherwise 'false'.
5926      */
5927     equals: function( other ) {
5928         return ( other instanceof $.Rect ) &&
5929             ( this.x === other.x ) && 
5930             ( this.y === other.y ) &&
5931             ( this.width === other.width ) && 
5932             ( this.height === other.height );
5933     },
5934 
5935     /**
5936      * Provides a string representation of the retangle which is useful for 
5937      * debugging.
5938      * @function
5939      * @returns {String} A string representation of the rectangle.
5940      */
5941     toString: function() {
5942         return "[" + 
5943             this.x + "," + 
5944             this.y + "," + 
5945             this.width + "x" +
5946             this.height + 
5947         "]";
5948     }
5949 };
5950 
5951 
5952 }( OpenSeadragon ));
5953 
5954 (function( $ ){
5955 
5956 /**
5957  * A display rectanlge is very similar to the OpenSeadragon.Rect but adds two
5958  * fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels
5959  * for this rectangle.
5960  * @class
5961  * @extends OpenSeadragon.Rect
5962  * @param {Number} x The vector component 'x'.
5963  * @param {Number} y The vector component 'y'.
5964  * @param {Number} width The vector component 'height'.
5965  * @param {Number} height The vector component 'width'.
5966  * @param {Number} minLevel The lowest zoom level supported.
5967  * @param {Number} maxLevel The highest zoom level supported.
5968  * @property {Number} minLevel The lowest zoom level supported.
5969  * @property {Number} maxLevel The highest zoom level supported.
5970  */
5971 $.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) {
5972     $.Rect.apply( this, [ x, y, width, height ] );
5973 
5974     this.minLevel = minLevel;
5975     this.maxLevel = maxLevel;
5976 }
5977 
5978 $.extend( $.DisplayRect.prototype, $.Rect.prototype );
5979 
5980 }( OpenSeadragon ));
5981 
5982 (function( $ ){
5983     
5984 /**
5985  * @class
5986  * @param {Object} options - Spring configuration settings.
5987  * @param {Number} options.initial - Initial value of spring, default to 0 so 
5988  *  spring is not in motion initally by default.
5989  * @param {Number} options.springStiffness - Spring stiffness.
5990  * @param {Number} options.animationTime - Animation duration per spring.
5991  * 
5992  * @property {Number} initial - Initial value of spring, default to 0 so 
5993  *  spring is not in motion initally by default.
5994  * @property {Number} springStiffness - Spring stiffness.
5995  * @property {Number} animationTime - Animation duration per spring.
5996  * @property {Object} current 
5997  * @property {Number} start
5998  * @property {Number} target
5999  */
6000 $.Spring = function( options ) {
6001     var args = arguments;
6002 
6003     if( typeof( options ) != 'object' ){
6004         //allows backward compatible use of ( initialValue, config ) as 
6005         //constructor parameters
6006         options = {
6007             initial: args.length && typeof ( args[ 0 ] ) == "number" ? 
6008                 args[ 0 ] : 
6009                 0,
6010             springStiffness: args.length > 1 ? 
6011                 args[ 1 ].springStiffness : 
6012                 5.0,
6013             animationTime: args.length > 1 ? 
6014                 args[ 1 ].animationTime : 
6015                 1.5
6016         };
6017     }
6018 
6019     $.extend( true, this, options);
6020 
6021 
6022     this.current = {
6023         value: typeof ( this.initial ) == "number" ? 
6024             this.initial : 
6025             0,
6026         time:  new Date().getTime() // always work in milliseconds
6027     };
6028 
6029     this.start = {
6030         value: this.current.value,
6031         time:  this.current.time
6032     };
6033 
6034     this.target = {
6035         value: this.current.value,
6036         time:  this.current.time
6037     };
6038 };
6039 
6040 $.Spring.prototype = {
6041 
6042     /**
6043      * @function
6044      * @param {Number} target
6045      */
6046     resetTo: function( target ) {
6047         this.target.value = target;
6048         this.target.time  = this.current.time;
6049         this.start.value  = this.target.value;
6050         this.start.time   = this.target.time;
6051     },
6052 
6053     /**
6054      * @function
6055      * @param {Number} target
6056      */
6057     springTo: function( target ) {
6058         this.start.value  = this.current.value;
6059         this.start.time   = this.current.time;
6060         this.target.value = target;
6061         this.target.time  = this.start.time + 1000 * this.animationTime;
6062     },
6063 
6064     /**
6065      * @function
6066      * @param {Number} delta
6067      */
6068     shiftBy: function( delta ) {
6069         this.start.value  += delta;
6070         this.target.value += delta;
6071     },
6072 
6073     /**
6074      * @function
6075      */
6076     update: function() {
6077         this.current.time  = new Date().getTime();
6078         this.current.value = (this.current.time >= this.target.time) ? 
6079             this.target.value :
6080             this.start.value + 
6081                 ( this.target.value - this.start.value ) *
6082                 transform( 
6083                     this.springStiffness, 
6084                     ( this.current.time - this.start.time ) / 
6085                     ( this.target.time  - this.start.time )
6086                 );
6087     }
6088 }
6089 
6090 /**
6091  * @private
6092  */
6093 function transform( stiffness, x ) {
6094     return ( 1.0 - Math.exp( stiffness * -x ) ) / 
6095         ( 1.0 - Math.exp( -stiffness ) );
6096 };
6097 
6098 }( OpenSeadragon ));
6099 
6100 (function( $ ){
6101     
6102 /**
6103  * @class
6104  * @param {Number} level The zoom level this tile belongs to.
6105  * @param {Number} x The vector component 'x'.
6106  * @param {Number} y The vector component 'y'.
6107  * @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized 
6108  *      coordinates.
6109  * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has 
6110  *      this tile failed to load? )
6111  * @param {String} url The URL of this tile's image.
6112  *
6113  * @property {Number} level The zoom level this tile belongs to.
6114  * @property {Number} x The vector component 'x'.
6115  * @property {Number} y The vector component 'y'.
6116  * @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized 
6117  *      coordinates
6118  * @property {Boolean} exists Is this tile a part of a sparse image? ( Also has 
6119  *      this tile failed to load?
6120  * @property {String} url The URL of this tile's image.
6121  * @property {Boolean} loaded Is this tile loaded?
6122  * @property {Boolean} loading Is this tile loading
6123  * @property {Element} element The HTML element for this tile
6124  * @property {Image} image The Image object for this tile
6125  * @property {String} style The alias of this.element.style.
6126  * @property {String} position This tile's position on screen, in pixels.
6127  * @property {String} size This tile's size on screen, in pixels
6128  * @property {String} blendStart The start time of this tile's blending
6129  * @property {String} opacity The current opacity this tile should be.
6130  * @property {String} distance The distance of this tile to the viewport center
6131  * @property {String} visibility The visibility score of this tile.
6132  * @property {Boolean} beingDrawn Whether this tile is currently being drawn
6133  * @property {Number} lastTouchTime Timestamp the tile was last touched.
6134  */
6135 $.Tile = function(level, x, y, bounds, exists, url) {
6136     this.level   = level;
6137     this.x       = x;
6138     this.y       = y;
6139     this.bounds  = bounds;
6140     this.exists  = exists;
6141     this.url     = url;
6142     this.loaded  = false;
6143     this.loading = false;
6144 
6145     this.element    = null;
6146     this.image      = null;
6147 
6148     this.style      = null;
6149     this.position   = null;
6150     this.size       = null;
6151     this.blendStart = null;
6152     this.opacity    = null;
6153     this.distance   = null;
6154     this.visibility = null;
6155 
6156     this.beingDrawn     = false;
6157     this.lastTouchTime  = 0;
6158 };
6159 
6160 $.Tile.prototype = {
6161     
6162     /**
6163      * Provides a string representation of this tiles level and (x,y) 
6164      * components.
6165      * @function
6166      * @returns {String}
6167      */
6168     toString: function() {
6169         return this.level + "/" + this.x + "_" + this.y;
6170     },
6171 
6172     /**
6173      * Renders the tile in an html container.
6174      * @function
6175      * @param {Element} container
6176      */
6177     drawHTML: function( container ) {
6178 
6179         var position = this.position.apply( Math.floor ),
6180             size     = this.size.apply( Math.ceil );
6181 
6182         if ( !this.loaded || !this.image ) {
6183             $.console.warn(
6184                 "Attempting to draw tile %s when it's not yet loaded.",
6185                 this.toString()
6186             );
6187             return;
6188         }
6189 
6190         if ( !this.element ) {
6191             this.element        = $.makeNeutralElement("img");
6192             this.element.src    = this.url;
6193             this.style          = this.element.style;
6194 
6195             this.style.position            = "absolute";
6196             this.style.msInterpolationMode = "nearest-neighbor";
6197         }
6198 
6199 
6200         if ( this.element.parentNode != container ) {
6201             container.appendChild( this.element );
6202         }
6203 
6204         this.element.style.left    = position.x + "px";
6205         this.element.style.top     = position.y + "px";
6206         this.element.style.width   = size.x + "px";
6207         this.element.style.height  = size.y + "px";
6208 
6209         $.setElementOpacity( this.element, this.opacity );
6210 
6211     },
6212 
6213     /**
6214      * Renders the tile in a canvas-based context.
6215      * @function
6216      * @param {Canvas} context
6217      */
6218     drawCanvas: function( context ) {
6219 
6220         var position = this.position,
6221             size     = this.size;
6222 
6223         if ( !this.loaded || !this.image ) {
6224             $.console.warn(
6225                 "Attempting to draw tile %s when it's not yet loaded.",
6226                 this.toString()
6227             );
6228             return;
6229         }
6230         context.globalAlpha = this.opacity;
6231         context.drawImage( this.image, position.x, position.y, size.x, size.y );
6232     },
6233 
6234     /**
6235      * Removes tile from it's contianer.
6236      * @function
6237      */
6238     unload: function() {
6239         if ( this.element && this.element.parentNode ) {
6240             this.element.parentNode.removeChild( this.element );
6241         }
6242 
6243         this.element    = null;
6244         this.image   = null;
6245         this.loaded  = false;
6246         this.loading = false;
6247     }
6248 };
6249 
6250 }( OpenSeadragon ));
6251 
6252 (function( $ ){
6253 
6254     /**
6255      * An enumeration of positions that an overlay may be assigned relative
6256      * to the viewport including CENTER, TOP_LEFT (default), TOP, TOP_RIGHT,
6257      * RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, and LEFT.
6258      * @static
6259      */
6260     $.OverlayPlacement = {
6261         CENTER:       0,
6262         TOP_LEFT:     1,
6263         TOP:          2,
6264         TOP_RIGHT:    3,
6265         RIGHT:        4,
6266         BOTTOM_RIGHT: 5,
6267         BOTTOM:       6,
6268         BOTTOM_LEFT:  7,
6269         LEFT:         8
6270     };
6271 
6272     /**
6273      * An Overlay provides a 
6274      * @class
6275      */
6276     $.Overlay = function( element, location, placement ) {
6277         this.element    = element;
6278         this.scales     = location instanceof $.Rect;
6279         this.bounds     = new $.Rect(
6280             location.x, 
6281             location.y,
6282             location.width, 
6283             location.height
6284         );
6285         this.position   = new $.Point(
6286             location.x, 
6287             location.y
6288         );
6289         this.size       = new $.Point(
6290             location.width, 
6291             location.height
6292         );
6293         this.style      = element.style;
6294         // rects are always top-left
6295         this.placement  = location instanceof $.Point ? 
6296             placement : 
6297             $.OverlayPlacement.TOP_LEFT;    
6298     };
6299 
6300     $.Overlay.prototype = {
6301 
6302         /**
6303          * @function
6304          * @param {OpenSeadragon.OverlayPlacement} position
6305          * @param {OpenSeadragon.Point} size
6306          */
6307         adjust: function( position, size ) {
6308             switch ( this.placement ) {
6309                 case $.OverlayPlacement.TOP_LEFT:
6310                     break;
6311                 case $.OverlayPlacement.TOP:
6312                     position.x -= size.x / 2;
6313                     break;
6314                 case $.OverlayPlacement.TOP_RIGHT:
6315                     position.x -= size.x;
6316                     break;
6317                 case $.OverlayPlacement.RIGHT:
6318                     position.x -= size.x;
6319                     position.y -= size.y / 2;
6320                     break;
6321                 case $.OverlayPlacement.BOTTOM_RIGHT:
6322                     position.x -= size.x;
6323                     position.y -= size.y;
6324                     break;
6325                 case $.OverlayPlacement.BOTTOM:
6326                     position.x -= size.x / 2;
6327                     position.y -= size.y;
6328                     break;
6329                 case $.OverlayPlacement.BOTTOM_LEFT:
6330                     position.y -= size.y;
6331                     break;
6332                 case $.OverlayPlacement.LEFT:
6333                     position.y -= size.y / 2;
6334                     break;
6335                 case $.OverlayPlacement.CENTER:
6336                 default:
6337                     position.x -= size.x / 2;
6338                     position.y -= size.y / 2;
6339                     break;
6340             }
6341         },
6342 
6343         /**
6344          * @function
6345          */
6346         destroy: function() {
6347             var element = this.element,
6348                 style   = this.style;
6349 
6350             if ( element.parentNode ) {
6351                 element.parentNode.removeChild( element );
6352             }
6353 
6354             style.top = "";
6355             style.left = "";
6356             style.position = "";
6357 
6358             if ( this.scales ) {
6359                 style.width = "";
6360                 style.height = "";
6361             }
6362         },
6363 
6364         /**
6365          * @function
6366          * @param {Element} container
6367          */
6368         drawHTML: function( container ) {
6369             var element = this.element,
6370                 style   = this.style,
6371                 scales  = this.scales,
6372                 position,
6373                 size;
6374 
6375             if ( element.parentNode != container ) {
6376                 container.appendChild( element );
6377             }
6378 
6379             if ( !scales ) {
6380                 this.size = $.getElementSize( element );
6381             }
6382 
6383             position = this.position;
6384             size     = this.size;
6385 
6386             this.adjust( position, size );
6387 
6388             position = position.apply( Math.floor );
6389             size     = size.apply( Math.ceil );
6390 
6391             style.left     = position.x + "px";
6392             style.top      = position.y + "px";
6393             style.position = "absolute";
6394 
6395             if ( scales ) {
6396                 style.width  = size.x + "px";
6397                 style.height = size.y + "px";
6398             }
6399         },
6400 
6401         /**
6402          * @function
6403          * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location
6404          * @param {OpenSeadragon.OverlayPlacement} position
6405          */
6406         update: function( location, placement ) {
6407             this.scales     = location instanceof $.Rect;
6408             this.bounds     = new $.Rect( 
6409                 location.x, 
6410                 location.y, 
6411                 location.width, 
6412                 location.height
6413             );
6414             // rects are always top-left
6415             this.placement  = location instanceof $.Point ?
6416                 placement : 
6417                 $.OverlayPlacement.TOP_LEFT;    
6418         }
6419 
6420     };
6421 
6422 }( OpenSeadragon ));
6423 
6424 (function( $ ){
6425     
6426 var TIMEOUT             = 5000,
6427     DEVICE_SCREEN       = $.getWindowSize(),
6428     BROWSER             = $.Browser.vendor,
6429     BROWSER_VERSION     = $.Browser.version,
6430 
6431     SUBPIXEL_RENDERING = (
6432         ( BROWSER == $.BROWSERS.FIREFOX ) ||
6433         ( BROWSER == $.BROWSERS.OPERA )   ||
6434         ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) ||
6435         ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) ||
6436         ( BROWSER == $.BROWSERS.IE     && BROWSER_VERSION >= 9 )
6437     ), 
6438 
6439     USE_CANVAS = SUBPIXEL_RENDERING 
6440         && !( DEVICE_SCREEN.x < 600 || DEVICE_SCREEN.y < 600 ) 
6441         && !( navigator.appVersion.match( 'Mobile' ) )
6442         && $.isFunction( document.createElement( "canvas" ).getContext );
6443 
6444 //console.error( 'USE_CANVAS ' + USE_CANVAS );
6445 
6446 /**
6447  * @class
6448  * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
6449  * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
6450  * @param {Element} element - Reference to Viewer 'canvas'.
6451  * @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
6452  * @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
6453  * @property {Element} container - Reference to Viewer 'canvas'.
6454  * @property {Element|Canvas} canvas - TODO
6455  * @property {CanvasContext} context - TODO
6456  * @property {Object} config - Reference to Viewer config.
6457  * @property {Number} downloading - How many images are currently being loaded in parallel.
6458  * @property {Number} normHeight - Ratio of zoomable image height to width.
6459  * @property {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile.
6460  * @property {Array} tilesLoaded - An unordered list of Tiles with loaded images.
6461  * @property {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
6462  * @property {Array} overlays - An unordered list of Overlays added.
6463  * @property {Array} lastDrawn - An unordered list of Tiles drawn last frame.
6464  * @property {Number} lastResetTime - Last time for which the drawer was reset.
6465  * @property {Boolean} midUpdate - Is the drawer currently updating the viewport?
6466  * @property {Boolean} updateAgain - Does the drawer need to update the viewort again?
6467  * @property {Element} element - DEPRECATED Alias for container.
6468  */
6469 $.Drawer = function( options ) {
6470     
6471     //backward compatibility for positional args while prefering more 
6472     //idiomatic javascript options object as the only argument
6473     var args  = arguments,
6474         i;
6475 
6476     if( !$.isPlainObject( options ) ){
6477         options = {
6478             source:     args[ 0 ],
6479             viewport:   args[ 1 ],
6480             element:    args[ 2 ]
6481         };
6482     }
6483 
6484     $.extend( true, this, {
6485 
6486         //internal state properties
6487         downloading:    0,
6488         tilesMatrix:    {},
6489         tilesLoaded:    [],
6490         coverage:       {},
6491         lastDrawn:      [],
6492         lastResetTime:  0,
6493         midUpdate:      false,
6494         updateAgain:    true,
6495 
6496         //internal state / configurable settings 
6497         overlays:       [],
6498 
6499         //configurable settings
6500         maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount,
6501         imageLoaderLimit:   $.DEFAULT_SETTINGS.imageLoaderLimit,
6502         minZoomImageRatio:  $.DEFAULT_SETTINGS.minZoomImageRatio,
6503         wrapHorizontal:     $.DEFAULT_SETTINGS.wrapHorizontal,
6504         wrapVertical:       $.DEFAULT_SETTINGS.wrapVertical,
6505         immediateRender:    $.DEFAULT_SETTINGS.immediateRender,
6506         blendTime:          $.DEFAULT_SETTINGS.blendTime,
6507         alwaysBlend:        $.DEFAULT_SETTINGS.alwaysBlend,
6508         minPixelRatio:      $.DEFAULT_SETTINGS.minPixelRatio
6509 
6510     }, options );
6511 
6512     this.container  = $.getElement( this.element );
6513     this.canvas     = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" );
6514     this.context    = USE_CANVAS ? this.canvas.getContext( "2d" ) : null;
6515     this.normHeight = this.source.dimensions.y / this.source.dimensions.x;
6516     this.element    = this.container;
6517 
6518     
6519     this.canvas.style.width     = "100%";
6520     this.canvas.style.height    = "100%";
6521     this.canvas.style.position  = "absolute";
6522     
6523     // explicit left-align
6524     this.container.style.textAlign = "left";
6525     this.container.appendChild( this.canvas );
6526 
6527     //create the correct type of overlay by convention if the overlays
6528     //are not already OpenSeadragon.Overlays
6529     for( i = 0; i < this.overlays.length; i++ ){
6530         if( $.isPlainObject( this.overlays[ i ] ) ){
6531             
6532             (function( _this, overlay ){
6533                 
6534                 var link  = document.createElement("a"),
6535                     rect = new $.Rect(
6536                         overlay.x, 
6537                         overlay.y, 
6538                         overlay.width, 
6539                         overlay.height
6540                     ),
6541                     id = Math.floor(Math.random()*10000000);
6542 
6543                 link.href      = "#/overlay/"+id;
6544                 link.id        = id;
6545                 link.className = overlay.className ?
6546                     overlay.className :
6547                     "openseadragon-overlay";
6548 
6549                 _this.overlays[ i ] = new $.Overlay( link, rect );
6550 
6551             }( this, this.overlays[ i ] ));
6552 
6553         } else if ( $.isFunction( this.overlays[ i ] ) ){
6554             
6555         }
6556     }
6557 
6558     //this.profiler    = new $.Profiler();
6559 };
6560 
6561 $.Drawer.prototype = {
6562 
6563     /**
6564      * Adds an html element as an overlay to the current viewport.  Useful for
6565      * highlighting words or areas of interest on an image or other zoomable
6566      * interface.
6567      * @method
6568      * @param {Element|String} element - A reference to an element or an id for
6569      *      the element which will overlayed.
6570      * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or 
6571      *      rectangle which will be overlayed.
6572      * @param {OpenSeadragon.OverlayPlacement} placement - The position of the 
6573      *      viewport which the location coordinates will be treated as relative 
6574      *      to. 
6575      */
6576     addOverlay: function( element, location, placement ) {
6577         element = $.getElement( element );
6578 
6579         if ( getOverlayIndex( this.overlays, element ) >= 0 ) {
6580             // they're trying to add a duplicate overlay
6581             return;     
6582         }
6583 
6584         this.overlays.push( new $.Overlay( element, location, placement ) );
6585         this.updateAgain = true;
6586     },
6587 
6588     /**
6589      * Updates the overlay represented by the reference to the element or  
6590      * element id moving it to the new location, relative to the new placement.
6591      * @method
6592      * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or 
6593      *      rectangle which will be overlayed.
6594      * @param {OpenSeadragon.OverlayPlacement} placement - The position of the 
6595      *      viewport which the location coordinates will be treated as relative 
6596      *      to. 
6597      */
6598     updateOverlay: function( element, location, placement ) {
6599         var i;
6600 
6601         element = $.getElement( element );
6602         i = getOverlayIndex( this.overlays, element );
6603 
6604         if ( i >= 0 ) {
6605             this.overlays[ i ].update( location, placement );
6606             this.updateAgain = true;
6607         }
6608     },
6609 
6610     /**
6611      * Removes and overlay identified by the reference element or element id 
6612      *      and schedules and update.
6613      * @method
6614      * @param {Element|String} element - A reference to the element or an 
6615      *      element id which represent the ovelay content to be removed.
6616      */
6617     removeOverlay: function( element ) {
6618         var i;
6619 
6620         element = $.getElement( element );
6621         i = getOverlayIndex( this.overlays, element );
6622 
6623         if ( i >= 0 ) {
6624             this.overlays[ i ].destroy();
6625             this.overlays.splice( i, 1 );
6626             this.updateAgain = true;
6627         }
6628     },
6629 
6630     /**
6631      * Removes all currently configured Overlays from this Drawer and schedules
6632      *      and update.
6633      * @method
6634      */
6635     clearOverlays: function() {
6636         while ( this.overlays.length > 0 ) {
6637             this.overlays.pop().destroy();
6638             this.updateAgain = true;
6639         }
6640     },
6641 
6642 
6643     /**
6644      * Returns whether the Drawer is scheduled for an update at the 
6645      *      soonest possible opportunity.
6646      * @method
6647      * @returns {Boolean} - Whether the Drawer is scheduled for an update at the 
6648      *      soonest possible opportunity.
6649      */
6650     needsUpdate: function() {
6651         return this.updateAgain;
6652     },
6653 
6654     /**
6655      * Returns the total number of tiles that have been loaded by this Drawer.
6656      * @method
6657      * @returns {Number} - The total number of tiles that have been loaded by 
6658      *      this Drawer.
6659      */
6660     numTilesLoaded: function() {
6661         return this.tilesLoaded.length;
6662     },
6663 
6664     /**
6665      * Clears all tiles and triggers an update on the next call to 
6666      * Drawer.prototype.update().
6667      * @method
6668      */
6669     reset: function() {
6670         clearTiles( this );
6671         this.lastResetTime = +new Date();
6672         this.updateAgain = true;
6673     },
6674 
6675     /**
6676      * Forces the Drawer to update.
6677      * @method
6678      */
6679     update: function() {
6680         //this.profiler.beginUpdate();
6681         this.midUpdate = true;
6682         updateViewport( this );
6683         this.midUpdate = false;
6684         //this.profiler.endUpdate();
6685     },
6686 
6687     /**
6688      * Used internally to load images when required.  May also be used to 
6689      * preload a set of images so the browser will have them available in 
6690      * the local cache to optimize user experience in certain cases. Because
6691      * the number of parallel image loads is configurable, if too many images
6692      * are currently being loaded, the request will be ignored.  Since by 
6693      * default drawer.imageLoaderLimit is 0, the native browser parallel 
6694      * image loading policy will be used.
6695      * @method
6696      * @param {String} src - The url of the image to load.
6697      * @param {Function} callback - The function that will be called with the
6698      *      Image object as the only parameter, whether on 'load' or on 'abort'.
6699      *      For now this means the callback is expected to distinguish between
6700      *      error and success conditions by inspecting the Image object.
6701      * @return {Boolean} loading - Wheter the request was submitted or ignored 
6702      *      based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit.
6703      */
6704     loadImage: function( src, callback ) {
6705         var _this = this,
6706             loading = false,
6707             image,
6708             jobid,
6709             complete;
6710         
6711         if ( !this.imageLoaderLimit || 
6712               this.downloading < this.imageLoaderLimit ) {
6713             
6714             this.downloading++;
6715 
6716             image = new Image();
6717 
6718             complete = function( imagesrc ){
6719                 _this.downloading--;
6720                 if (typeof ( callback ) == "function") {
6721                     try {
6722                         callback( image );
6723                     } catch ( e ) {
6724                         $.console.error(
6725                             "%s while executing %s callback: %s", 
6726                             e.name,
6727                             src,
6728                             e.message,
6729                             e
6730                         );
6731                     }
6732                 }
6733             };
6734 
6735             image.onload = function(){
6736                 finishLoadingImage( image, complete, true );
6737             };
6738 
6739             image.onabort = image.onerror = function(){
6740                 finishLoadingImage( image, complete, false );
6741             };
6742 
6743             jobid = window.setTimeout( function(){
6744                 finishLoadingImage( image, complete, false, jobid );
6745             }, TIMEOUT );
6746 
6747             loading   = true;
6748             image.src = src;
6749         }
6750 
6751         return loading;
6752     }
6753 };
6754 
6755 /**
6756  * @private
6757  * @inner
6758  * Pretty much every other line in this needs to be documented so its clear
6759  * how each piece of this routine contributes to the drawing process.  That's
6760  * why there are so many TODO's inside this function.
6761  */
6762 function updateViewport( drawer ) {
6763     
6764     drawer.updateAgain = false;
6765 
6766     var tile,
6767         level,
6768         best            = null,
6769         haveDrawn       = false,
6770         currentTime     = +new Date(),
6771         viewportSize    = drawer.viewport.getContainerSize(),
6772         viewportBounds  = drawer.viewport.getBounds( true ),
6773         viewportTL      = viewportBounds.getTopLeft(),
6774         viewportBR      = viewportBounds.getBottomRight(),
6775         zeroRatioC      = drawer.viewport.deltaPixelsFromPoints( 
6776             drawer.source.getPixelRatio( 0 ), 
6777             true
6778         ).x,
6779         lowestLevel     = Math.max(
6780             drawer.source.minLevel, 
6781             Math.floor( 
6782                 Math.log( drawer.minZoomImageRatio ) / 
6783                 Math.log( 2 )
6784             )
6785         ),
6786         highestLevel    = Math.min(
6787             drawer.source.maxLevel,
6788             Math.floor( 
6789                 Math.log( zeroRatioC / drawer.minPixelRatio ) / 
6790                 Math.log( 2 )
6791             )
6792         ),
6793         renderPixelRatioC,
6794         renderPixelRatioT,
6795         zeroRatioT,
6796         optimalRatio,
6797         levelOpacity,
6798         levelVisibility;
6799 
6800     //TODO
6801     while ( drawer.lastDrawn.length > 0 ) {
6802         tile = drawer.lastDrawn.pop();
6803         tile.beingDrawn = false;
6804     }
6805 
6806     //TODO
6807     drawer.canvas.innerHTML   = "";
6808     if ( USE_CANVAS ) {
6809         drawer.canvas.width   = viewportSize.x;
6810         drawer.canvas.height  = viewportSize.y;
6811         drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y );
6812     }
6813 
6814     //TODO
6815     if  ( !drawer.wrapHorizontal && 
6816         ( viewportBR.x < 0 || viewportTL.x > 1 ) ) {
6817         return;
6818     } else if 
6819         ( !drawer.wrapVertical &&
6820         ( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) {
6821         return;
6822     }
6823 
6824     //TODO
6825     if ( !drawer.wrapHorizontal ) {
6826         viewportTL.x = Math.max( viewportTL.x, 0 );
6827         viewportBR.x = Math.min( viewportBR.x, 1 );
6828     }
6829     if ( !drawer.wrapVertical ) {
6830         viewportTL.y = Math.max( viewportTL.y, 0 );
6831         viewportBR.y = Math.min( viewportBR.y, drawer.normHeight );
6832     }
6833 
6834     //TODO
6835     lowestLevel = Math.min( lowestLevel, highestLevel );
6836 
6837     //TODO
6838     for ( level = highestLevel; level >= lowestLevel; level-- ) {
6839 
6840         //Avoid calculations for draw if we have already drawn this
6841         renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints(
6842             drawer.source.getPixelRatio( level ), 
6843             true
6844         ).x;
6845 
6846         if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) ||
6847              ( level == lowestLevel ) ) {
6848             drawLevel = true;
6849             haveDrawn = true;
6850         } else if ( !haveDrawn ) {
6851             continue;
6852         }
6853 
6854         renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints(
6855             drawer.source.getPixelRatio( level ), 
6856             false
6857         ).x;
6858 
6859         zeroRatioT      = drawer.viewport.deltaPixelsFromPoints( 
6860             drawer.source.getPixelRatio( 0 ), 
6861             false
6862         ).x;
6863         
6864         optimalRatio    = drawer.immediateRender ? 
6865             1 : 
6866             zeroRatioT;
6867 
6868         levelOpacity    = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
6869         
6870         levelVisibility = optimalRatio / Math.abs( 
6871             optimalRatio - renderPixelRatioT 
6872         );
6873 
6874         //TODO
6875         best = updateLevel(
6876             drawer, 
6877             haveDrawn,
6878             level, 
6879             levelOpacity,
6880             levelVisibility,
6881             viewportTL, 
6882             viewportBR, 
6883             currentTime, 
6884             best 
6885         );
6886 
6887         //TODO
6888         if (  providesCoverage( drawer.coverage, level ) ) {
6889             break;
6890         }
6891     }
6892 
6893     //TODO
6894     drawTiles( drawer, drawer.lastDrawn );
6895     drawOverlays( drawer.viewport, drawer.overlays, drawer.container );
6896 
6897     //TODO
6898     if ( best ) {
6899         loadTile( drawer, best, currentTime );
6900         // because we haven't finished drawing, so
6901         drawer.updateAgain = true; 
6902     }
6903 };
6904 
6905 
6906 function updateLevel( drawer, haveDrawn, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
6907     
6908     var x, y,
6909         tileTL,
6910         tileBR,
6911         numberOfTiles,
6912         viewportCenter  = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() );
6913 
6914 
6915     //OK, a new drawing so do your calculations
6916     tileTL    = drawer.source.getTileAtPoint( level, viewportTL );
6917     tileBR    = drawer.source.getTileAtPoint( level, viewportBR );
6918     numberOfTiles  = drawer.source.getNumTiles( level );
6919 
6920     resetCoverage( drawer.coverage, level );
6921 
6922     if ( !drawer.wrapHorizontal ) {
6923         tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
6924     }
6925     if ( !drawer.wrapVertical ) {
6926         tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
6927     }
6928 
6929     for ( x = tileTL.x; x <= tileBR.x; x++ ) {
6930         for ( y = tileTL.y; y <= tileBR.y; y++ ) {
6931 
6932             best = updateTile( 
6933                 drawer,
6934                 drawLevel,
6935                 haveDrawn,
6936                 x, y,
6937                 level,
6938                 levelOpacity,
6939                 levelVisibility,
6940                 viewportCenter,
6941                 numberOfTiles,
6942                 currentTime,
6943                 best
6944             );
6945 
6946         }
6947     }
6948     return best;
6949 };
6950 
6951 function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
6952     
6953     var tile = getTile( 
6954             x, y, 
6955             level, 
6956             drawer.source,
6957             drawer.tilesMatrix,
6958             currentTime, 
6959             numberOfTiles, 
6960             drawer.normHeight 
6961         ),
6962         drawTile = drawLevel,
6963         newbest;
6964 
6965     setCoverage( drawer.coverage, level, x, y, false );
6966 
6967     if ( !tile.exists ) {
6968         return best;
6969     }
6970 
6971     if ( haveDrawn && !drawTile ) {
6972         if ( isCovered( drawer.coverage, level, x, y ) ) {
6973             setCoverage( drawer.coverage, level, x, y, true );
6974         } else {
6975             drawTile = true;
6976         }
6977     }
6978 
6979     if ( !drawTile ) {
6980         return best;
6981     }
6982 
6983     positionTile( 
6984         tile, 
6985         drawer.source.tileOverlap,
6986         drawer.viewport,
6987         viewportCenter, 
6988         levelVisibility 
6989     );
6990 
6991     if ( tile.loaded ) {
6992         
6993         drawer.updateAgain = blendTile(
6994             drawer,
6995             tile, 
6996             x, y,
6997             level,
6998             levelOpacity, 
6999             currentTime 
7000         );
7001     } else if ( tile.loading ) {
7002         // the tile is already in the download queue 
7003         // thanks josh1093 for finally translating this typo
7004     } else {
7005         best = compareTiles( best, tile );
7006     }
7007 
7008     return best;
7009 };
7010 
7011 function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) {
7012     var xMod,
7013         yMod,
7014         bounds,
7015         exists,
7016         url,
7017         tile;
7018 
7019     if ( !tilesMatrix[ level ] ) {
7020         tilesMatrix[ level ] = {};
7021     }
7022     if ( !tilesMatrix[ level ][ x ] ) {
7023         tilesMatrix[ level ][ x ] = {};
7024     }
7025 
7026     if ( !tilesMatrix[ level ][ x ][ y ] ) {
7027         xMod    = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
7028         yMod    = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
7029         bounds  = tileSource.getTileBounds( level, xMod, yMod );
7030         exists  = tileSource.tileExists( level, xMod, yMod );
7031         url     = tileSource.getTileUrl( level, xMod, yMod );
7032 
7033         bounds.x += 1.0 * ( x - xMod ) / numTiles.x;
7034         bounds.y += normHeight * ( y - yMod ) / numTiles.y;
7035 
7036         tilesMatrix[ level ][ x ][ y ] = new $.Tile(
7037             level, 
7038             x, 
7039             y, 
7040             bounds, 
7041             exists, 
7042             url
7043         );
7044     }
7045 
7046     tile = tilesMatrix[ level ][ x ][ y ];
7047     tile.lastTouchTime = time;
7048 
7049     return tile;
7050 };
7051 
7052 
7053 function loadTile( drawer, tile, time ) {
7054     tile.loading = drawer.loadImage(
7055         tile.url,
7056         function( image ){
7057             onTileLoad( drawer, tile, time, image );
7058         }
7059     );
7060 };
7061 
7062 function onTileLoad( drawer, tile, time, image ) {
7063     var insertionIndex,
7064         cutoff,
7065         worstTile,
7066         worstTime,
7067         worstLevel,
7068         worstTileIndex,
7069         prevTile,
7070         prevTime,
7071         prevLevel,
7072         i;
7073 
7074     tile.loading = false;
7075 
7076     if ( drawer.midUpdate ) {
7077         $.console.warn( "Tile load callback in middle of drawing routine." );
7078         return;
7079     } else if ( !image ) {
7080         $.console.log( "Tile %s failed to load: %s", tile, tile.url );
7081         tile.exists = false;
7082         return;
7083     } else if ( time < drawer.lastResetTime ) {
7084         $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
7085         return;
7086     }
7087 
7088     tile.loaded = true;
7089     tile.image  = image;
7090 
7091     insertionIndex = drawer.tilesLoaded.length;
7092 
7093     if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) {
7094         cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) );
7095 
7096         worstTile       = null;
7097         worstTileIndex  = -1;
7098 
7099         for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) {
7100             prevTile = drawer.tilesLoaded[ i ];
7101 
7102             if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) {
7103                 continue;
7104             } else if ( !worstTile ) {
7105                 worstTile       = prevTile;
7106                 worstTileIndex  = i;
7107                 continue;
7108             }
7109 
7110             prevTime    = prevTile.lastTouchTime;
7111             worstTime   = worstTile.lastTouchTime;
7112             prevLevel   = prevTile.level;
7113             worstLevel  = worstTile.level;
7114 
7115             if ( prevTime < worstTime || 
7116                ( prevTime == worstTime && prevLevel > worstLevel ) ) {
7117                 worstTile       = prevTile;
7118                 worstTileIndex  = i;
7119             }
7120         }
7121 
7122         if ( worstTile && worstTileIndex >= 0 ) {
7123             worstTile.unload();
7124             insertionIndex = worstTileIndex;
7125         }
7126     }
7127 
7128     drawer.tilesLoaded[ insertionIndex ] = tile;
7129     drawer.updateAgain = true;
7130 };
7131 
7132 
7133 function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){
7134     var boundsTL     = tile.bounds.getTopLeft(),
7135         boundsSize   = tile.bounds.getSize(),
7136         positionC    = viewport.pixelFromPoint( boundsTL, true ),
7137         positionT    = viewport.pixelFromPoint( boundsTL, false ),
7138         sizeC        = viewport.deltaPixelsFromPoints( boundsSize, true ),
7139         sizeT        = viewport.deltaPixelsFromPoints( boundsSize, false ),
7140         tileCenter   = positionT.plus( sizeT.divide( 2 ) ),
7141         tileDistance = viewportCenter.distanceTo( tileCenter );
7142 
7143     if ( !overlap ) {
7144         sizeC = sizeC.plus( new $.Point( 1, 1 ) );
7145     }
7146 
7147     tile.position   = positionC;
7148     tile.size       = sizeC;
7149     tile.distance   = tileDistance;
7150     tile.visibility = levelVisibility;
7151 };
7152 
7153 
7154 function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){
7155     var blendTimeMillis = 1000 * drawer.blendTime,
7156         deltaTime,
7157         opacity;
7158 
7159     if ( !tile.blendStart ) {
7160         tile.blendStart = currentTime;
7161     }
7162 
7163     deltaTime   = currentTime - tile.blendStart;
7164     opacity     = Math.min( 1, deltaTime / blendTimeMillis );
7165     
7166     if ( drawer.alwaysBlend ) {
7167         opacity *= levelOpacity;
7168     }
7169 
7170     tile.opacity = opacity;
7171 
7172     drawer.lastDrawn.push( tile );
7173 
7174     if ( opacity == 1 ) {
7175         setCoverage( drawer.coverage, level, x, y, true );
7176     } else if ( deltaTime < blendTimeMillis ) {
7177         return true;
7178     }
7179 
7180     return false;
7181 };
7182 
7183 
7184 function clearTiles( drawer ) {
7185     drawer.tilesMatrix = {};
7186     drawer.tilesLoaded = [];
7187 };
7188 
7189 /**
7190  * @private
7191  * @inner
7192  * Returns true if the given tile provides coverage to lower-level tiles of
7193  * lower resolution representing the same content. If neither x nor y is
7194  * given, returns true if the entire visible level provides coverage.
7195  * 
7196  * Note that out-of-bounds tiles provide coverage in this sense, since
7197  * there's no content that they would need to cover. Tiles at non-existent
7198  * levels that are within the image bounds, however, do not.
7199  */
7200 function providesCoverage( coverage, level, x, y ) {
7201     var rows,
7202         cols,
7203         i, j;
7204 
7205     if ( !coverage[ level ] ) {
7206         return false;
7207     }
7208 
7209     if ( x === undefined || y === undefined ) {
7210         rows = coverage[ level ];
7211         for ( i in rows ) {
7212             if ( rows.hasOwnProperty( i ) ) {
7213                 cols = rows[ i ];
7214                 for ( j in cols ) {
7215                     if ( cols.hasOwnProperty( j ) && !cols[ j ] ) {
7216                         return false;
7217                     }
7218                 }
7219             }
7220         }
7221 
7222         return true;
7223     }
7224 
7225     return (
7226         coverage[ level ][ x] === undefined ||
7227         coverage[ level ][ x ][ y ] === undefined ||
7228         coverage[ level ][ x ][ y ] === true
7229     );
7230 };
7231 
7232 /**
7233  * @private
7234  * @inner
7235  * Returns true if the given tile is completely covered by higher-level
7236  * tiles of higher resolution representing the same content. If neither x
7237  * nor y is given, returns true if the entire visible level is covered.
7238  */
7239 function isCovered( coverage, level, x, y ) {
7240     if ( x === undefined || y === undefined ) {
7241         return providesCoverage( coverage, level + 1 );
7242     } else {
7243         return (
7244              providesCoverage( coverage, level + 1, 2 * x, 2 * y ) &&
7245              providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) &&
7246              providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) &&
7247              providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 )
7248         );
7249     }
7250 };
7251 
7252 /**
7253  * @private
7254  * @inner
7255  * Sets whether the given tile provides coverage or not.
7256  */
7257 function setCoverage( coverage, level, x, y, covers ) {
7258     if ( !coverage[ level ] ) {
7259         $.console.warn(
7260             "Setting coverage for a tile before its level's coverage has been reset: %s", 
7261             level
7262         );
7263         return;
7264     }
7265 
7266     if ( !coverage[ level ][ x ] ) {
7267         coverage[ level ][ x ] = {};
7268     }
7269 
7270     coverage[ level ][ x ][ y ] = covers;
7271 };
7272 
7273 /**
7274  * @private
7275  * @inner
7276  * Resets coverage information for the given level. This should be called
7277  * after every draw routine. Note that at the beginning of the next draw
7278  * routine, coverage for every visible tile should be explicitly set. 
7279  */
7280 function resetCoverage( coverage, level ) {
7281     coverage[ level ] = {};
7282 };
7283 
7284 /**
7285  * @private
7286  * @inner
7287  * Determines the 'z-index' of the given overlay.  Overlays are ordered in
7288  * a z-index based on the order they are added to the Drawer.
7289  */
7290 function getOverlayIndex( overlays, element ) {
7291     var i;
7292     for ( i = overlays.length - 1; i >= 0; i-- ) {
7293         if ( overlays[ i ].element == element ) {
7294             return i;
7295         }
7296     }
7297 
7298     return -1;
7299 };
7300 
7301 /**
7302  * @private
7303  * @inner
7304  * Determines whether the 'last best' tile for the area is better than the 
7305  * tile in question.
7306  */
7307 function compareTiles( previousBest, tile ) {
7308     if ( !previousBest ) {
7309         return tile;
7310     }
7311 
7312     if ( tile.visibility > previousBest.visibility ) {
7313         return tile;
7314     } else if ( tile.visibility == previousBest.visibility ) {
7315         if ( tile.distance < previousBest.distance ) {
7316             return tile;
7317         }
7318     }
7319 
7320     return previousBest;
7321 };
7322 
7323 function finishLoadingImage( image, callback, successful, jobid ){
7324 
7325     image.onload = null;
7326     image.onabort = null;
7327     image.onerror = null;
7328 
7329     if ( jobid ) {
7330         window.clearTimeout( jobid );
7331     }
7332     window.setTimeout( function() {
7333         callback( image.src, successful ? image : null);
7334     }, 1 );
7335 
7336 };
7337 
7338 
7339 function drawOverlays( viewport, overlays, container ){
7340     var i,
7341         length = overlays.length;
7342     for ( i = 0; i < length; i++ ) {
7343         drawOverlay( viewport, overlays[ i ], container );
7344     }
7345 };
7346 
7347 function drawOverlay( viewport, overlay, container ){
7348 
7349     overlay.position = viewport.pixelFromPoint(
7350         overlay.bounds.getTopLeft(), 
7351         true
7352     );
7353     overlay.size     = viewport.deltaPixelsFromPoints(
7354         overlay.bounds.getSize(), 
7355         true
7356     );
7357     overlay.drawHTML( container );
7358 };
7359 
7360 function drawTiles( drawer, lastDrawn ){
7361     var i, 
7362         tile;
7363 
7364     for ( i = lastDrawn.length - 1; i >= 0; i-- ) {
7365         tile = lastDrawn[ i ];
7366 
7367         //TODO: get rid of this if by determining the tile draw method once up
7368         //      front and defining the appropriate 'draw' function
7369         if ( USE_CANVAS ) {
7370             tile.drawCanvas( drawer.context );
7371         } else {
7372             tile.drawHTML( drawer.canvas );
7373         }
7374 
7375         tile.beingDrawn = true;
7376     }
7377 };
7378 
7379 }( OpenSeadragon ));
7380 
7381 (function( $ ){
7382 
7383 
7384 /**
7385  * @class
7386  */
7387 $.Viewport = function( options ) {
7388 
7389     //backward compatibility for positional args while prefering more 
7390     //idiomatic javascript options object as the only argument
7391     var args = arguments;
7392     if(  args.length && args[ 0 ] instanceof $.Point ){
7393         options = {
7394             containerSize:  args[ 0 ],
7395             contentSize:    args[ 1 ],
7396             config:         args[ 2 ]
7397         };
7398     }
7399 
7400     //options.config and the general config argument are deprecated
7401     //in favor of the more direct specification of optional settings
7402     //being pass directly on the options object
7403     if ( options.config ){
7404         $.extend( true, options, options.config );
7405         delete options.config;
7406     }
7407 
7408     $.extend( true, this, {
7409         
7410         //required settings
7411         containerSize:      null,
7412         contentSize:        null,
7413 
7414         //internal state properties
7415         zoomPoint:          null,
7416 
7417         //configurable options
7418         springStiffness:    $.DEFAULT_SETTINGS.springStiffness,
7419         animationTime:      $.DEFAULT_SETTINGS.animationTime,
7420         minZoomImageRatio:  $.DEFAULT_SETTINGS.minZoomImageRatio,
7421         maxZoomPixelRatio:  $.DEFAULT_SETTINGS.maxZoomPixelRatio,
7422         visibilityRatio:    $.DEFAULT_SETTINGS.visibilityRatio,
7423         wrapHorizontal:     $.DEFAULT_SETTINGS.wrapHorizontal,
7424         wrapVertical:       $.DEFAULT_SETTINGS.wrapVertical
7425 
7426     }, options );
7427 
7428     this.centerSpringX = new $.Spring({
7429         initial: 0, 
7430         springStiffness: this.springStiffness,
7431         animationTime:   this.animationTime
7432     });
7433     this.centerSpringY = new $.Spring({
7434         initial: 0, 
7435         springStiffness: this.springStiffness,
7436         animationTime:   this.animationTime
7437     });
7438     this.zoomSpring    = new $.Spring({
7439         initial: 1, 
7440         springStiffness: this.springStiffness,
7441         animationTime:   this.animationTime
7442     });
7443 
7444     this.resetContentSize( this.contentSize );
7445     this.goHome( true );
7446     //this.fitHorizontally( true );
7447     this.update();
7448 };
7449 
7450 $.Viewport.prototype = {
7451 
7452     resetContentSize: function( contentSize ){
7453         this.contentSize    = contentSize;
7454         this.contentAspectX = this.contentSize.x / this.contentSize.y;
7455         this.contentAspectY = this.contentSize.y / this.contentSize.x;
7456         this.homeBounds     = new $.Rect( 
7457             0, 
7458             0, 
7459             1, 
7460             this.contentAspectY
7461         );
7462         this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX );
7463         this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY );
7464     },
7465 
7466     /**
7467      * @function
7468      */
7469     getHomeZoom: function() {
7470         
7471         var aspectFactor = Math.min( 
7472             this.contentAspectX, 
7473             this.contentAspectY 
7474         ) / this.getAspectRatio();
7475 
7476         return ( aspectFactor >= 1 ) ? 
7477             1 : 
7478             aspectFactor;
7479     },
7480 
7481     /**
7482      * @function
7483      */
7484     getMinZoom: function() {
7485         var homeZoom = this.getHomeZoom()
7486             zoom = this.minZoomImageRatio * homeZoom;
7487 
7488         return Math.min( zoom, homeZoom );
7489     },
7490 
7491     /**
7492      * @function
7493      */
7494     getMaxZoom: function() {
7495         var zoom = 
7496             this.contentSize.x * 
7497             this.maxZoomPixelRatio / 
7498             this.containerSize.x;
7499         return Math.max( zoom, this.getHomeZoom() );
7500     },
7501 
7502     /**
7503      * @function
7504      */
7505     getAspectRatio: function() {
7506         return this.containerSize.x / this.containerSize.y;
7507     },
7508 
7509     /**
7510      * @function
7511      */
7512     getContainerSize: function() {
7513         return new $.Point(
7514             this.containerSize.x, 
7515             this.containerSize.y
7516         );
7517     },
7518 
7519     /**
7520      * @function
7521      */
7522     getBounds: function( current ) {
7523         var center = this.getCenter( current ),
7524             width  = 1.0 / this.getZoom( current ),
7525             height = width / this.getAspectRatio();
7526 
7527         return new $.Rect(
7528             center.x - ( width / 2.0 ), 
7529             center.y - ( height / 2.0 ),
7530             width, 
7531             height
7532         );
7533     },
7534 
7535     /**
7536      * @function
7537      */
7538     getCenter: function( current ) {
7539         var centerCurrent = new $.Point(
7540                 this.centerSpringX.current.value,
7541                 this.centerSpringY.current.value
7542             ),
7543             centerTarget = new $.Point(
7544                 this.centerSpringX.target.value,
7545                 this.centerSpringY.target.value
7546             ),
7547             oldZoomPixel,
7548             zoom,
7549             width,
7550             height,
7551             bounds,
7552             newZoomPixel,
7553             deltaZoomPixels,
7554             deltaZoomPoints;
7555 
7556         if ( current ) {
7557             return centerCurrent;
7558         } else if ( !this.zoomPoint ) {
7559             return centerTarget;
7560         }
7561 
7562         oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
7563 
7564         zoom    = this.getZoom();
7565         width   = 1.0 / zoom;
7566         height  = width / this.getAspectRatio();
7567         bounds  = new $.Rect(
7568             centerCurrent.x - width / 2.0,
7569             centerCurrent.y - height / 2.0, 
7570             width, 
7571             height
7572         );
7573 
7574         newZoomPixel    = this.zoomPoint.minus(
7575             bounds.getTopLeft()
7576         ).times(
7577             this.containerSize.x / bounds.width
7578         );
7579         deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
7580         deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom );
7581 
7582         return centerTarget.plus( deltaZoomPoints );
7583     },
7584 
7585     /**
7586      * @function
7587      */
7588     getZoom: function( current ) {
7589         if ( current ) {
7590             return this.zoomSpring.current.value;
7591         } else {
7592             return this.zoomSpring.target.value;
7593         }
7594     },
7595 
7596 
7597     /**
7598      * @function
7599      */
7600     applyConstraints: function( immediately ) {
7601         var actualZoom = this.getZoom(),
7602             constrainedZoom = Math.max(
7603                 Math.min( actualZoom, this.getMaxZoom() ), 
7604                 this.getMinZoom()
7605             ),
7606             bounds,
7607             horizontalThreshold,
7608             verticalThreshold,
7609             left,
7610             right,
7611             top,
7612             bottom,
7613             dx = 0,
7614             dy = 0;
7615 
7616         if ( actualZoom != constrainedZoom ) {
7617             this.zoomTo( constrainedZoom, this.zoomPoint, immediately );
7618         }
7619 
7620         bounds = this.getBounds();
7621 
7622         horizontalThreshold = this.visibilityRatio * bounds.width;
7623         verticalThreshold   = this.visibilityRatio * bounds.height;
7624 
7625         left   = bounds.x + bounds.width;
7626         right  = 1 - bounds.x;
7627         top    = bounds.y + bounds.height;
7628         bottom = this.contentAspectY - bounds.y;
7629 
7630         if ( this.wrapHorizontal ) {
7631             //do nothing
7632         } else if ( left < horizontalThreshold ) {
7633             dx = horizontalThreshold - left;
7634         } else if ( right < horizontalThreshold ) {
7635             dx = right - horizontalThreshold;
7636         }
7637 
7638         if ( this.wrapVertical ) {
7639             //do nothing
7640         } else if ( top < verticalThreshold ) {
7641             dy = verticalThreshold - top;
7642         } else if ( bottom < verticalThreshold ) {
7643             dy = bottom - verticalThreshold;
7644         }
7645 
7646         if ( dx || dy ) {
7647             bounds.x += dx;
7648             bounds.y += dy;
7649             this.fitBounds( bounds, immediately );
7650         }
7651     },
7652 
7653     /**
7654      * @function
7655      * @param {Boolean} immediately
7656      */
7657     ensureVisible: function( immediately ) {
7658         this.applyConstraints( immediately );
7659     },
7660 
7661     /**
7662      * @function
7663      * @param {OpenSeadragon.Rect} bounds
7664      * @param {Boolean} immediately
7665      */
7666     fitBounds: function( bounds, immediately ) {
7667         var aspect = this.getAspectRatio(),
7668             center = bounds.getCenter(),
7669             newBounds = new $.Rect(
7670                 bounds.x, 
7671                 bounds.y, 
7672                 bounds.width, 
7673                 bounds.height
7674             ),
7675             oldBounds,
7676             oldZoom,
7677             newZoom,
7678             referencePoint;
7679 
7680         if ( newBounds.getAspectRatio() >= aspect ) {
7681             newBounds.height = bounds.width / aspect;
7682             newBounds.y      = center.y - newBounds.height / 2;
7683         } else {
7684             newBounds.width = bounds.height * aspect;
7685             newBounds.x     = center.x - newBounds.width / 2;
7686         }
7687 
7688         this.panTo( this.getCenter( true ), true );
7689         this.zoomTo( this.getZoom( true ), null, true );
7690 
7691         oldBounds = this.getBounds();
7692         oldZoom   = this.getZoom();
7693         newZoom   = 1.0 / newBounds.width;
7694         if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) {
7695             this.panTo( center, immediately );
7696             return;
7697         }
7698 
7699         referencePoint = oldBounds.getTopLeft().times( 
7700             this.containerSize.x / oldBounds.width 
7701         ).minus(
7702             newBounds.getTopLeft().times( 
7703                 this.containerSize.x / newBounds.width 
7704             )
7705         ).divide(
7706             this.containerSize.x / oldBounds.width - 
7707             this.containerSize.x / newBounds.width
7708         );
7709 
7710         this.zoomTo( newZoom, referencePoint, immediately );
7711     },
7712     
7713     /**
7714      * @function
7715      * @param {Boolean} immediately
7716      */
7717     goHome: function( immediately ) {
7718         return this.fitVertically( immediately );
7719     },
7720 
7721     /**
7722      * @function
7723      * @param {Boolean} immediately
7724      */
7725     fitVertically: function( immediately ) {
7726         var center = this.getCenter();
7727 
7728         if ( this.wrapHorizontal ) {
7729             center.x = ( 1 + ( center.x % 1 ) ) % 1;
7730             this.centerSpringX.resetTo( center.x );
7731             this.centerSpringX.update();
7732         }
7733 
7734         if ( this.wrapVertical ) {
7735             center.y = (
7736                 this.contentAspectY + ( center.y % this.contentAspectY )
7737             ) % this.contentAspectY;
7738             this.centerSpringY.resetTo( center.y );
7739             this.centerSpringY.update();
7740         }
7741 
7742         this.fitBounds( this.homeBounds, immediately );
7743     },
7744 
7745     /**
7746      * @function
7747      * @param {Boolean} immediately
7748      */
7749     fitHorizontally: function( immediately ) {
7750         var center = this.getCenter();
7751 
7752         if ( this.wrapHorizontal ) {
7753             center.x = ( 
7754                 this.contentAspectX + ( center.x % this.contentAspectX ) 
7755             ) % this.contentAspectX;
7756             this.centerSpringX.resetTo( center.x );
7757             this.centerSpringX.update();
7758         }
7759 
7760         if ( this.wrapVertical ) {
7761             center.y = ( 1 + ( center.y % 1 ) ) % 1;
7762             this.centerSpringY.resetTo( center.y );
7763             this.centerSpringY.update();
7764         }
7765 
7766         this.fitBounds( this.fitWidthBounds, immediately );
7767     },
7768 
7769 
7770     /**
7771      * @function
7772      * @param {OpenSeadragon.Point} delta
7773      * @param {Boolean} immediately
7774      */
7775     panBy: function( delta, immediately ) {
7776         var center = new $.Point(
7777             this.centerSpringX.target.value,
7778             this.centerSpringY.target.value
7779         );
7780         this.panTo( center.plus( delta ), immediately );
7781     },
7782 
7783     /**
7784      * @function
7785      * @param {OpenSeadragon.Point} center
7786      * @param {Boolean} immediately
7787      */
7788     panTo: function( center, immediately ) {
7789         if ( immediately ) {
7790             this.centerSpringX.resetTo( center.x );
7791             this.centerSpringY.resetTo( center.y );
7792         } else {
7793             this.centerSpringX.springTo( center.x );
7794             this.centerSpringY.springTo( center.y );
7795         }
7796     },
7797 
7798     /**
7799      * @function
7800      */
7801     zoomBy: function( factor, refPoint, immediately ) {
7802         this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately );
7803     },
7804 
7805     /**
7806      * @function
7807      */
7808     zoomTo: function( zoom, refPoint, immediately ) {
7809 
7810         if ( immediately ) {
7811             this.zoomSpring.resetTo( zoom );
7812         } else {        
7813             this.zoomSpring.springTo( zoom );
7814         }
7815 
7816         this.zoomPoint = refPoint instanceof $.Point ? 
7817             refPoint : 
7818             null;
7819     },
7820 
7821     /**
7822      * @function
7823      */
7824     resize: function( newContainerSize, maintain ) {
7825         var oldBounds = this.getBounds(),
7826             newBounds = oldBounds,
7827             widthDeltaFactor = newContainerSize.x / this.containerSize.x;
7828 
7829         this.containerSize = new $.Point(
7830             newContainerSize.x, 
7831             newContainerSize.y
7832         );
7833 
7834         if (maintain) {
7835             newBounds.width  = oldBounds.width * widthDeltaFactor;
7836             newBounds.height = newBounds.width / this.getAspectRatio();
7837         }
7838 
7839         this.fitBounds( newBounds, true );
7840     },
7841 
7842     /**
7843      * @function
7844      */
7845     update: function() {
7846         var oldCenterX = this.centerSpringX.current.value,
7847             oldCenterY = this.centerSpringY.current.value,
7848             oldZoom    = this.zoomSpring.current.value,
7849             oldZoomPixel,
7850             newZoomPixel,
7851             deltaZoomPixels,
7852             deltaZoomPoints;
7853 
7854         if (this.zoomPoint) {
7855             oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true );
7856         }
7857 
7858         this.zoomSpring.update();
7859 
7860         if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) {
7861             newZoomPixel    = this.pixelFromPoint( this.zoomPoint, true );
7862             deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
7863             deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true );
7864 
7865             this.centerSpringX.shiftBy( deltaZoomPoints.x );
7866             this.centerSpringY.shiftBy( deltaZoomPoints.y );
7867         } else {
7868             this.zoomPoint = null;
7869         }
7870 
7871         this.centerSpringX.update();
7872         this.centerSpringY.update();
7873 
7874         return this.centerSpringX.current.value != oldCenterX ||
7875             this.centerSpringY.current.value != oldCenterY ||
7876             this.zoomSpring.current.value != oldZoom;
7877     },
7878 
7879 
7880     /**
7881      * @function
7882      */
7883     deltaPixelsFromPoints: function( deltaPoints, current ) {
7884         return deltaPoints.times(
7885             this.containerSize.x * this.getZoom( current )
7886         );
7887     },
7888 
7889     /**
7890      * @function
7891      */
7892     deltaPointsFromPixels: function( deltaPixels, current ) {
7893         return deltaPixels.divide(
7894             this.containerSize.x * this.getZoom( current )
7895         );
7896     },
7897 
7898     /**
7899      * @function
7900      */
7901     pixelFromPoint: function( point, current ) {
7902         var bounds = this.getBounds( current );
7903         return point.minus(
7904             bounds.getTopLeft()
7905         ).times(
7906             this.containerSize.x / bounds.width
7907         );
7908     },
7909 
7910     /**
7911      * @function
7912      */
7913     pointFromPixel: function( pixel, current ) {
7914         var bounds = this.getBounds( current );
7915         return pixel.divide(
7916             this.containerSize.x / bounds.width
7917         ).plus(
7918             bounds.getTopLeft()
7919         );
7920     }
7921 };
7922 
7923 }( OpenSeadragon ));
7924